Fix COUNT's logic for window run condition support

9d9c02ccd added code to allow the executor to stop early when processing
WindowAgg nodes where a monotonic window function starts producing
values that result in a pushed-down qual no longer matching, and will
never match again due to the window function's monotonic properties.

That commit requires a SupportRequestWFuncMonotonic to exist on the
window function and for it to detect when the function is monotonic.  For
COUNT(ANY) and COUNT(*), the support function failed to consider some
cases where the WindowClause used EXCLUDE to exclude certain rows from
being aggregated.  Some WindowClause definitions mean we aggregate rows
that come after the current row, and when processing those rows later,
if we EXCLUDE certain rows, the monotonic property can be broken.
Wrongly treating the COUNT(*) or COUNT(ANY) aggregate as monotonic could
lead to rows being filtered that should not be filtered from the result
set.

Another issue was that the support function for the COUNT aggregate
mistakenly thought that a WindowClause without an ORDER BY meant that
the results would be both monotonically increasing and decreasing, but
that's only true when in RANGE mode, where all rows are peers.

It is possible to support various cases that do have an EXCLUDE clause,
but getting the logic correct for the exact set of cases that are valid
is quite complex and would likely better be left for a future project.

Here, we mostly disable run condition pushdown when there is an EXCLUDE
clause unless the clause is for EXCLUDE CURRENT ROW, uses COUNT(*)
(rather than COUNT(ANY)), and the window aggregate has no FILTER clause.

Bug: #19533
Reported-by: Qifan Liu <imchifan@163.com>
Author: Chengpeng Yan <chengpeng_yan@outlook.com>
Author: David Rowley <dgrowleyml@gmail.com>
Reviewed-by: Richard Guo <guofenglinux@gmail.com>
Reviewed-by: John Naylor <johncnaylorls@gmail.com>
Discussion: https://postgr.es/m/19533-413a1014e5d0e766@postgresql.org
Backpatch-through: 15
This commit is contained in:
David Rowley 2026-07-07 23:59:08 +12:00
parent 441e4c8d69
commit cf184ec77a
3 changed files with 362 additions and 10 deletions

View file

