Describe the bug
A correlated filter that sits below a window function inside a subquery is pulled out of the subquery and attached to the decorrelated join. The window function then runs over all rows of the inner table instead of only the rows that match the outer row, so row_number(), rank(), lag() and similar functions give different values and the query gives wrong results.
This affects EXISTS and LATERAL subqueries.
To Reproduce
CREATE TABLE o(k INT) AS VALUES (1), (2), (5);
CREATE TABLE b(id INT, y INT) AS VALUES (1, 1), (2, 2);
For each row of o, the subquery keeps only the rows of b where b.y = o.k, then numbers them. Each k matches at most one row of b, so that row always gets rn = 1.
EXISTS form
SELECT o.k FROM o
WHERE EXISTS (
SELECT 1
FROM (SELECT b.id, row_number() OVER (ORDER BY b.id) AS rn
FROM (SELECT * FROM b WHERE b.y = o.k) AS b) AS w
WHERE w.rn = 1)
ORDER BY o.k;
| DataFusion |
DuckDB 1.5.2 |
PostgreSQL 17.11 |
1 |
1, 2 |
1, 2 |
LATERAL form
SELECT o.k, w.id, w.rn
FROM o, LATERAL (
SELECT b.id, row_number() OVER (ORDER BY b.id) AS rn
FROM (SELECT * FROM b WHERE b.y = o.k) AS b) AS w
ORDER BY o.k;
| k |
id |
DataFusion rn |
DuckDB 1.5.2 rn |
PostgreSQL 17.11 rn |
| 1 |
1 |
1 |
1 |
1 |
| 2 |
2 |
2 |
1 |
1 |
Expected behavior
The results of DuckDB and PostgreSQL above.
Additional context
The plan shows the cause. Filter: b.y = o.k is gone from below the WindowAggr and is now the condition of the semi join, so row_number() numbers both rows of b:
LeftSemi Join: o.k = __correlated_sq_1.y
TableScan: o projection=[k]
SubqueryAlias: __correlated_sq_1
SubqueryAlias: w
Projection: b.y
Filter: row_number() ORDER BY [b.id ASC NULLS LAST] ... = UInt64(1)
Projection: b.y, row_number() ORDER BY [b.id ASC NULLS LAST] ...
WindowAggr: windowExpr=[[row_number() ORDER BY [b.id ASC NULLS LAST] ...]]
SubqueryAlias: b
TableScan: b projection=[id, y]
PullUpCorrelatedExpr in datafusion/optimizer/src/decorrelate.rs pulls a correlated Filter up through every node that it does not model. LogicalPlan::Window is one of those nodes. A filter can only move above a window if it reads only the PARTITION BY columns, which is not the case here (the window has no PARTITION BY).
Found on main at 6a792c6.
This is the same class of bug as these, each for a different node:
A fix can do the same as those PRs: mark the subquery as not pull-up-able when a Window has outer references below it, so the query fails with a not-implemented error instead of returning wrong results.
Describe the bug
A correlated filter that sits below a window function inside a subquery is pulled out of the subquery and attached to the decorrelated join. The window function then runs over all rows of the inner table instead of only the rows that match the outer row, so
row_number(),rank(),lag()and similar functions give different values and the query gives wrong results.This affects
EXISTSandLATERALsubqueries.To Reproduce
For each row of
o, the subquery keeps only the rows ofbwhereb.y = o.k, then numbers them. Eachkmatches at most one row ofb, so that row always getsrn = 1.EXISTSform11,21,2LATERALformrnrnrn111211Expected behavior
The results of DuckDB and PostgreSQL above.
Additional context
The plan shows the cause.
Filter: b.y = o.kis gone from below theWindowAggrand is now the condition of the semi join, sorow_number()numbers both rows ofb:PullUpCorrelatedExprindatafusion/optimizer/src/decorrelate.rspulls a correlatedFilterup through every node that it does not model.LogicalPlan::Windowis one of those nodes. A filter can only move above a window if it reads only thePARTITION BYcolumns, which is not the case here (the window has noPARTITION BY).Found on
mainat 6a792c6.This is the same class of bug as these, each for a different node:
Join, the nullable side of an outer join), fixed by fix: keep a correlated filter below the nullable side of an outer join #25764EXISTSsubquery withOFFSETreturns wrong results #25283 (Limitwith anOFFSET), fixed by fix: keep the OFFSET of a correlated EXISTS subquery #25284A fix can do the same as those PRs: mark the subquery as not pull-up-able when a
Windowhas outer references below it, so the query fails with a not-implemented error instead of returning wrong results.