Skip to content

Commit 60d2c83

Browse files
apurva-metafacebook-github-bot
authored andcommitted
feat: Add stripe stats pruning (#970)
Summary: Add a stripe-level vectorized stats optional section for Nimble files and use it in the selective reader to conservatively skip stripes for integral filters when min/max proves no rows can match. The writer snapshots per-stripe column stats and reconstructs file-level stats by merging those snapshots, preserving a fallback path for files that do not emit stripe snapshots. The reader preloads and deserializes the new optional stats section while treating absent stats as a normal full-scan fallback. Deduplicated columns are now handled correctly across stripes. mergeColumnStats merges into the wrapped base statistics (which back the delegating getters) and accumulates the dedup counters, instead of writing to unused fields. The per-stripe ancestor deduplication wrapping applied during finalization is undone between stripes (releasing the base collector) so every stripe finalizes from an identical collector structure; without this, stripe 1+ skipped the child logical-size rollup and the reconstructed file-level stats were wrong (tripping the raw-size consistency check). Genuine deduplicated nodes configured at init stay wrapped. Differential Revision: D112143149
1 parent 5ebbe62 commit 60d2c83

14 files changed

Lines changed: 700 additions & 24 deletions

dwio/nimble/tablet/Constants.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ constexpr std::string_view kMetadataSection = "columnar.metadata";
3434
constexpr std::string_view kStatsSection = "columnar.stats";
3535
constexpr std::string_view kVectorizedStatsSection =
3636
"columnar.vectorized_stats";
37+
constexpr std::string_view kStripeStatsSection = "columnar.stripe_stats";
3738
constexpr std::string_view kClusterIndexSection = "columnar.cluster.index";
3839
constexpr std::string_view kChunkIndexSection = "columnar.chunk.index";
3940
constexpr std::string_view kHashIndexSection = "columnar.hash.index";

dwio/nimble/tablet/TabletReader.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ TabletReader::Options TabletReader::configureOptions(
7474
Options tabletOptions;
7575
tabletOptions.maxFooterIoBytes = options.footerSpeculativeIoSize();
7676
tabletOptions.preloadOptionalSections = {
77-
std::string(kSchemaSection), std::string(kVectorizedStatsSection)};
77+
std::string(kSchemaSection),
78+
std::string(kVectorizedStatsSection),
79+
std::string(kStripeStatsSection)};
7880
tabletOptions.loadClusterIndex = options.loadClusterIndex();
7981
if (tabletOptions.loadClusterIndex) {
8082
tabletOptions.preloadOptionalSections.emplace_back(kClusterIndexSection);

dwio/nimble/velox/FieldWriter.h

Lines changed: 135 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -299,22 +299,19 @@ class FieldWriterContext {
299299
}
300300

301301
inline std::vector<ColumnStatistics*> columnStats() {
302-
if (!statsFinalized_) {
303-
return {};
304-
}
305-
// TODO: add a build method for this pattern.
306302
std::vector<ColumnStatistics*> statsViews;
307-
statsViews.reserve(statsCollectors_.size());
308-
for (auto& collector : statsCollectors_) {
309-
// FIXME: don't need this branching.
310-
statsViews.push_back(
311-
collector->shared()
312-
? collector->as<SharedStatisticsCollector>()->getBaseStatistics()
313-
: collector->getStatsView());
303+
statsViews.reserve(fileStats_.size());
304+
for (auto& stat : fileStats_) {
305+
statsViews.push_back(stat.get());
314306
}
315307
return statsViews;
316308
}
317309

310+
inline const std::vector<std::vector<std::unique_ptr<ColumnStatistics>>>&
311+
stripeStats() const {
312+
return stripeStats_;
313+
}
314+
318315
void handleFlatmapFieldAddEvent(
319316
const TypeBuilder& typeBuilder,
320317
std::string_view flatmapKey,
@@ -479,10 +476,40 @@ class FieldWriterContext {
479476
// 1. roll up logical and physical sizes
480477
// 2. wrap all ancestors of deduplicated types
481478
// 3. backfill both known and newly wrapped deduplicated stats
482-
void finalizeStatsCollectors() {
479+
void finalizeStripeStatsCollectors() {
483480
NIMBLE_CHECK(!statsFinalized_);
484481
finalizeStatsCollector(schemaWithId_);
485482
statsFinalized_ = true;
483+
snapshotStripeStats();
484+
resetStatsCollectors();
485+
}
486+
487+
void finalizeFileStatsFromStripes() {
488+
fileStats_.clear();
489+
if (stripeStats_.empty()) {
490+
if (!statsFinalized_) {
491+
finalizeStatsCollector(schemaWithId_);
492+
statsFinalized_ = true;
493+
}
494+
fileStats_.reserve(statsCollectors_.size());
495+
for (auto& collector : statsCollectors_) {
496+
auto* stats = collector->shared()
497+
? collector->as<SharedStatisticsCollector>()->getBaseStatistics()
498+
: collector->getStatsView();
499+
fileStats_.push_back(stats->clone());
500+
}
501+
return;
502+
}
503+
fileStats_.reserve(stripeStats_.front().size());
504+
for (const auto& stat : stripeStats_.front()) {
505+
fileStats_.push_back(stat->clone());
506+
}
507+
for (size_t stripe = 1; stripe < stripeStats_.size(); ++stripe) {
508+
NIMBLE_CHECK_EQ(stripeStats_[stripe].size(), fileStats_.size());
509+
for (size_t column = 0; column < fileStats_.size(); ++column) {
510+
mergeColumnStats(*fileStats_[column], *stripeStats_[stripe][column]);
511+
}
512+
}
486513
}
487514

488515
protected:
@@ -513,6 +540,100 @@ class FieldWriterContext {
513540
[](TypeBuilder&, uint32_t) {}};
514541

515542
private:
543+
// Merges min/max from source into target for a scalar statistics subclass
544+
// (Integral/FloatingPoint/String). Keeps the widest range across stripes.
545+
template <typename StatsT>
546+
static void mergeMinMax(
547+
ColumnStatistics& target,
548+
const ColumnStatistics& source) {
549+
auto* targetStats = target.as<StatsT>();
550+
const auto* sourceStats = source.as<StatsT>();
551+
if (sourceStats->getMin().has_value() &&
552+
(!targetStats->getMin().has_value() ||
553+
*sourceStats->getMin() < *targetStats->getMin())) {
554+
targetStats->min_ = sourceStats->getMin();
555+
}
556+
if (sourceStats->getMax().has_value() &&
557+
(!targetStats->getMax().has_value() ||
558+
*sourceStats->getMax() > *targetStats->getMax())) {
559+
targetStats->max_ = sourceStats->getMax();
560+
}
561+
}
562+
563+
static void mergeColumnStats(
564+
ColumnStatistics& target,
565+
const ColumnStatistics& source) {
566+
NIMBLE_CHECK_EQ(target.getType(), source.getType());
567+
// Deduplicated stats delegate their base metrics (value/null count, sizes)
568+
// to the wrapped base statistics, so merge into that base rather than
569+
// target's own (unused) member fields, and accumulate the dedup-specific
570+
// counters separately.
571+
if (target.getType() == StatType::DEDUPLICATED) {
572+
auto* targetStats = target.as<DeduplicatedColumnStatistics>();
573+
const auto* sourceStats = source.as<DeduplicatedColumnStatistics>();
574+
targetStats->dedupedCount_ += sourceStats->getDedupedCount();
575+
targetStats->dedupedLogicalSize_ += sourceStats->getDedupedLogicalSize();
576+
auto* targetBase = targetStats->getBaseStatistics();
577+
const auto* sourceBase = sourceStats->getBaseStatistics();
578+
if (targetBase != nullptr && sourceBase != nullptr) {
579+
mergeColumnStats(*targetBase, *sourceBase);
580+
}
581+
return;
582+
}
583+
584+
target.valueCount_ += source.getValueCount();
585+
target.nullCount_ += source.getNullCount();
586+
target.logicalSize_ += source.getLogicalSize();
587+
target.physicalSize_ += source.getPhysicalSize();
588+
switch (target.getType()) {
589+
case StatType::INTEGRAL:
590+
mergeMinMax<IntegralStatistics>(target, source);
591+
break;
592+
case StatType::FLOATING_POINT:
593+
mergeMinMax<FloatingPointStatistics>(target, source);
594+
break;
595+
case StatType::STRING:
596+
mergeMinMax<StringStatistics>(target, source);
597+
break;
598+
case StatType::DEFAULT:
599+
case StatType::DEDUPLICATED:
600+
break;
601+
}
602+
}
603+
604+
void snapshotStripeStats() {
605+
auto& stripe = stripeStats_.emplace_back();
606+
stripe.reserve(statsCollectors_.size());
607+
for (auto& collector : statsCollectors_) {
608+
auto* stats = collector->shared()
609+
? collector->as<SharedStatisticsCollector>()->getBaseStatistics()
610+
: collector->getStatsView();
611+
stripe.push_back(stats->clone());
612+
}
613+
}
614+
615+
void resetStatsCollectors() {
616+
// finalizeStatsCollector() wraps ancestors of deduplicated nodes into a
617+
// DeduplicatedStatisticsCollector in place. That wrapping must be undone
618+
// between stripes; otherwise the next stripe's finalization would take the
619+
// "already deduplicated" branch and skip rolling child logical/physical
620+
// sizes up into the ancestor, corrupting the file-level stats
621+
// reconstruction. Genuine deduplicated nodes (configured at init via
622+
// dictionary array / deduplicated map ids) must remain wrapped.
623+
for (uint32_t id = 0; id < statsCollectors_.size(); ++id) {
624+
auto& collector = statsCollectors_[id];
625+
if (collector->getType() == StatType::DEDUPLICATED &&
626+
!hasDictionaryArrayNodeId(id) && !hasDeduplicatedMapNodeId(id)) {
627+
collector = collector->asChecked<DeduplicatedStatisticsCollector>()
628+
->releaseBaseCollector();
629+
}
630+
}
631+
for (auto& collector : statsCollectors_) {
632+
collector->reset();
633+
}
634+
statsFinalized_ = false;
635+
}
636+
516637
void finalizeStatsCollector(
517638
const std::shared_ptr<const velox::dwio::common::TypeWithId>& type) {
518639
auto statsCollector = statsCollectors_[type->id()].get();
@@ -627,6 +748,8 @@ class FieldWriterContext {
627748
std::vector<std::pair<uint32_t, std::unique_ptr<StreamData>>> streams_;
628749
std::shared_ptr<const velox::dwio::common::TypeWithId> schemaWithId_;
629750
std::vector<std::unique_ptr<StatisticsCollector>> statsCollectors_;
751+
std::vector<std::vector<std::unique_ptr<ColumnStatistics>>> stripeStats_;
752+
std::vector<std::unique_ptr<ColumnStatistics>> fileStats_;
630753
bool statsFinalized_{false};
631754
};
632755

dwio/nimble/velox/VeloxWriter.cpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,7 @@ void VeloxWriter::writeMetadata() {
954954
}
955955

956956
void VeloxWriter::writeColumnStats() {
957+
context_->finalizeFileStatsFromStripes();
957958
// When enableStatsConsistencyCheck is true, verify that fileRawSize
958959
// (accumulated via RawSizeUtils) matches the root column statistics.
959960
if (context_->options().enableStatsConsistencyCheck) {
@@ -969,6 +970,12 @@ void VeloxWriter::writeColumnStats() {
969970
Buffer buffer{*encodingMemoryPool_};
970971
tabletWriter_->writeOptionalSection(
971972
std::string(kVectorizedStatsSection), fileStats.serialize(buffer));
973+
VectorizedStripeStats stripeStats{
974+
context_->stripeStats(), encodingMemoryPool_.get()};
975+
Buffer stripeStatsBuffer{*encodingMemoryPool_};
976+
tabletWriter_->writeOptionalSection(
977+
std::string(kStripeStatsSection),
978+
stripeStats.serialize(stripeStatsBuffer));
972979
} else {
973980
flatbuffers::FlatBufferBuilder builder;
974981
builder.Finish(
@@ -1086,9 +1093,6 @@ void VeloxWriter::close() {
10861093
try {
10871094
writeStripe();
10881095
rootWriter_->close();
1089-
if (context_->options().enableStatsCollection) {
1090-
context_->finalizeStatsCollectors();
1091-
}
10921096

10931097
writeMetadata();
10941098
if (context_->options().enableStatsCollection) {
@@ -1553,6 +1557,9 @@ bool VeloxWriter::writeStripe() {
15531557
} else {
15541558
writeStreams();
15551559
}
1560+
if (context_->options().enableStatsCollection) {
1561+
context_->finalizeStripeStatsCollectors();
1562+
}
15561563

15571564
uint64_t stripeSize{0};
15581565
{

dwio/nimble/velox/selective/ReaderBase.cpp

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,32 @@ ReaderBase::ReaderBase(
153153
return {};
154154
}
155155
return fileStats->toColumnStatistics(fileSchema_, nimbleSchema_);
156-
}()} {}
156+
}()},
157+
stripeColumnStats_{
158+
[&]() -> std::vector<std::vector<std::unique_ptr<ColumnStatistics>>> {
159+
auto statsSection =
160+
tablet_->loadOptionalSection(std::string(kStripeStatsSection));
161+
if (!statsSection.has_value()) {
162+
return {};
163+
}
164+
auto stripeStats = VectorizedStripeStats::deserialize(
165+
statsSection->content(), *pool_);
166+
if (!stripeStats) {
167+
return {};
168+
}
169+
const auto& stats = stripeStats->toStripeColumnStatistics(
170+
fileSchema_, nimbleSchema_);
171+
std::vector<std::vector<std::unique_ptr<ColumnStatistics>>> result;
172+
result.reserve(stats.size());
173+
for (const auto& stripe : stats) {
174+
auto& resultStripe = result.emplace_back();
175+
resultStripe.reserve(stripe.size());
176+
for (const auto& stat : stripe) {
177+
resultStripe.push_back(stat->clone());
178+
}
179+
}
180+
return result;
181+
}()} {}
157182

158183
void LazyInput::load() {
159184
if (!loaded_) {

dwio/nimble/velox/selective/ReaderBase.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ class ReaderBase {
118118
return fileColumnStats_;
119119
}
120120

121+
const std::vector<std::vector<std::unique_ptr<ColumnStatistics>>>&
122+
stripeColumnStats() const {
123+
return stripeColumnStats_;
124+
}
125+
121126
private:
122127
ReaderBase(
123128
std::unique_ptr<velox::dwio::common::BufferedInput> input,
@@ -138,6 +143,8 @@ class ReaderBase {
138143
// File-level column statistics deserialized from the vectorized stats
139144
// optional section at construction.
140145
const std::vector<std::unique_ptr<ColumnStatistics>> fileColumnStats_;
146+
const std::vector<std::vector<std::unique_ptr<ColumnStatistics>>>
147+
stripeColumnStats_;
141148
mutable std::shared_ptr<const velox::dwio::common::TypeWithId>
142149
fileSchemaWithId_;
143150
};

dwio/nimble/velox/selective/SelectiveNimbleReader.cpp

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ class SelectiveNimbleRowReader : public dwio::common::RowReader {
204204
// if stats are available.
205205
void computeStatsBasedRowSize() const;
206206

207+
bool shouldSkipStripe(uint32_t stripe) const;
208+
207209
// Computes which top-level columns should use lazy I/O based on the scan
208210
// spec and remaining filter columns. Returns a const set used for the
209211
// lifetime of this reader.
@@ -272,7 +274,14 @@ int64_t SelectiveNimbleRowReader::nextRowNumber() {
272274
readerBase_->randomSkip()->nextSkip() >= numStripeRows) {
273275
readerBase_->randomSkip()->consume(numStripeRows);
274276
++skippedStripes_;
275-
goto advanceToNextStripe;
277+
advanceToNextStripe();
278+
continue;
279+
}
280+
if (shouldSkipStripe(currentStripe_)) {
281+
maybeUpdateRandomSkip(numStripeRows);
282+
++skippedStripes_;
283+
advanceToNextStripe();
284+
continue;
276285
}
277286
loadCurrentStripe();
278287
}
@@ -284,7 +293,6 @@ int64_t SelectiveNimbleRowReader::nextRowNumber() {
284293
nextRowNumber_ = stripeRowOffsets_[currentStripe_] + rowInCurrentStripe_;
285294
return *nextRowNumber_;
286295
}
287-
advanceToNextStripe:
288296
advanceToNextStripe();
289297
}
290298
// Update random skip tracker for trailing rows that were skipped due to upper
@@ -370,6 +378,60 @@ void SelectiveNimbleRowReader::computeStatsBasedRowSize() const {
370378
}
371379
}
372380

381+
bool SelectiveNimbleRowReader::shouldSkipStripe(uint32_t stripe) const {
382+
const auto& stripeStats = readerBase_->stripeColumnStats();
383+
if (stripe >= stripeStats.size() || stripeStats[stripe].empty()) {
384+
return false;
385+
}
386+
const auto& rootType = *readerBase_->fileSchemaWithId();
387+
const auto& rowType = rootType.type()->asRow();
388+
for (auto* childSpec : options_.scanSpec()->stableChildren()) {
389+
if (!childSpec->hasFilter() || childSpec->filter() == nullptr ||
390+
childSpec->isConstant() || !childSpec->readFromFile()) {
391+
continue;
392+
}
393+
const auto columnIndex =
394+
rowType.getChildIdxIfExists(childSpec->fieldName());
395+
if (!columnIndex.has_value()) {
396+
continue;
397+
}
398+
const auto& childType = rootType.childAt(columnIndex.value());
399+
if (childType == nullptr) {
400+
continue;
401+
}
402+
// Only top-level integral columns are pruned here. Use an explicit
403+
// comparison rather than a switch over TypeKind to avoid -Wswitch-enum
404+
// requiring every enumerator to be listed.
405+
const auto kind = childType->type()->kind();
406+
if (kind != TypeKind::TINYINT && kind != TypeKind::SMALLINT &&
407+
kind != TypeKind::INTEGER && kind != TypeKind::BIGINT) {
408+
continue;
409+
}
410+
// Per-stripe stats are laid out by schema type id: the writer snapshots
411+
// statsCollectors_ (indexed by TypeWithId::id()) in order, so
412+
// stripeStats[stripe][id] matches childType->id() just like the file-level
413+
// column stats. The bound check below guards against a stats section that
414+
// covers fewer columns than the current schema.
415+
const auto columnId = childType->id();
416+
if (columnId >= stripeStats[stripe].size() ||
417+
!stripeStats[stripe][columnId]) {
418+
continue;
419+
}
420+
const auto* integralStats =
421+
stripeStats[stripe][columnId]->as<IntegralStatistics>();
422+
if (integralStats == nullptr || !integralStats->getMin().has_value() ||
423+
!integralStats->getMax().has_value()) {
424+
continue;
425+
}
426+
const auto hasNull = integralStats->getNullCount() > 0;
427+
if (!childSpec->filter()->testInt64Range(
428+
*integralStats->getMin(), *integralStats->getMax(), hasNull)) {
429+
return true;
430+
}
431+
}
432+
return false;
433+
}
434+
373435
std::optional<size_t> SelectiveNimbleRowReader::estimatedRowSize() const {
374436
if (const_cast<SelectiveNimbleRowReader*>(this)->nextRowNumber() == kAtEnd) {
375437
return std::nullopt;

0 commit comments

Comments
 (0)