@ -24,7 +24,7 @@
#include "nodes/supportnodes.h"
#include "optimizer/optimizer.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
typedef struct
{
@ -833,8 +833,38 @@ int8inc_support(PG_FUNCTION_ARGS)
MonotonicFunction monotonic = MONOTONICFUNC_NONE;
int frameOptions = req->window_clause->frameOptions;
/* No ORDER BY clause then all rows are peers */
if (req->window_clause->orderClause == NIL)
/*
* Because an EXCLUDE clauses in the window definition can exclude
* rows that have previously been included in the aggregate result for
* prior rows, this can break the monotonic properties that might
* otherwise be guaranteed. There's a narrow set of circumstances
* that can be guaranteed, which we check for below.
*/
if (frameOptions & FRAMEOPTION_EXCLUSION)
{
WindowFunc *wfunc = req->window_func;
/*
* To add handling for all valid monotonic cases with an EXCLUDE
* clause is complex and likely not worth troubling over. For
* now, just bail unless we see EXCLUDE CURRENT ROW with COUNT(*)
* and no FILTER. Excluding the current row is fine when using
* COUNT(*) as this always reduces the count by 1. The same isn't
* true for COUNY(ANY) as a NULL won't be counted, and a
* subsequent non-NULL could make the count decrease.
*/
if ((frameOptions & FRAMEOPTION_EXCLUDE_CURRENT_ROW) == 0 ||
wfunc->winfnoid != F_COUNT_ ||
wfunc->aggfilter != NULL)
{
req->monotonic = MONOTONICFUNC_NONE;
PG_RETURN_POINTER(req);
}
}
/* No ORDER BY clause and RANGE mode means all rows are peers. */
if (req->window_clause->orderClause == NIL &&
(frameOptions & FRAMEOPTION_RANGE))
monotonic = MONOTONICFUNC_BOTH;
else
{

View file

@ -4232,23 +4232,59 @@ WHERE c <= 3;
(8 rows)
-- Ensure we get the correct run condition when the window function is both
-- monotonically increasing and decreasing.
-- monotonically increasing and decreasing in RANGE mode without an ORDER BY
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
depname,
salary,
count(empno) OVER () c
count(empno) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) c
FROM empsalary) emp
WHERE c = 1;
QUERY PLAN
-------------------------------------------------------
QUERY PLAN
-------------------------------------------------------------
WindowAgg
Window: w1 AS ()
Window: w1 AS (RANGE BETWEEN CURRENT ROW AND CURRENT ROW)
Run Condition: (count(empsalary.empno) OVER w1 = 1)
-> Seq Scan on empsalary
(4 rows)
-- As above, but check we detect it's monotonically increasing
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
depname,
salary,
count(empno) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) c
FROM empsalary) emp
WHERE c <= 3;
QUERY PLAN
-------------------------------------------------------------
WindowAgg
Window: w1 AS (RANGE BETWEEN CURRENT ROW AND CURRENT ROW)
Run Condition: (count(empsalary.empno) OVER w1 <= 3)
-> Seq Scan on empsalary
(4 rows)
-- Ensure that ROWS mode without an ORDER BY doesn't think it's monotonically
-- decreasing, i.e. don't push down the run condition.
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
depname,
salary,
count(empno) OVER (ROWS BETWEEN CURRENT ROW AND CURRENT ROW) c
FROM empsalary) emp
WHERE c > 1;
QUERY PLAN
------------------------------------------------------------------
Subquery Scan on emp
Filter: (emp.c > 1)
-> WindowAgg
Window: w1 AS (ROWS BETWEEN CURRENT ROW AND CURRENT ROW)
-> Seq Scan on empsalary
(5 rows)
-- Try another case with a WindowFunc with a byref return type
SELECT * FROM
(SELECT row_number() OVER (PARTITION BY salary) AS rn,
@ -4436,6 +4472,181 @@ WHERE c = 1;
-> Seq Scan on empsalary
(9 rows)
--
-- Ensure we get the correct behavior for run condition pushdown when the
-- frame option has an EXCLUDE clause
--
-- Ensure pushdown occurs for ROWS BETWEEN UNBOUNDED PRECEDING with EXCLUDE
-- CURRENT ROW with COUNT(*)
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE CURRENT ROW) c
FROM empsalary) emp
WHERE c <= 3;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------
WindowAgg
Window: w1 AS (ORDER BY empsalary.salary ROWS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE CURRENT ROW)
Run Condition: (count(*) OVER w1 <= 3)
-> Sort
Sort Key: empsalary.salary
-> Seq Scan on empsalary
(6 rows)
-- Ensure pushdown occurs for GROUPS BETWEEN UNBOUNDED PRECEDING with EXCLUDE
-- CURRENT ROW with COUNT(*)
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW) c
FROM empsalary) emp
WHERE c <= 3;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
WindowAgg
Window: w1 AS (ORDER BY empsalary.salary GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW)
Run Condition: (count(*) OVER w1 <= 3)
-> Sort
Sort Key: empsalary.salary
-> Seq Scan on empsalary
(6 rows)
-- Ensure pushdown occurs for RANGE BETWEEN UNBOUNDED PRECEDING with EXCLUDE
-- CURRENT ROW with COUNT(*)
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW) c
FROM empsalary) emp
WHERE c <= 3;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------
WindowAgg
Window: w1 AS (ORDER BY empsalary.salary RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW)
Run Condition: (count(*) OVER w1 <= 3)
-> Sort
Sort Key: empsalary.salary
-> Seq Scan on empsalary
(6 rows)
-- Ensure we don't get pushdown when a FILTER clause is present
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) FILTER (WHERE salary > 4000) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE CURRENT ROW) c
FROM empsalary) emp
WHERE c <= 3;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Subquery Scan on emp
Filter: (emp.c <= 3)
-> WindowAgg
Window: w1 AS (ORDER BY empsalary.salary ROWS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE CURRENT ROW)
-> Sort
Sort Key: empsalary.salary
-> Seq Scan on empsalary
(7 rows)
-- Ensure we don't get pushdown with COUNT(ANY)
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(salary) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE CURRENT ROW) c
FROM empsalary) emp
WHERE c <= 3;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Subquery Scan on emp
Filter: (emp.c <= 3)
-> WindowAgg
Window: w1 AS (ORDER BY empsalary.salary ROWS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE CURRENT ROW)
-> Sort
Sort Key: empsalary.salary
-> Seq Scan on empsalary
(7 rows)
-- Ensure we don't get pushdown with EXCLUDE GROUP
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE GROUP) c
FROM empsalary) emp
WHERE c <= 3;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------
Subquery Scan on emp
Filter: (emp.c <= 3)
-> WindowAgg
Window: w1 AS (ORDER BY empsalary.salary ROWS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE GROUP)
-> Sort
Sort Key: empsalary.salary
-> Seq Scan on empsalary
(7 rows)
-- Ensure we don't get pushdown with EXCLUDE TIES
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE TIES) c
FROM empsalary) emp
WHERE c <= 3;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Subquery Scan on emp
Filter: (emp.c <= 3)
-> WindowAgg
Window: w1 AS (ORDER BY empsalary.salary ROWS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE TIES)
-> Sort
Sort Key: empsalary.salary
-> Seq Scan on empsalary
(7 rows)
-- Ensure we don't get pushdown with GROUPS mode and EXCLUDE GROUP
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary GROUPS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE GROUP) c
FROM empsalary) emp
WHERE c <= 3;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------
Subquery Scan on emp
Filter: (emp.c <= 3)
-> WindowAgg
Window: w1 AS (ORDER BY empsalary.salary GROUPS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE GROUP)
-> Sort
Sort Key: empsalary.salary
-> Seq Scan on empsalary
(7 rows)
-- Ensure we don't get pushdown with GROUPS mode and EXCLUDE TIES
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary GROUPS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE TIES) c
FROM empsalary) emp
WHERE c <= 3;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------
Subquery Scan on emp
Filter: (emp.c <= 3)
-> WindowAgg
Window: w1 AS (ORDER BY empsalary.salary GROUPS BETWEEN UNBOUNDED PRECEDING AND '0'::bigint PRECEDING EXCLUDE TIES)
-> Sort
Sort Key: empsalary.salary
-> Seq Scan on empsalary
(7 rows)
-- Test Sort node collapsing
EXPLAIN (COSTS OFF)
SELECT * FROM

