Skip to content

Commit 949180a

Browse files
apurva-metafacebook-github-bot
authored andcommitted
feat: Add stripe stats pruning
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 58482c7 commit 949180a

14 files changed

Lines changed: 773 additions & 25 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: 193 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#pragma once
1818

19+
#include <cmath>
1920
#include <set>
2021

2122
#include <folly/Executor.h>
@@ -299,22 +300,19 @@ class FieldWriterContext {
299300
}
300301

301302
inline std::vector<ColumnStatistics*> columnStats() {
302-
if (!statsFinalized_) {
303-
return {};
304-
}
305-
// TODO: add a build method for this pattern.
306303
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());
304+
statsViews.reserve(fileStats_.size());
305+
for (auto& stat : fileStats_) {
306+
statsViews.push_back(stat.get());
314307
}
315308
return statsViews;
316309
}
317310

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

488516
protected:
@@ -513,6 +541,157 @@ class FieldWriterContext {
513541
[](TypeBuilder&, uint32_t) {}};
514542

515543
private:
544+
// Merges min/max from source into target for a scalar statistics subclass
545+
// (Integral/FloatingPoint/String). Keeps the widest range across stripes.
546+
template <typename StatsT>
547+
static void mergeMinMax(
548+
ColumnStatistics& target,
549+
const ColumnStatistics& source) {
550+
auto* targetStats = target.as<StatsT>();
551+
const auto* sourceStats = source.as<StatsT>();
552+
if (sourceStats->getMin().has_value() &&
553+
(!targetStats->getMin().has_value() ||
554+
*sourceStats->getMin() < *targetStats->getMin())) {
555+
targetStats->min_ = sourceStats->getMin();
556+
}
557+
if (sourceStats->getMax().has_value() &&
558+
(!targetStats->getMax().has_value() ||
559+
*sourceStats->getMax() > *targetStats->getMax())) {
560+
targetStats->max_ = sourceStats->getMax();
561+
}
562+
}
563+
564+
static bool shouldUseSourceFloatingPointMin(
565+
const FloatingPointStatistics& target,
566+
const FloatingPointStatistics& source) {
567+
if (!source.getMin().has_value()) {
568+
return false;
569+
}
570+
if (!target.getMin().has_value()) {
571+
return true;
572+
}
573+
if (std::isnan(*target.getMin())) {
574+
return false;
575+
}
576+
if (std::isnan(*source.getMin())) {
577+
return true;
578+
}
579+
return *source.getMin() < *target.getMin();
580+
}
581+
582+
static bool shouldUseSourceFloatingPointMax(
583+
const FloatingPointStatistics& target,
584+
const FloatingPointStatistics& source) {
585+
if (!source.getMax().has_value()) {
586+
return false;
587+
}
588+
if (!target.getMax().has_value()) {
589+
return true;
590+
}
591+
if (std::isnan(*target.getMax())) {
592+
return false;
593+
}
594+
if (std::isnan(*source.getMax())) {
595+
return true;
596+
}
597+
return *source.getMax() > *target.getMax();
598+
}
599+
600+
static void mergeFloatingPointMinMax(
601+
ColumnStatistics& target,
602+
const ColumnStatistics& source) {
603+
auto* targetStats = target.as<FloatingPointStatistics>();
604+
const auto* sourceStats = source.as<FloatingPointStatistics>();
605+
if (shouldUseSourceFloatingPointMin(*targetStats, *sourceStats)) {
606+
targetStats->min_ = sourceStats->getMin();
607+
}
608+
if (shouldUseSourceFloatingPointMax(*targetStats, *sourceStats)) {
609+
targetStats->max_ = sourceStats->getMax();
610+
}
611+
}
612+
613+
static void mergeColumnStats(
614+
ColumnStatistics& target,
615+
const ColumnStatistics& source) {
616+
NIMBLE_CHECK(
617+
target.getType() == source.getType(),
618+
"Merging stats with mismatched types: {} vs {}.",
619+
static_cast<uint8_t>(target.getType()),
620+
static_cast<uint8_t>(source.getType()));
621+
// Deduplicated stats delegate their base metrics (value/null count, sizes)
622+
// to the wrapped base statistics, so merge into that base rather than
623+
// target's own (unused) member fields, and accumulate the dedup-specific
624+
// counters separately.
625+
if (target.getType() == StatType::DEDUPLICATED) {
626+
auto* targetStats = target.as<DeduplicatedColumnStatistics>();
627+
const auto* sourceStats = source.as<DeduplicatedColumnStatistics>();
628+
targetStats->dedupedCount_ += sourceStats->getDedupedCount();
629+
targetStats->dedupedLogicalSize_ += sourceStats->getDedupedLogicalSize();
630+
auto* targetBase = targetStats->getBaseStatistics();
631+
const auto* sourceBase = sourceStats->getBaseStatistics();
632+
NIMBLE_CHECK(
633+
(targetBase == nullptr) == (sourceBase == nullptr),
634+
"Mismatched deduplicated stats structure during merge.");
635+
if (targetBase != nullptr && sourceBase != nullptr) {
636+
mergeColumnStats(*targetBase, *sourceBase);
637+
}
638+
return;
639+
}
640+
641+
target.valueCount_ += source.getValueCount();
642+
target.nullCount_ += source.getNullCount();
643+
target.logicalSize_ += source.getLogicalSize();
644+
target.physicalSize_ += source.getPhysicalSize();
645+
switch (target.getType()) {
646+
case StatType::INTEGRAL:
647+
mergeMinMax<IntegralStatistics>(target, source);
648+
break;
649+
case StatType::FLOATING_POINT:
650+
mergeFloatingPointMinMax(target, source);
651+
break;
652+
case StatType::STRING:
653+
mergeMinMax<StringStatistics>(target, source);
654+
break;
655+
case StatType::DEFAULT:
656+
break;
657+
case StatType::DEDUPLICATED:
658+
NIMBLE_UNREACHABLE("Deduplicated stats are handled above.");
659+
}
660+
}
661+
662+
void snapshotStripeStats() {
663+
auto& stripe = stripeStats_.emplace_back();
664+
stripe.reserve(statsCollectors_.size());
665+
for (auto& collector : statsCollectors_) {
666+
auto* stats = collector->shared()
667+
? collector->as<SharedStatisticsCollector>()->getBaseStatistics()
668+
: collector->getStatsView();
669+
stripe.push_back(stats->clone());
670+
}
671+
}
672+
673+
void resetStatsCollectors() {
674+
// finalizeStatsCollector() wraps ancestors of deduplicated nodes into a
675+
// DeduplicatedStatisticsCollector in place. That wrapping must be undone
676+
// between stripes; otherwise the next stripe's finalization would take the
677+
// "already deduplicated" branch and skip rolling child logical/physical
678+
// sizes up into the ancestor, corrupting the file-level stats
679+
// reconstruction. Genuine deduplicated nodes (configured at init via
680+
// dictionary array / deduplicated map ids) must remain wrapped.
681+
for (uint32_t id = 0; id < statsCollectors_.size(); ++id) {
682+
auto& collector = statsCollectors_[id];
683+
if (collector->getType() == StatType::DEDUPLICATED &&
684+
!hasDictionaryArrayNodeId(id) && !hasDeduplicatedMapNodeId(id)) {
685+
collector = collector->asChecked<DeduplicatedStatisticsCollector>()
686+
->releaseBaseCollector();
687+
}
688+
}
689+
for (auto& collector : statsCollectors_) {
690+
collector->reset();
691+
}
692+
statsFinalized_ = false;
693+
}
694+
516695
void finalizeStatsCollector(
517696
const std::shared_ptr<const velox::dwio::common::TypeWithId>& type) {
518697
auto statsCollector = statsCollectors_[type->id()].get();
@@ -627,6 +806,8 @@ class FieldWriterContext {
627806
std::vector<std::pair<uint32_t, std::unique_ptr<StreamData>>> streams_;
628807
std::shared_ptr<const velox::dwio::common::TypeWithId> schemaWithId_;
629808
std::vector<std::unique_ptr<StatisticsCollector>> statsCollectors_;
809+
std::vector<std::vector<std::unique_ptr<ColumnStatistics>>> stripeStats_;
810+
std::vector<std::unique_ptr<ColumnStatistics>> fileStats_;
630811
bool statsFinalized_{false};
631812
};
632813

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: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,22 @@ 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+
return stripeStats->takeStripeColumnStatistics(
170+
fileSchema_, nimbleSchema_);
171+
}()} {}
157172

158173
void LazyInput::load() {
159174
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
};

0 commit comments

Comments
 (0)