v8-0006-SLOPE-redundancy-checks.patch
application/octet-stream
Filename: v8-0006-SLOPE-redundancy-checks.patch
Type: application/octet-stream
Part: 5
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v8-0006
Subject: SLOPE: redundancy checks
| File | + | − |
|---|---|---|
| src/backend/optimizer/path/pathkeys.c | 68 | 44 |
| src/test/regress/expected/slope.out | 119 | 0 |
| src/test/regress/sql/slope.sql | 59 | 0 |
From bbde7e9d939647862dc0bd0fcf7ede6a4ed8aaab Mon Sep 17 00:00:00 2001
From: Alexandre Felipe <o.alexandre.felipe@gmail.com>
Date: Tue, 7 Apr 2026 08:23:20 +0100
Subject: [PATCH 6/6] SLOPE: redundancy checks
Detect two redundant cases that can be proven from monotonicity
1. When multiple pathkeys depend on the same index column,
and each is a monotonic function of the next
i.e. f2(f1(x)), f1(x), x, where f1 and f2 are monotonic functions.
This can be motivated by a query
SELECT row_number() OVER (PARTITION BY ts::date ORDER BY ts)
An index on ts would satisfy both the order on ts::date and on ts,
but when ts expression ts::dat
2. A pathkey is a function of a preceeding pathkey
i.e. x, ..., f(x),
This is a generalisation of the existing redundancy check
Notice that this doesn't require f(x) to be monotonic,
just invariant.
---
src/backend/optimizer/path/pathkeys.c | 112 ++++++++++++++----------
src/test/regress/expected/slope.out | 119 ++++++++++++++++++++++++++
src/test/regress/sql/slope.sql | 59 +++++++++++++
3 files changed, 246 insertions(+), 44 deletions(-)
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index 0c0d1138cac..d5f6439f7f8 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -1140,14 +1140,22 @@ build_index_pathkeys(PlannerInfo *root,
false);
/*
- * SLOPE: if the first unmatched query pathkey is a monotonic
- * function of this index column, use that pathkey instead of
- * the column's own pathkey so the index can satisfy the query
- * ordering without a Sort.
+ * SLOPE: scan query_pathkeys to emit all pathkeys that this
+ * index column can satisfy — both direct (EquivalenceClass)
+ * and monotonic (SLOPE) matches — in query order.
+ *
+ * Emitting in query order handles pathkey chains like:
+ *
+ * [f(x), g(x), x] with all monotonic in x, the index order
+ * on x implies the order of every function.
+ *
+ * [x, ..., f(x)] with f(x) is constant when x is constant, so
+ * f(x) is a redundant tiebreaker after x.
*/
- if (index->rel->reloptkind == RELOPT_BASEREL)
{
ListCell *lc2;
+ bool cpathkey_emitted = false;
+ bool slope_matched = false;
foreach(lc2, root->query_pathkeys)
{
@@ -1156,56 +1164,72 @@ build_index_pathkeys(PlannerInfo *root,
if (pathkey_is_redundant(qpk, retval))
continue;
- if (cpathkey && qpk->pk_eclass == cpathkey->pk_eclass)
- break;
+ if (cpathkey &&
+ qpk->pk_eclass == cpathkey->pk_eclass)
+ {
+ if (!pathkey_is_redundant(cpathkey, retval))
+ retval = lappend(retval, cpathkey);
+ cpathkey_emitted = true;
+ continue;
+ }
- if (qpk->pk_var != NULL &&
+ if (index->rel->reloptkind == RELOPT_BASEREL &&
+ qpk->pk_var != NULL &&
!qpk->pk_eclass->ec_has_volatile &&
qpk->pk_varrelid == index->rel->relid &&
equal(qpk->pk_var, indexkey))
{
- PathKey *spk;
-
- if (qpk->pk_slope == MONOTONICFUNC_BOTH)
+ /*
+ * Case 1: f(x) before x — descending chain.
+ * Need monotonicity to determine the sort direction
+ * that f(x) inherits from the index order on x.
+ */
+ if (!cpathkey_emitted)
{
- EquivalenceMember *em;
-
- em = linitial(qpk->pk_eclass->ec_members);
- qpk->pk_slope = get_expr_slope_wrt(em->em_expr,
- qpk->pk_var);
+ PathKey *spk;
+
+ if (qpk->pk_slope == MONOTONICFUNC_BOTH)
+ {
+ EquivalenceMember *em;
+
+ em = linitial(qpk->pk_eclass->ec_members);
+ qpk->pk_slope = get_expr_slope_wrt(em->em_expr,
+ qpk->pk_var);
+ }
+ spk = slope_emit_pathkey(root, qpk, indexkey,
+ reverse_sort, nulls_first);
+ if (spk && !pathkey_is_redundant(spk, retval))
+ retval = lappend(retval, spk);
+ slope_matched = true;
+ continue;
}
- spk = slope_emit_pathkey(root, qpk, indexkey,
- reverse_sort, nulls_first);
- if (spk)
- cpathkey = spk;
+
+ /*
+ * Case 2: f(x) after x — ascending chain.
+ * x is already in retval, so within each group of
+ * equal x values, f(x) is constant (for any
+ * deterministic f). The pathkey is redundant as a
+ * tiebreaker regardless of monotonicity.
+ */
+ if (!pathkey_is_redundant(qpk, retval))
+ retval = lappend(retval, qpk);
+ slope_matched = true;
+ continue;
}
break;
}
- }
- if (cpathkey)
- {
- /*
- * We found the sort key in an EquivalenceClass, so it's relevant
- * for this query. Add it to list, unless it's redundant.
- */
- if (!pathkey_is_redundant(cpathkey, retval))
- retval = lappend(retval, cpathkey);
- }
- else
- {
- /*
- * Boolean index keys might be redundant even if they do not
- * appear in an EquivalenceClass, because of our special treatment
- * of boolean equality conditions --- see the comment for
- * indexcol_is_bool_constant_for_query(). If that applies, we can
- * continue to examine lower-order index columns. Otherwise, the
- * sort key is not an interesting sort order for this query, so we
- * should stop considering index columns; any lower-order sort
- * keys won't be useful either.
- */
- if (!indexcol_is_bool_constant_for_query(root, index, i))
- break;
+ if (!cpathkey_emitted)
+ {
+ if (cpathkey)
+ {
+ if (!pathkey_is_redundant(cpathkey, retval))
+ retval = lappend(retval, cpathkey);
+ }
+ else if (!slope_matched &&
+ !indexcol_is_bool_constant_for_query(root, index, i))
+ break;
+ }
}
i++;
diff --git a/src/test/regress/expected/slope.out b/src/test/regress/expected/slope.out
index 13fe339caf5..74d7f82b94a 100644
--- a/src/test/regress/expected/slope.out
+++ b/src/test/regress/expected/slope.out
@@ -401,5 +401,124 @@ FROM slope_nulls_results ORDER BY seq;
RESET enable_seqscan;
DROP TABLE slope_nulls_results;
DROP TABLE slope_nulls_tmp;
+--
+-- Test pathkey chains: f(x), x (descending chain)
+-- The order of f(x) is implied by the order of x when f is monotonic.
+-- A single index on ts should satisfy ORDER BY ts::date, ts.
+--
+-- descending chain: ts::date, ts — both from index on ts
+explain (costs off, verbose)
+select * from slope_src order by ts::date, ts limit 10;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Limit
+ Output: id, v_int2, v_int4, v_int8, v_float4, v_float8, v_numeric, ts, tstz, ((ts)::date)
+ -> Index Scan using slope_src_ts_idx on public.slope_src
+ Output: id, v_int2, v_int4, v_int8, v_float4, v_float8, v_numeric, ts, tstz, (ts)::date
+(4 rows)
+
+-- descending chain: date_trunc('day', ts), ts
+explain (costs off, verbose)
+select * from slope_src order by date_trunc('day', ts), ts limit 10;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: id, v_int2, v_int4, v_int8, v_float4, v_float8, v_numeric, ts, tstz, (date_trunc('day'::text, ts))
+ -> Index Scan using slope_src_ts_idx on public.slope_src
+ Output: id, v_int2, v_int4, v_int8, v_float4, v_float8, v_numeric, ts, tstz, date_trunc('day'::text, ts)
+(4 rows)
+
+-- descending chain with siblings: date_trunc('month', ts), ts::date, ts
+explain (costs off, verbose)
+select * from slope_src order by date_trunc('month', ts), ts::date, ts limit 10;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: id, v_int2, v_int4, v_int8, v_float4, v_float8, v_numeric, ts, tstz, (date_trunc('month'::text, ts)), ((ts)::date)
+ -> Index Scan using slope_src_ts_idx on public.slope_src
+ Output: id, v_int2, v_int4, v_int8, v_float4, v_float8, v_numeric, ts, tstz, date_trunc('month'::text, ts), (ts)::date
+(4 rows)
+
+--
+-- Test pathkey chains: x, f(x) (ascending chain)
+-- f(x) is constant when x is constant, so f(x) is a redundant tiebreaker.
+-- A single index on ts should satisfy ORDER BY ts, ts::date.
+--
+-- ascending chain: ts, ts::date — f(x) redundant after x
+explain (costs off, verbose)
+select * from slope_src order by ts, ts::date limit 10;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Limit
+ Output: id, v_int2, v_int4, v_int8, v_float4, v_float8, v_numeric, ts, tstz, ((ts)::date)
+ -> Index Scan using slope_src_ts_idx on public.slope_src
+ Output: id, v_int2, v_int4, v_int8, v_float4, v_float8, v_numeric, ts, tstz, (ts)::date
+(4 rows)
+
+-- ascending chain: ts, date_trunc('day', ts)
+explain (costs off, verbose)
+select * from slope_src order by ts, date_trunc('day', ts) limit 10;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------
+ Limit
+ Output: id, v_int2, v_int4, v_int8, v_float4, v_float8, v_numeric, ts, tstz, (date_trunc('day'::text, ts))
+ -> Index Scan using slope_src_ts_idx on public.slope_src
+ Output: id, v_int2, v_int4, v_int8, v_float4, v_float8, v_numeric, ts, tstz, date_trunc('day'::text, ts)
+(4 rows)
+
+-- ascending chain with non-monotonic but immutable function:
+-- abs() is immutable but NOT monotonic; still valid as tiebreaker.
+explain (costs off, verbose)
+select v_float8, abs(v_float8)
+from slope_src order by v_float8, abs(v_float8) limit 10;
+ QUERY PLAN
+------------------------------------------------------------------------
+ Limit
+ Output: v_float8, (abs(v_float8))
+ -> Index Only Scan using slope_src_v_float8_idx on public.slope_src
+ Output: v_float8, abs(v_float8)
+(4 rows)
+
+--
+-- Test DISTINCT with monotonic functions
+--
+-- DISTINCT on ts::date should use index on ts
+explain (costs off, verbose)
+select distinct ts::date from slope_src;
+ QUERY PLAN
+------------------------------------------------------------------
+ Unique
+ Output: ((ts)::date)
+ -> Index Only Scan using slope_src_ts_idx on public.slope_src
+ Output: (ts)::date
+(4 rows)
+
+-- DISTINCT on floor(v_float8)
+explain (costs off, verbose)
+select distinct floor(v_float8) from slope_src;
+ QUERY PLAN
+------------------------------------------------------------------------
+ Unique
+ Output: (floor(v_float8))
+ -> Index Only Scan using slope_src_v_float8_idx on public.slope_src
+ Output: floor(v_float8)
+(4 rows)
+
+--
+-- Test window functions with PARTITION BY monotonic function
+--
+-- Window with PARTITION BY ts::date ORDER BY ts
+explain (costs off, verbose)
+select ts::date, ts, row_number() over (partition by ts::date order by ts)
+from slope_src;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------
+ WindowAgg
+ Output: ((ts)::date), ts, row_number() OVER w1
+ Window: w1 AS (PARTITION BY (slope_src.ts)::date ORDER BY slope_src.ts ROWS UNBOUNDED PRECEDING)
+ -> Index Only Scan using slope_src_ts_idx on public.slope_src
+ Output: (ts)::date, ts
+(5 rows)
+
-- Cleanup
RESET enable_hashagg;
diff --git a/src/test/regress/sql/slope.sql b/src/test/regress/sql/slope.sql
index 2de5d69cdf3..31eba90695f 100644
--- a/src/test/regress/sql/slope.sql
+++ b/src/test/regress/sql/slope.sql
@@ -215,5 +215,64 @@ RESET enable_seqscan;
DROP TABLE slope_nulls_results;
DROP TABLE slope_nulls_tmp;
+--
+-- Test pathkey chains: f(x), x (descending chain)
+-- The order of f(x) is implied by the order of x when f is monotonic.
+-- A single index on ts should satisfy ORDER BY ts::date, ts.
+--
+
+-- descending chain: ts::date, ts — both from index on ts
+explain (costs off, verbose)
+select * from slope_src order by ts::date, ts limit 10;
+
+-- descending chain: date_trunc('day', ts), ts
+explain (costs off, verbose)
+select * from slope_src order by date_trunc('day', ts), ts limit 10;
+
+-- descending chain with siblings: date_trunc('month', ts), ts::date, ts
+explain (costs off, verbose)
+select * from slope_src order by date_trunc('month', ts), ts::date, ts limit 10;
+
+--
+-- Test pathkey chains: x, f(x) (ascending chain)
+-- f(x) is constant when x is constant, so f(x) is a redundant tiebreaker.
+-- A single index on ts should satisfy ORDER BY ts, ts::date.
+--
+
+-- ascending chain: ts, ts::date — f(x) redundant after x
+explain (costs off, verbose)
+select * from slope_src order by ts, ts::date limit 10;
+
+-- ascending chain: ts, date_trunc('day', ts)
+explain (costs off, verbose)
+select * from slope_src order by ts, date_trunc('day', ts) limit 10;
+
+-- ascending chain with non-monotonic but immutable function:
+-- abs() is immutable but NOT monotonic; still valid as tiebreaker.
+explain (costs off, verbose)
+select v_float8, abs(v_float8)
+from slope_src order by v_float8, abs(v_float8) limit 10;
+
+--
+-- Test DISTINCT with monotonic functions
+--
+
+-- DISTINCT on ts::date should use index on ts
+explain (costs off, verbose)
+select distinct ts::date from slope_src;
+
+-- DISTINCT on floor(v_float8)
+explain (costs off, verbose)
+select distinct floor(v_float8) from slope_src;
+
+--
+-- Test window functions with PARTITION BY monotonic function
+--
+
+-- Window with PARTITION BY ts::date ORDER BY ts
+explain (costs off, verbose)
+select ts::date, ts, row_number() over (partition by ts::date order by ts)
+from slope_src;
+
-- Cleanup
RESET enable_hashagg;
--
2.53.0