Skip to content

[Parquet] Implement num distinct values for parquet writer - #10654

Open
Rich-T-kid wants to merge 5 commits into
apache:mainfrom
Rich-T-kid:rich-t-kid/parquet-write-nvd
Open

[Parquet] Implement num distinct values for parquet writer#10654
Rich-T-kid wants to merge 5 commits into
apache:mainfrom
Rich-T-kid:rich-t-kid/parquet-write-nvd

Conversation

@Rich-T-kid

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

see #8608, #10650 & apache/datafusion#24114

What changes are included in this PR?

Adds set_write_row_group_number_distinct_values to WriterProperties, which when enabled causes the ArrowWriter to track the exact number of distinct non-null values per column across the full row group and write it into the column chunk statistics footer as distinct_count. Tracking is implemented by hashing each non-null value using XxHash64 into a per-column HashSet that persists across batch writes and is finalized in close(). The flag defaults to false so there is no impact on existing writers.

Are these changes tested?

yes, see test.

Are there any user-facing changes?

yes, users will not be able to write distinct_count meta data to parquet files.

@github-actions github-actions Bot added the parquet Changes to the parquet crate label Aug 12, 2026
@Rich-T-kid
Rich-T-kid force-pushed the rich-t-kid/parquet-write-nvd branch from cda6e4a to 5111139 Compare August 12, 2026 13:09
Comment on lines +905 to +909
/// # Performance
///
/// Computing the distinct count requires hashing every non-null value in the column.
/// For large row groups or columns with many values this adds measurable overhead.
/// Benchmark your workload before enabling this globally.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth noting that turning the flag on does cause a 1.4x-8.8x regressions for the arrow writer.

Happy to make a separate issue to improve the perf of the current approach. I suspect this will also cause a decent regression when turned on since we need an exact count which requires a hash set which is both expensive in term of compute and memory.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd prefer "significant" to "measurable".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd prefer "significant" to "measurable".

@etseidl updated it

@Rich-T-kid
Rich-T-kid force-pushed the rich-t-kid/parquet-write-nvd branch from 5111139 to 5676cca Compare August 12, 2026 13:16
@etseidl etseidl changed the title [Parquet] Implement nvd for parquet writer [Parquet] Implement num distinct values for parquet writer Aug 12, 2026
@Rich-T-kid
Rich-T-kid requested a review from etseidl August 14, 2026 13:54
Comment on lines +1988 to +2043
fn update_distinct_values_seen(
array: &dyn arrow_array::Array,
non_null_indices: &[usize],
seen: &mut DistinctValuesSet,
) {
let data = array.to_data();
let offset = data.offset();

match array.data_type() {
ArrowDataType::Boolean => {
let arr = array
.as_any()
.downcast_ref::<arrow_array::BooleanArray>()
.unwrap();
for &row in non_null_indices {
seen.insert(arr.value(row) as u64);
}
}
ArrowDataType::Utf8 | ArrowDataType::Binary => {
let offsets = data.buffers()[0].typed_data::<i32>();
let values = data.buffers()[1].as_slice();
for &row in non_null_indices {
let start = offsets[offset + row] as usize;
let end = offsets[offset + row + 1] as usize;
seen.insert(hash_bytes(&values[start..end]));
}
}
ArrowDataType::LargeUtf8 | ArrowDataType::LargeBinary => {
let offsets = data.buffers()[0].typed_data::<i64>();
let values = data.buffers()[1].as_slice();
for &row in non_null_indices {
let start = offsets[offset + row] as usize;
let end = offsets[offset + row + 1] as usize;
seen.insert(hash_bytes(&values[start..end]));
}
}
ArrowDataType::FixedSizeBinary(byte_width) => {
let byte_width = *byte_width as usize;
let buffer = data.buffers()[0].as_slice();
for &row in non_null_indices {
let start = (offset + row) * byte_width;
seen.insert(hash_bytes(&buffer[start..start + byte_width]));
}
}
data_type => {
if let Some(width) = fixed_byte_width(data_type) {
let buffer = data.buffers()[0].as_slice();
for &row in non_null_indices {
let pos = (offset + row) * width;
seen.insert(hash_bytes(&buffer[pos..pos + width]));
}
}
// Utf8View, BinaryView, nested types: skip
}
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a follow up PR it'd be nice to add some benchmarks for this

seen.insert(hash_bytes(&buffer[pos..pos + width]));
}
}
// Utf8View, BinaryView, nested types: skip

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can also make a follow up issue to support the rest of the types

Comment on lines +1125 to +1127
// For dictionary arrays, hash the integer keys rather than the actual values.
// Key cardinality equals value cardinality, so distinct-value counting stays
// correct while avoiding the cost of hashing arbitrary-length values.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a similar approach may or may not be worth it for REE's, worth benchmarking first

@Rich-T-kid
Rich-T-kid force-pushed the rich-t-kid/parquet-write-nvd branch from 48c40aa to d1fa404 Compare August 14, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

parquet Changes to the parquet crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Populate and read distinct_count from parquet column statistics Implement (optional) distinct count population in Parquet statistics

2 participants