Repository navigation
Closed
Labels
enhancementNew feature or requestNew feature or requestperformanceMake DataFusion fasterMake DataFusion faster
Description
Part of #23600.
Is your feature request related to a problem or challenge?
A very common "top-1 per group" pattern is written as:
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY p ORDER BY o DESC) AS rn
FROM t
) WHERE rn = 1DataFusion currently plans this as:
FilterExec(rn = 1)
BoundedWindowAggExec(row_number() PARTITION BY p ORDER BY o)
SortExec(p, o DESC) ← buffers all input rows
...
The SortExec buffers the entire input, including all wide payload columns, even though the semantics only require one row per p. On wide payload this scales badly:
- Production observation: 11M rows × ~2.5 KB payload → ~90 GB peak OOM on 16 CPU
- The hand-written aggregate equivalent (
SELECT p, FIRST_VALUE(...ORDER BY o DESC), ... GROUP BY p) runs at 3.6 GB peak on the same data, once the nested-typeGroupsAccumulatorblocker is out of the way
The only physical-side optimization today is PartitionedTopKExec (enable_window_topn, #21479), which does not compose well with wide payloads on high-cardinality groups (each group carries K wide rows in a heap → still #groups × K × wide_row).
Describe the solution you'd like
Add a logical optimizer rule that rewrites the ROW_NUMBER() = 1 pattern to an aggregate:
Input plan:
Filter: rn = 1 -- or rn <= 1, or rn < 2
Projection: ... -- optional passthrough
WindowAggr: row_number() OVER (PARTITION BY p ORDER BY o DESC) AS rn
child
Rewritten plan:
Aggregate:
group_by=[p]
aggr=[first_value(col_i ORDER BY o DESC) for each output col_i]
child
Preconditions for the rewrite:
- The window function is exactly
row_number()(notrank,dense_rank,nth_value, etc.) - Filter predicate is
rn = 1,rn <= 1, orrn < 2(all equivalent to "top-1 per group") rnis not referenced anywhere else in the plan above the filter (no aliasing, no join key, no other projection)- The window has a
PARTITION BYclause (without it the semantics differ)
Behavior:
- Order-key ties remain "unspecified", matching the existing
ROW_NUMBERbehavior (bothROW_NUMBERandFIRST_VALUEbreak ties arbitrarily) - All non-partition columns are wrapped in
FIRST_VALUE(... ORDER BY o)in the aggregate output
Describe alternatives you've considered
- Physical rule instead of logical. Feasible but constrained — a logical rule composes with downstream rules (projection pushdown, partial aggregate pushdown, distribution planning) that don't know about the physical
BoundedWindowAggExecshape. - Expand
PartitionedTopKExecto be memory-efficient on wide payloads. Would need late materialization or row-id primitives that DataFusion does not have today. The aggregate rewrite reuses existing infrastructure. - Leave to users. Users routinely write the
ROW_NUMBER() = 1form (it is the standard SQL idiom for "pick one row per group with ordering"); auto-rewrite converts a well-known trap into transparent optimization.
Additional context
Companion epic: #23600
This rewrite depends on:
- Nested-type
GroupsAccumulatorsupport — without it the rewrite would emit the slow per-groupAccumulatorpath on wide payloads and could regress users. Rewrite should only fire when the aggregate destination is confirmed to hit the columnar path, or should be gated by config until the blocker lands. - Peer
FIRST_VALUEcoalescing — optional but strongly recommended for wide-payload rewrites (avoids N × per-group state).
Related:
- Perf: Window topn optimisation #21479 —
PartitionedTopKExec— a complementary physical rewrite; the aggregate rewrite is the natural fit for wide payloads / high group cardinality
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or requestperformanceMake DataFusion fasterMake DataFusion faster
Activity
saadtajwar commentedon Jul 16, 2026
take
2010YOUY01 commentedon Jul 17, 2026
I found
arg_max()ormax_by()aggregate function are more ergonomic for such workloadshttps://duckdb.org/docs/lts/sql/functions/aggregates#arg_maxarg-val
https://docs.snowflake.com/en/sql-reference/functions/max_by
Its internal mechanism should be the same as
first_value(v1 order by v2), perhaps we can support them as an alias tofirst_value()zhuqi-lucas commentedon Jul 17, 2026
Thanks @2010YOUY01 ! Good info and suggestion, i agree this looks better.
saadtajwar commentedon Jul 17, 2026
Thank you @2010YOUY01 , I will take a look!
saadtajwar commentedon Jul 21, 2026
Starting on this now, hoping to have a PR up in the next couple of days! I'll start with the core optimizer pass for the first PR, and might split out the
arg_max/max_byinto a subsequent PR :)saadtajwar commentedon Jul 23, 2026
@tohuya6 & @zhuqi-lucas - PR for this up at #23824 ! Looking forward to the feedback - thanks! 🥳
feat(functions-aggregate): support nested types (List, Struct, Map) i…
feat(functions-aggregate): support nested types (List, Struct, Map) i…
feat(optimizer): logical rewrite Filter(row_number() = 1) over PARTIT…