From 4ae2e580fefb839a8b1cfb7fc160542e1cac4962 Mon Sep 17 00:00:00 2001
From: Alexandre Felipe <o.alexandre.felipe@gmail.com>
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

