Repository navigation
Conversation
apache#1916 collects per-column null counts on the executor and aggregates them on the completed stage, but nothing read them: a resolved exchange still reported unknown column statistics. - Collect null counts on the passthrough and range shuffle writers too, not only the sort writer, so every stage reports them. - When an AQE stage completes, aggregate its tasks' column statistics and store them on the stage's ExchangeExec before the replan. - Report them from ExchangeExec::partition_statistics for the exchange as a whole. Per-partition statistics stay unknown, since the counts are stage totals. Closes apache#1960
…ts plumbing - The sort shuffle writer counts nulls through ColumnNullCounts like the passthrough and range writers, replacing its inline count and fold. - Each stream counts into its own vector and adds it to the task total once, when dropped, instead of taking a shared lock per batch. - Column statistics are set in the same finalise_stage lookup that resolves the exchange's partitions, rather than a separate lookup before it; the partitions-only helper moves to the test module. - Tests reuse mock_partitions_with_size and a shared null_count_statistics helper.
Shuffle writers now sketch the distinct count of each hash-partitioning key column with a precision-12 HyperLogLog (4 KiB per column per task) and report the registers in TaskColumnStats.sketches. The scheduler merges every successful task's sketch per column and sets ColumnStatistics::distinct_count to Inexact on the stage's output, which reaches the resolved exchange, so join cardinality estimates downstream of a shuffle no longer fall back to row counts. The count is left Absent if any successful task did not sketch the column. Values are hashed with DataFusion's HLL_RANDOM_STATE, as approx_distinct does. Collection can be turned off with ballista.shuffle.collect_distinct_counts, which shuffle_bench exposes as --collect-distinct-counts. Closes apache#2665.
# Conflicts: # ballista/scheduler/src/state/aqe/test/mod.rs
villebro
reviewed
Oct 27, 2026
villebro
left a comment
Member
There was a problem hiding this comment.
Very valuable feature 👍 Some random thoughts.
Comment on lines
+24
to
+29
| //! Values are hashed with DataFusion's `HLL_RANDOM_STATE`, the hash state | ||
| //! `approx_distinct` uses, and the count uses the same estimator (Ertl, | ||
| //! "New cardinality estimation algorithms for HyperLogLog sketches", 2017). | ||
| //! Precision is 12 rather than DataFusion's 14: 4 KiB of registers per | ||
| //! column per task instead of 16 KiB, for a standard error of about 1.6%, | ||
| //! which is plenty for join cardinality estimation. |
Member
There was a problem hiding this comment.
I don't disagree with 12 over 14 here, but it does pose the question, why not 10, or some other less memory intensive number?
| <!-- prettier-ignore --> | ||
| | key | type | default | description | | ||
| | -------------------------------------------------------- | ------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | ballista.shuffle.collect_distinct_counts | Boolean | true | Sketch the approximate distinct count of each hash-partitioning key column with HyperLogLog while writing shuffle output, so the adaptive planner can estimate join cardinalities downstream of the shuffle. Costs hashing every key value on the write path and 4 KiB per key column in each task's status update. | |
Member
There was a problem hiding this comment.
Optional consideration: expose ballista.shuffle.collect_distinct_counts_precision that defaults to 12 for those that really want to fine tune their deployment?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Closes #2665.
Rationale for this change
A resolved exchange carries no distinct counts, so DataFusion's join cardinality estimate downstream of a shuffle falls back to row counts and estimates an inner join at the smaller input's size. For a key/foreign-key join whose large side is the foreign key, that undercounts by the join's fan-out, which is what led AQE to broadcast a 151M-row join estimated at 400K rows in TPC-H q21 (#2661, #2662). #2664 already carries per-column statistics from completed stages onto the AQE
ExchangeExec, and the protocol already reservesTaskColumnStats.sketchesfor HLL sketches, so this fills that slot.What changes are included in this PR?
ballista_core::hll: a precision-12 HyperLogLog (4 KiB of registers, about 1.6% standard error). Values are hashed with DataFusion'sHLL_RANDOM_STATE, asapprox_distinctdoes, and counted with the same estimator. Nulls are skipped.ColumnStatsCollector, which also updates one sketch per hash-partitioning key column (plain column references in the hash expressions). Each concurrently drained stream sketches on its own and merges into the task total when dropped, so there is no per-batch locking. The passthrough writer takes the keys from its child's hash partitioning, the sort writer from its own, and the range writer sketches nothing. Each writer gets acolumn_stats()that the executor reports asTaskColumnStats, with the registers in aColumnSketch { type: HLL }.RunningStage::output_column_statsmerges each column's sketches across successful tasks (element-wise max) and setsdistinct_counttoInexact. It staysAbsentif any successful task didn't sketch the column, mirroringnull_count.ballista.shuffle.collect_distinct_counts(defaulttrue) to turn collection off, and a matching--collect-distinct-countsflag onshuffle_benchfor measuring the cost.Sketching only the partition keys keeps the task status payload at 4 KiB per key column per task. While a task writes, each input partition being drained holds 4 KiB per key column outside the memory pool.
On a local
shuffle_benchrun (sort writer, 60M TPC-H SF100 lineitem rows hashed onl_orderkey, 8 output partitions, 3 iterations each, alternating), the difference between collection on and off was within run-to-run noise (19.4s to 19.6s for both, plus one 22.2s outlier with collection on caused by a slow spill). That run is spill-dominated, so a TPC-H SF1000 cluster run is still worth doing before relying on the default.Tests cover the sketch itself (accuracy, nulls, merge, serialization round trip and validation), key column selection, the sort and passthrough writers reporting a sketch only on the hash key, the disable flag, and the scheduler merge, including overlapping keys across tasks and a task without a sketch.
Are there any user-facing changes?
A new config key,
ballista.shuffle.collect_distinct_counts, documented in the generated config reference. No breaking API changes.