Skip to content

Commit b2f8a32

Browse files
prashantgolashmeta-codesync[bot]
authored andcommitted
feat(nimble): add per-chunk null count
Summary: The first real per-chunk statistic. Chunk stats are still purely positional; this records how many nulls each chunk holds, so a later reader can skip chunks on null-related predicates and we can eventually retire the read-time byte-peeking row estimates in ChunkedDecoder. - The writer counts nulls per chunk during encode, only when chunk stats are enabled. The fleet default is off, so no writer pays for this yet. - Stored as a new field appended to the existing chunk-stats flatbuffer. Files written before this simply lack it, and the reader reports "unknown" rather than a wrong value. - Reader accessor StreamIndex::chunkNullCount returns the count, or nullopt for pre-stats files. Differential Revision: D111913795
1 parent 0783870 commit b2f8a32

10 files changed

Lines changed: 103 additions & 4 deletions

dwio/nimble/index/ChunkStatsGroup.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,23 @@ ChunkLocation StreamIndex::lookupChunk(uint32_t rowId) const {
144144
chunkIndex, streamOffset, nextOffset - streamOffset, rowOffset};
145145
}
146146

147+
std::optional<uint32_t> StreamIndex::chunkNullCount(uint32_t chunkIndex) const {
148+
const auto* root = asFlatBuffersRoot<serialization::StripeChunkStats>(
149+
chunkStats_->metadata_->content());
150+
151+
const auto* nullCounts = root->stream_chunk_null_counts();
152+
if (nullCounts == nullptr) {
153+
// Absent in files written before per-chunk null statistics were added; the
154+
// null count is unknown.
155+
return std::nullopt;
156+
}
157+
// The array is present, so chunkIndex (derived from ChunkLocation) must be in
158+
// range. An out-of-range index is a programmer error or malformed metadata,
159+
// not a legacy file, so fail loudly rather than masking it as "unknown".
160+
NIMBLE_CHECK_LT(chunkIndex, nullCounts->size());
161+
return nullCounts->Get(chunkIndex);
162+
}
163+
147164
uint32_t StreamIndex::rowCount() const {
148165
if (endChunkOffset_ == startChunkOffset_) {
149166
return 0;

dwio/nimble/index/ChunkStatsGroup.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include <cstdint>
1919
#include <memory>
20+
#include <optional>
2021

2122
#include "dwio/nimble/common/Exceptions.h"
2223
#include "dwio/nimble/index/IndexTypes.h"
@@ -101,6 +102,11 @@ class StreamIndex {
101102
/// Lookup chunk by row ID within the stream's row range.
102103
ChunkLocation lookupChunk(uint32_t rowId) const;
103104

105+
/// Returns the per-chunk null-value count for the chunk at the given absolute
106+
/// position (ChunkLocation::chunkIndex), or std::nullopt when per-chunk null
107+
/// statistics are absent (files written before chunk statistics were added).
108+
std::optional<uint32_t> chunkNullCount(uint32_t chunkIndex) const;
109+
104110
/// Returns the total number of rows in this stream.
105111
uint32_t rowCount() const;
106112

dwio/nimble/index/tests/ClusterIndexTestUtils.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ std::vector<Chunk> createChunks(
103103
auto pos = buffer.reserve(spec.size);
104104
std::memset(pos, 'X', spec.size);
105105
chunks.push_back(
106-
{.rowCount = spec.rowCount, .content = {{pos, spec.size}}});
106+
{.rowCount = spec.rowCount,
107+
.nullCount = spec.nullCount,
108+
.content = {{pos, spec.size}}});
107109
}
108110
return chunks;
109111
}
@@ -167,6 +169,7 @@ StreamStats ChunkStatsTestHelper::streamStats(uint32_t streamId) const {
167169
NIMBLE_CHECK_NOT_NULL(chunkRows);
168170
const auto* chunkOffsets = root->stream_chunk_offsets();
169171
NIMBLE_CHECK_NOT_NULL(chunkOffsets);
172+
const auto* chunkNullCounts = root->stream_chunk_null_counts();
170173

171174
const uint32_t stripeCount = chunkStats_->stripeCount_;
172175

@@ -186,6 +189,9 @@ StreamStats ChunkStatsTestHelper::streamStats(uint32_t streamId) const {
186189
const uint32_t chunkIndex = startChunkIndex + i;
187190
stats.chunkRows.push_back(chunkRows->Get(chunkIndex));
188191
stats.chunkOffsets.push_back(chunkOffsets->Get(chunkIndex));
192+
if (chunkNullCounts != nullptr) {
193+
stats.chunkNullCounts.push_back(chunkNullCounts->Get(chunkIndex));
194+
}
189195
}
190196
}
191197

dwio/nimble/index/tests/ClusterIndexTestUtils.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,16 @@ struct StreamStats {
134134
std::vector<uint32_t> chunkRows;
135135
/// Byte offset of each chunk within its stream.
136136
std::vector<uint32_t> chunkOffsets;
137+
/// Per-chunk null-value count for this stream. Empty when per-chunk null
138+
/// statistics are absent from the section.
139+
std::vector<uint32_t> chunkNullCounts;
137140
};
138141

139142
/// Specification for a single chunk in a stream (for test data creation).
140143
struct ChunkSpec {
141-
uint32_t rowCount;
142-
uint32_t size;
144+
uint32_t rowCount{};
145+
uint32_t size{};
146+
uint32_t nullCount{0};
143147
};
144148

145149
/// Specification for a single stream (for test data creation).

dwio/nimble/tablet/Chunk.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ namespace facebook::nimble {
2727
struct Chunk {
2828
uint32_t rowCount{0};
2929

30+
/// Number of null values in this chunk. Only populated when chunk statistics
31+
/// are enabled (VeloxWriterOptions::enableChunkIndex); left at 0 otherwise.
32+
uint32_t nullCount{0};
33+
3034
/// The encoded and compressed data content of this chunk, stored as a vector
3135
/// of string views. Each string_view points to a buffer containing a portion
3236
/// of the chunk's data. Multiple buffers may be used for large chunks.

dwio/nimble/tablet/ChunkStats.fbs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ table StripeChunkStats {
3939
stream_chunk_rows:[uint32];
4040
/// Byte offset of each chunk within its stream.
4141
stream_chunk_offsets:[uint32];
42+
/// Per-chunk null-value count, parallel to stream_chunk_rows/offsets.
43+
/// Appended field: absent in files written before per-chunk null statistics
44+
/// were added, in which case readers treat the null count as unknown.
45+
stream_chunk_null_counts:[uint32];
4246
}
4347

4448
/// Root table for the chunk stats optional section ("columnar.chunk.stats").

dwio/nimble/tablet/ChunkStatsWriter.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ void ChunkStatsWriter::addStream(
6666
accumulatedRows += chunk.rowCount;
6767
index.chunkRows.emplace_back(accumulatedRows);
6868
index.chunkOffsets.emplace_back(accumulatedOffset);
69+
index.chunkNullCounts.emplace_back(chunk.nullCount);
6970
accumulatedOffset += chunk.contentSize();
7071
++index.chunkCount;
7172
}
@@ -123,8 +124,10 @@ void ChunkStatsWriter::writeGroup(
123124

124125
std::vector<uint32_t> flattenedChunkRows;
125126
std::vector<uint32_t> flattenedChunkOffsets;
127+
std::vector<uint32_t> flattenedChunkNullCounts;
126128
flattenedChunkRows.reserve(accumulatedChunkCount);
127129
flattenedChunkOffsets.reserve(accumulatedChunkCount);
130+
flattenedChunkNullCounts.reserve(accumulatedChunkCount);
128131

129132
for (const auto& stripe : groupIndex_->stripes) {
130133
for (size_t streamId = 0; streamId < streamCount; ++streamId) {
@@ -138,6 +141,10 @@ void ChunkStatsWriter::writeGroup(
138141
flattenedChunkOffsets.end(),
139142
stream.chunkOffsets.begin(),
140143
stream.chunkOffsets.end());
144+
flattenedChunkNullCounts.insert(
145+
flattenedChunkNullCounts.end(),
146+
stream.chunkNullCounts.begin(),
147+
stream.chunkNullCounts.end());
141148
}
142149
}
143150
}
@@ -148,7 +155,8 @@ void ChunkStatsWriter::writeGroup(
148155
static_cast<uint32_t>(streamCount),
149156
builder.CreateVector(flattenedStreamChunkCounts),
150157
builder.CreateVector(flattenedChunkRows),
151-
builder.CreateVector(flattenedChunkOffsets));
158+
builder.CreateVector(flattenedChunkOffsets),
159+
builder.CreateVector(flattenedChunkNullCounts));
152160
builder.Finish(chunkIndex);
153161

154162
chunkIndexSections_.push_back(createMetadataSection(asView(builder)));

dwio/nimble/tablet/ChunkStatsWriter.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ class ChunkStatsWriter {
8282
std::vector<uint32_t> chunkRows;
8383
// Byte offsets of each chunk within the stream.
8484
std::vector<uint32_t> chunkOffsets;
85+
// Per-chunk null-value count (statistic used for chunk skipping).
86+
std::vector<uint32_t> chunkNullCounts;
8587
// Number of chunks in this stripe for this stream.
8688
uint32_t chunkCount{0};
8789
};

dwio/nimble/tablet/tests/ChunkStatsWriterTest.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,45 @@ TEST_F(ChunkStatsWriterTest, singleStripe) {
165165
EXPECT_EQ(r11.rowOffset, 60);
166166
}
167167

168+
TEST_F(ChunkStatsWriterTest, perChunkNullCounts) {
169+
ChunkStatsWriter writer(*pool_);
170+
Buffer buffer{*pool_};
171+
TestChunkFileIndex fileIndex;
172+
173+
// 1 stripe, 2 streams. ChunkSpec = {rowCount, size, nullCount}.
174+
// Stream 0: 3 chunks with null counts 3, 0, 5.
175+
// Stream 1: 2 chunks with null counts 0, 40 (a fully-null chunk).
176+
writer.newStripe(2);
177+
auto chunks0 = createChunks(buffer, {{30, 10, 3}, {45, 15, 0}, {25, 8, 5}});
178+
auto chunks1 = createChunks(buffer, {{60, 20, 0}, {40, 12, 40}});
179+
writer.addStream(0, chunks0);
180+
writer.addStream(1, chunks1);
181+
182+
writer.writeGroup(2, 1, createMetadataSectionCallback(fileIndex));
183+
writer.writeRoot(writeRootCallback(fileIndex));
184+
185+
auto chunkIndex = loadChunkIndex(fileIndex.groupMetadataSections[0], 0, 1);
186+
187+
// Null counts round-trip through the flatbuffer in flattened order.
188+
ChunkStatsTestHelper helper(chunkIndex.get());
189+
EXPECT_EQ(
190+
helper.streamStats(0).chunkNullCounts, (std::vector<uint32_t>{3, 0, 5}));
191+
EXPECT_EQ(
192+
helper.streamStats(1).chunkNullCounts, (std::vector<uint32_t>{0, 40}));
193+
194+
// Public reader accessor: lookupChunk() yields the absolute chunk index,
195+
// which chunkNullCount() maps to the per-chunk null statistic.
196+
auto stream0 = chunkIndex->createStreamIndex(0, 0, 33);
197+
ASSERT_NE(stream0, nullptr);
198+
EXPECT_EQ(stream0->chunkNullCount(stream0->lookupChunk(0).chunkIndex), 3);
199+
EXPECT_EQ(stream0->chunkNullCount(stream0->lookupChunk(30).chunkIndex), 0);
200+
EXPECT_EQ(stream0->chunkNullCount(stream0->lookupChunk(75).chunkIndex), 5);
201+
202+
auto stream1 = chunkIndex->createStreamIndex(0, 1, 32);
203+
ASSERT_NE(stream1, nullptr);
204+
EXPECT_EQ(stream1->chunkNullCount(stream1->lookupChunk(60).chunkIndex), 40);
205+
}
206+
168207
TEST_F(ChunkStatsWriterTest, multipleStripesInSingleGroup) {
169208
ChunkStatsWriter writer(*pool_);
170209
Buffer buffer{*pool_};

dwio/nimble/velox/VeloxWriter.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
#include "dwio/nimble/velox/VeloxWriter.h"
1717

18+
#include <algorithm>
1819
#include <memory>
1920
#include <optional>
2021
#include <unordered_map>
@@ -1453,6 +1454,14 @@ uint32_t VeloxWriter::encodeChunk(
14531454
}
14541455
uint32_t chunkBytes{0};
14551456
chunk.rowCount = chunkView.rowCount();
1457+
// Collect the per-chunk null-count statistic only when chunk statistics are
1458+
// enabled, so writers with chunk stats off pay no extra cost. Streams with no
1459+
// nulls keep the default 0 without scanning.
1460+
if (context_->options().enableChunkIndex && chunkView.hasNulls()) {
1461+
const auto nonNulls = chunkView.nonNulls();
1462+
chunk.nullCount = static_cast<uint32_t>(
1463+
std::count(nonNulls.begin(), nonNulls.end(), false));
1464+
}
14561465
ChunkedStreamWriter chunkWriter{
14571466
*encodingBuffer_, context_->options().chunkCompression};
14581467
for (auto& buffer : chunkWriter.encode(encoded)) {

0 commit comments

Comments
 (0)