Skip to content

Commit 00a55ed

Browse files
committed
Merge remote-tracking branch 'apache/main' into fix/pure-native-metrics-interval
2 parents 1d0a556 + b38a60b commit 00a55ed

34 files changed

Lines changed: 4157 additions & 264 deletions

File tree

‎docs/source/contributor-guide/adding_a_new_operator.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ remove the inserted local node when restoring Spark execution. Restoring its `or
8585
apply the global TopK twice. A single input partition uses a final native limit and projection;
8686
multiple input partitions still require a final TopK after the shuffle.
8787

88+
With `spark.comet.exec.topK.dynamicFilter.enabled`, only the local sort sets the protobuf
89+
`Sort.dynamic_filter_enabled` flag. The native planner wraps an eligible sort in
90+
`TopKReaderFilterExec`. Its permanent plan contains an unexecuted sort template; each execution
91+
creates a fresh sort and live predicate, then attaches that predicate to the Parquet reader when
92+
eligible. The stream owns the heap and predicate until completion, error, or cancellation. The
93+
final TopK after an exchange does not share this state. Reader work stays on scan metrics while
94+
attachment counters belong to the local TopK.
95+
8896
### Choosing the Right Operator Type
8997

9098
When adding a new operator, choose based on these criteria:

‎docs/source/contributor-guide/expression-audits/conditional_funcs.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@
2727
- Spark 3.5.8 (audited 2026-05-27): baseline. `Coalesce(children) extends Expression with ComplexTypeMergingExpression`; returns the first non-null child, evaluated left-to-right with short-circuit. Result type is the merged child type. Comet routes via `CometCoalesce`, which serialises as nested `CaseWhen(IsNotNull(c1) -> c1, IsNotNull(c2) -> c2, ..., else cN)` so the native engine preserves the short-circuit semantics.
2828
- Spark 4.0.1 (audited 2026-05-27): byte-for-byte identical to 3.5.8.
2929
- Spark 4.1.1 (audited 2026-05-27): byte-for-byte identical to 3.5.8.
30+
- Performance (tuned 2026-09-28, PR [#6350](https://github.com/apache/datafusion-comet/pull/6350)): runs through the `CASE WHEN` evaluation described under `when`. 12-13x faster for two columns. Benchmark: `benches/conditional.rs`.
3031

3132
## if
3233

3334
- Spark 3.4.3 (audited 2026-05-27): identical to 3.5.8.
3435
- Spark 3.5.8 (audited 2026-05-27): baseline. `If(predicate, trueValue, falseValue) extends ComplexTypeMergingExpression`; standard ternary semantics with short-circuit, predicate must be `BooleanType`. Comet routes via `CometIf` to the native `IfExpr` proto.
3536
- Spark 4.0.1 (audited 2026-05-27): adds an `override def withNewAlwaysEvaluatedInputs(...)` hook for the new optimizer phase; semantics unchanged. Error messages reformatted (`paramIndex` -> `ordinalNumber`).
3637
- Spark 4.1.1 (audited 2026-05-27): identical to 4.0.1.
38+
- Performance (tuned 2026-09-28, PR [#6350](https://github.com/apache/datafusion-comet/pull/6350)): runs through the `CASE WHEN` evaluation described under `when`, with an `IF` or `CASE WHEN` in the ELSE flattened into one merge. 1.5x faster with literal results, 6.6x with column results, 3.3-4.7x with string columns, 3.1-5.9x nested. Benchmark: `benches/conditional.rs`.
3739

3840
## ifnull
3941

@@ -69,5 +71,6 @@
6971
- Spark 3.5.8 (audited 2026-05-27): the `CASE WHEN ... THEN ...` SQL form lowers to `CaseWhen(branches: Seq[(Expression, Expression)], elseValue: Option[Expression])`. Spark evaluates left-to-right with short-circuit; result type is the merged branch type. Comet routes via `CometCaseWhen` to the native `CaseWhen` proto.
7072
- Spark 4.0.1 (audited 2026-05-27): adds the `withNewAlwaysEvaluatedInputs` optimizer hook; semantics unchanged.
7173
- Spark 4.1.1 (audited 2026-05-27): identical to 4.0.1.
74+
- Performance (tuned 2026-09-28, PR [#6350](https://github.com/apache/datafusion-comet/pull/6350)): when no branch value and no WHEN after the first can fail, `CaseWhenExpr` evaluates them over the whole batch and takes each row's value with typed merges for primitive, boolean, string and binary results, instead of filtering the batch for each branch and merging the partial results with `MutableArrayData`. A branch is no longer wrapped in a cast to the type it already has, which hid literals and columns from DataFusion's fast paths. 2.8-4x faster with literal results and 2.2-7.8x with column results. When a branch can fail it is about 2% slower, because DataFusion's `CaseExpr` derives the result type of a bare branch for every batch. Benchmark: `benches/conditional.rs`.
7275

7376
[Spark Expression Support]: ../../user-guide/latest/expressions.md

‎docs/source/user-guide/latest/metrics.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,30 @@ by the reader. An attached filter does not guarantee that any row groups are pru
107107
probe scan's `bytes_scanned` and `row_groups_pruned_statistics` with filtering disabled to assess
108108
reader savings. Existing join, scan, and intervening filter metrics retain their own meanings.
109109

110+
### Local TopK
111+
112+
With `spark.comet.exec.topK.dynamicFilter.enabled=true`, eligible fused local TopK operators report
113+
these counters. See [TopK Reader Pruning](tuning/operators.md#topk-reader-pruning) for the required
114+
fusion option and reader restrictions.
115+
116+
| Metric | Description |
117+
| -------------------------------------- | ---------------------------------------------------------------------------- |
118+
| `dynamic_filter_topk_filters_attached` | Executions that attach their live TopK threshold to a native Parquet reader. |
119+
| `dynamic_filter_topk_filters_skipped` | Eligible TopK executions whose input cannot accept reader attachment. |
120+
121+
These counters belong to the local TopK. An attached predicate may prune nothing, and a file's
122+
schema adaptation can disable it to preserve conversion errors. Reader work remains on the scan:
123+
`output_rows` counts rows emitted by the scan, `bytes_scanned` measures requested data/Bloom-filter
124+
ranges (excluding footer and page-index reads), and page and decoder filtering use the existing
125+
`page_index_rows_pruned` and `pushdown_rows_pruned` counters.
126+
127+
Row groups skipped by runtime pruning within a file increment `row_groups_pruned_dynamic_filter`.
128+
When later files open, the reader can prune using the TopK threshold already available; those groups
129+
increment `row_groups_pruned_statistics`. With one row group per file, the dynamic counter can remain
130+
zero even when TopK skips most later groups. The statistics counter also includes pruning by other
131+
predicates, so compare both counters and `bytes_scanned` with filtering disabled to assess TopK savings.
132+
This feature adds no separate decoded-batch filter or evaluated/pruned row counters to the TopK.
133+
110134
### Exchange
111135

112136
Comet adds some additional metrics:
@@ -185,6 +209,7 @@ execution metrics. Counters accumulate per scan operator; they do not instrument
185209
| `scan_io_object_store_response_bytes_read` | Response bytes actually consumed at that API, including bytes fetched between coalesced ranges. Not HTTP wire bytes. |
186210
| `scan_io_metadata_cache_hits` | Successful, cache-eligible metadata opens requiring no storage reads. |
187211
| `scan_io_metadata_cache_misses` | Successful, cache-eligible metadata opens requiring storage reads. Failed opens and encrypted opens, which bypass this shared cache, increment neither cache counter. |
212+
| `row_groups_pruned_dynamic_filter` | Row groups skipped by a runtime predicate whose value changes during execution, such as a local TopK threshold. |
188213

189214
Reader-level and object-store bytes are two views of the same reads; do not add them together.
190215
Likewise, footer bytes are a subset of metadata bytes, not a third reader-level category. A warm

‎docs/source/user-guide/latest/tuning/operators.md‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,44 @@ Each scan partition keeps enough candidates for both `LIMIT` and `OFFSET`. With
111111
Comet shuffles those candidates and performs the final TopK. With one partition, it reuses the local
112112
ordering without building a second heap. The final stage applies the offset and output projection.
113113

114-
Fusion reduces the work of passing scan batches between native execution blocks. It still reads all
115-
input rows and does not enable TopK reader pruning. It can also reduce overlap between scan decoding
114+
Fusion reduces the work of passing scan batches between native execution blocks. Fusion alone still
115+
reads all input rows. [TopK reader pruning](#topk-reader-pruning) is enabled separately. Fusion can
116+
also reduce overlap between scan decoding
116117
and TopK processing, so some workloads may run slower. Compare enabled and disabled runs with your
117118
data layout, payload width, limit, and partition count before enabling it. The
118119
`CometTopKBenchmark` microbenchmark covers these cases with ascending, descending, and random layouts.
119120

121+
### TopK Reader Pruning
122+
123+
Set both `spark.comet.exec.topK.fusion.enabled=true` and
124+
`spark.comet.exec.topK.dynamicFilter.enabled=true` to pass the local TopK's improving threshold to
125+
its Parquet reader. Both options are experimental and disabled by default. Eligibility is the same
126+
single signed integer key described above. Each task creates a fresh threshold; it is not shared
127+
across Spark partitions or exchanges, or retained for later executions.
128+
129+
Once the heap contains enough candidates for `LIMIT + OFFSET`, the reader can skip later row groups
130+
whose statistics prove that no row can improve those candidates. Existing Parquet page-index and
131+
decoder-filter options can also use the predicate. This option adds no separate filter over decoded
132+
scan batches. TopK continues to select the final candidates.
133+
134+
Reader attachment is conservative. A scan with a fetch limit, supplied file statistics, or a static
135+
predicate other than direct column `IS NOT NULL` checks keeps the existing execution path. For each
136+
file, schema adaptation disables pruning if it could hide a conversion error in a projected or
137+
filtered column. Missing null counts remain unknown, which can prevent pruning even when min/max
138+
statistics are present. These cases can still execute a fused TopK.
139+
140+
Reader pruning is most useful when small K values and the file order establish a strong threshold
141+
early. Descending or random layouts for an ascending query can prune few or no groups, while still
142+
paying the cost of attaching and checking the predicate. Wider rows can increase the benefit when
143+
groups are skipped. Compare `pruning` with `fused` in `CometTopKBenchmark` to measure the reader effect,
144+
and compare both with `unfused` to include the cost of fusion. Check the scan's emitted rows,
145+
`bytes_scanned`, `row_groups_pruned_dynamic_filter`, and `row_groups_pruned_statistics` alongside elapsed
146+
time; attachment alone does not demonstrate a saving. Pruning when later files open uses the TopK
147+
threshold already available and increments `row_groups_pruned_statistics`. With one row group per
148+
file, the dynamic counter can stay zero despite substantial TopK pruning. The statistics counter also
149+
includes other predicates, so compare with filtering disabled to assess TopK savings.
150+
See [TopK metrics](../metrics.md#local-topk).
151+
120152
## Optimizing Sorting on Floating-Point Values
121153

122154
Comet normalizes NaN payloads and signed zeros in scalar `FLOAT` and `DOUBLE` ordering keys, so `ORDER BY`, window

‎native/core/src/execution/operators/dynamic_filter/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
2020
mod join;
2121
mod parquet_reader;
22+
mod topk;
2223

2324
pub(crate) use join::DynamicFilterJoinExec;
25+
pub(crate) use topk::TopKReaderFilterExec;
2426

2527
use std::fmt::Formatter;
2628
use std::sync::Arc;

‎native/core/src/execution/operators/dynamic_filter/parquet_reader.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use schema_adapter::RuntimeFilterSchemaAdapterFactory;
4141
/// or changing the predicate. Every accepted leaf is deterministic, infallible,
4242
/// and only discards rows, so reader pruning cannot suppress expression errors
4343
/// or alter stateful evaluation. All other expressions remain a boundary.
44-
fn is_direct_column_null_checks(predicate: &Arc<dyn PhysicalExpr>) -> bool {
44+
pub(super) fn is_direct_column_null_checks(predicate: &Arc<dyn PhysicalExpr>) -> bool {
4545
if let Some(binary) = predicate.downcast_ref::<BinaryExpr>() {
4646
return binary.op() == &Operator::And
4747
&& is_direct_column_null_checks(binary.left())

0 commit comments

Comments
 (0)