From 4ae2e580fefb839a8b1cfb7fc160542e1cac4962 Mon Sep 17 00:00:00 2001 From: Alexandre Felipe Date: Tue, 14 Jul 2026 00:41:55 +0100 Subject: [PATCH-v10 3/8] Order by NOT NULL When building index path keys both the sort order and the null order must match. If a column is not null the nulls order actually doesn't affect the result, so it can be ignored. This change adds a verification when creating cannonical pathkeys, if all the other conditions match, but the nulls order is different it will check if the pathkey expression can be proven null. --- src/backend/optimizer/path/pathkeys.c | 17 ++++- src/test/regress/expected/slope.out | 92 +++++++++++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/slope.sql | 32 ++++++++++ 4 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/slope.out create mode 100644 src/test/regress/sql/slope.sql diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c index 3e3eb720c1f..1c89d9db997 100644 --- a/src/backend/optimizer/path/pathkeys.c +++ b/src/backend/optimizer/path/pathkeys.c @@ -74,9 +74,20 @@ make_canonical_pathkey(PlannerInfo *root, pk = (PathKey *) lfirst(lc); if (eclass == pk->pk_eclass && opfamily == pk->pk_opfamily && - cmptype == pk->pk_cmptype && - nulls_first == pk->pk_nulls_first) - return pk; + cmptype == pk->pk_cmptype) + { + ListCell *lc2; + + if (nulls_first == pk->pk_nulls_first) + return pk; + foreach(lc2, eclass->ec_members) + { + EquivalenceMember *em = (EquivalenceMember *) lfirst(lc2); + + if (expr_is_nonnullable(root, em->em_expr, NOTNULL_SOURCE_RELOPT)) + return pk; + } + } } /* diff --git a/src/test/regress/expected/slope.out b/src/test/regress/expected/slope.out new file mode 100644 index 00000000000..a72195a9fec --- /dev/null +++ b/src/test/regress/expected/slope.out @@ -0,0 +1,92 @@ +CREATE TABLE slope_t1 (a int, b int NOT NULL); +INSERT INTO slope_t1 SELECT i, i FROM generate_series(1, 10) AS i; +CREATE INDEX slope_t1_a_idx ON slope_t1 (a); +CREATE INDEX slope_t1_b_idx ON slope_t1 (b); +ANALYSE slope_t1; +SET enable_seqscan = off; +SET enable_bitmapscan = off; +-- Column `a` can be scanned in two directions, but the +-- nulls order of the query must match the nulls order of the index. +EXPLAIN SELECT a FROM slope_t1 ORDER BY 1 ASC NULLS FIRST; + QUERY PLAN +------------------------------------------------------------------------------------------- + Sort (cost=8.45..8.48 rows=10 width=4) + Sort Key: a NULLS FIRST + -> Index Only Scan using slope_t1_a_idx on slope_t1 (cost=0.14..8.29 rows=10 width=4) +(3 rows) + +EXPLAIN SELECT a FROM slope_t1 ORDER BY 1 ASC NULLS LAST; + QUERY PLAN +------------------------------------------------------------------------------------- + Index Only Scan using slope_t1_a_idx on slope_t1 (cost=0.14..8.29 rows=10 width=4) +(1 row) + +EXPLAIN SELECT a FROM slope_t1 ORDER BY 1 DESC NULLS FIRST; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Index Only Scan Backward using slope_t1_a_idx on slope_t1 (cost=0.14..8.29 rows=10 width=4) +(1 row) + +EXPLAIN SELECT a FROM slope_t1 ORDER BY 1 DESC NULLS LAST; + QUERY PLAN +------------------------------------------------------------------------------------------- + Sort (cost=8.45..8.48 rows=10 width=4) + Sort Key: a DESC NULLS LAST + -> Index Only Scan using slope_t1_a_idx on slope_t1 (cost=0.14..8.29 rows=10 width=4) +(3 rows) + +-- Column `b` is NOT NULL by the schema. +EXPLAIN SELECT b FROM slope_t1 ORDER BY 1 ASC NULLS FIRST; + QUERY PLAN +------------------------------------------------------------------------------------- + Index Only Scan using slope_t1_b_idx on slope_t1 (cost=0.14..8.29 rows=10 width=4) +(1 row) + +EXPLAIN SELECT b FROM slope_t1 ORDER BY 1 ASC NULLS LAST; + QUERY PLAN +------------------------------------------------------------------------------------- + Index Only Scan using slope_t1_b_idx on slope_t1 (cost=0.14..8.29 rows=10 width=4) +(1 row) + +EXPLAIN SELECT b FROM slope_t1 ORDER BY 1 DESC NULLS FIRST; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Index Only Scan Backward using slope_t1_b_idx on slope_t1 (cost=0.14..8.29 rows=10 width=4) +(1 row) + +EXPLAIN SELECT b FROM slope_t1 ORDER BY 1 DESC NULLS LAST; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Index Only Scan Backward using slope_t1_b_idx on slope_t1 (cost=0.14..8.29 rows=10 width=4) +(1 row) + +-- a query where a is not null by an equivalence class +EXPLAIN SELECT a FROM slope_t1 WHERE a = b ORDER BY 1 ASC NULLS FIRST; + QUERY PLAN +------------------------------------------------------------------------------- + Index Scan using slope_t1_b_idx on slope_t1 (cost=0.14..8.31 rows=1 width=4) + Filter: (a = b) +(2 rows) + +EXPLAIN SELECT a FROM slope_t1 WHERE a = b ORDER BY 1 ASC NULLS LAST; + QUERY PLAN +------------------------------------------------------------------------------- + Index Scan using slope_t1_b_idx on slope_t1 (cost=0.14..8.31 rows=1 width=4) + Filter: (a = b) +(2 rows) + +EXPLAIN SELECT a FROM slope_t1 WHERE a = b ORDER BY 1 DESC NULLS FIRST; + QUERY PLAN +---------------------------------------------------------------------------------------- + Index Scan Backward using slope_t1_b_idx on slope_t1 (cost=0.14..8.31 rows=1 width=4) + Filter: (a = b) +(2 rows) + +EXPLAIN SELECT a FROM slope_t1 WHERE a = b ORDER BY 1 DESC NULLS LAST; + QUERY PLAN +---------------------------------------------------------------------------------------- + Index Scan Backward using slope_t1_b_idx on slope_t1 (cost=0.14..8.31 rows=1 width=4) + Filter: (a = b) +(2 rows) + +DROP TABLE slope_t1; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8fa0a6c47fb..a88cde8945f 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -66,7 +66,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi # ---------- # Another group of parallel tests # ---------- -test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated_stored join_hash +test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated_stored join_hash slope # ---------- # Additional BRIN tests diff --git a/src/test/regress/sql/slope.sql b/src/test/regress/sql/slope.sql new file mode 100644 index 00000000000..e2f4cfa2727 --- /dev/null +++ b/src/test/regress/sql/slope.sql @@ -0,0 +1,32 @@ +CREATE TABLE slope_t1 (a int, b int NOT NULL); + +INSERT INTO slope_t1 SELECT i, i FROM generate_series(1, 10) AS i; +CREATE INDEX slope_t1_a_idx ON slope_t1 (a); +CREATE INDEX slope_t1_b_idx ON slope_t1 (b); + +ANALYSE slope_t1; + +SET enable_seqscan = off; +SET enable_bitmapscan = off; + +-- Column `a` can be scanned in two directions, but the +-- nulls order of the query must match the nulls order of the index. +EXPLAIN SELECT a FROM slope_t1 ORDER BY 1 ASC NULLS FIRST; +EXPLAIN SELECT a FROM slope_t1 ORDER BY 1 ASC NULLS LAST; +EXPLAIN SELECT a FROM slope_t1 ORDER BY 1 DESC NULLS FIRST; +EXPLAIN SELECT a FROM slope_t1 ORDER BY 1 DESC NULLS LAST; + +-- Column `b` is NOT NULL by the schema. +EXPLAIN SELECT b FROM slope_t1 ORDER BY 1 ASC NULLS FIRST; +EXPLAIN SELECT b FROM slope_t1 ORDER BY 1 ASC NULLS LAST; +EXPLAIN SELECT b FROM slope_t1 ORDER BY 1 DESC NULLS FIRST; +EXPLAIN SELECT b FROM slope_t1 ORDER BY 1 DESC NULLS LAST; + + +-- a query where a is not null by an equivalence class +EXPLAIN SELECT a FROM slope_t1 WHERE a = b ORDER BY 1 ASC NULLS FIRST; +EXPLAIN SELECT a FROM slope_t1 WHERE a = b ORDER BY 1 ASC NULLS LAST; +EXPLAIN SELECT a FROM slope_t1 WHERE a = b ORDER BY 1 DESC NULLS FIRST; +EXPLAIN SELECT a FROM slope_t1 WHERE a = b ORDER BY 1 DESC NULLS LAST; + +DROP TABLE slope_t1; \ No newline at end of file -- 2.53.0