View file

@ -1361,16 +1361,37 @@ SELECT * FROM
WHERE c <= 3;
-- Ensure we get the correct run condition when the window function is both
-- monotonically increasing and decreasing.
-- monotonically increasing and decreasing in RANGE mode without an ORDER BY
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
depname,
salary,
count(empno) OVER () c
count(empno) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) c
FROM empsalary) emp
WHERE c = 1;
-- As above, but check we detect it's monotonically increasing
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
depname,
salary,
count(empno) OVER (RANGE BETWEEN CURRENT ROW AND CURRENT ROW) c
FROM empsalary) emp
WHERE c <= 3;
-- Ensure that ROWS mode without an ORDER BY doesn't think it's monotonically
-- decreasing, i.e. don't push down the run condition.
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
depname,
salary,
count(empno) OVER (ROWS BETWEEN CURRENT ROW AND CURRENT ROW) c
FROM empsalary) emp
WHERE c > 1;
-- Try another case with a WindowFunc with a byref return type
SELECT * FROM
(SELECT row_number() OVER (PARTITION BY salary) AS rn,
@ -1460,6 +1481,96 @@ SELECT * FROM
FROM empsalary) emp
WHERE c = 1;
--
-- Ensure we get the correct behavior for run condition pushdown when the
-- frame option has an EXCLUDE clause
--
-- Ensure pushdown occurs for ROWS BETWEEN UNBOUNDED PRECEDING with EXCLUDE
-- CURRENT ROW with COUNT(*)
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE CURRENT ROW) c
FROM empsalary) emp
WHERE c <= 3;
-- Ensure pushdown occurs for GROUPS BETWEEN UNBOUNDED PRECEDING with EXCLUDE
-- CURRENT ROW with COUNT(*)
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW) c
FROM empsalary) emp
WHERE c <= 3;
-- Ensure pushdown occurs for RANGE BETWEEN UNBOUNDED PRECEDING with EXCLUDE
-- CURRENT ROW with COUNT(*)
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW) c
FROM empsalary) emp
WHERE c <= 3;
-- Ensure we don't get pushdown when a FILTER clause is present
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) FILTER (WHERE salary > 4000) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE CURRENT ROW) c
FROM empsalary) emp
WHERE c <= 3;
-- Ensure we don't get pushdown with COUNT(ANY)
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(salary) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE CURRENT ROW) c
FROM empsalary) emp
WHERE c <= 3;
-- Ensure we don't get pushdown with EXCLUDE GROUP
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE GROUP) c
FROM empsalary) emp
WHERE c <= 3;
-- Ensure we don't get pushdown with EXCLUDE TIES
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE TIES) c
FROM empsalary) emp
WHERE c <= 3;
-- Ensure we don't get pushdown with GROUPS mode and EXCLUDE GROUP
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary GROUPS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE GROUP) c
FROM empsalary) emp
WHERE c <= 3;
-- Ensure we don't get pushdown with GROUPS mode and EXCLUDE TIES
EXPLAIN (COSTS OFF)
SELECT * FROM
(SELECT empno,
salary,
count(*) OVER (ORDER BY salary GROUPS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING EXCLUDE TIES) c
FROM empsalary) emp
WHERE c <= 3;
-- Test Sort node collapsing
EXPLAIN (COSTS OFF)
SELECT * FROM