]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Add _bt_set_startikey row compare test coverage.
authorPeter Geoghegan <pg@bowt.ie>
Sun, 26 Jul 2026 16:49:56 +0000 (12:49 -0400)
committerPeter Geoghegan <pg@bowt.ie>
Sun, 26 Jul 2026 16:49:56 +0000 (12:49 -0400)
Add pg_regress tests that exercise the row compare logic that commit
7d9cd2df added to _bt_set_startikey.  Also add tests that exercise the
_bt_set_startikey SAOP array path.

Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAH2-Wz=KjQsD2W2a=b51uH905=0mF6Le4evhWkN2FL1+uRPhUg@mail.gmail.com
Backpatch-through: 19

src/test/regress/expected/btree_index.out
src/test/regress/sql/btree_index.sql

index 21dc9b5783a7c5a35a698aa6ba7f487c814b1c0e..3a83e9a05347d41aa4dd35a12d7bd90e6ec24b22 100644 (file)
@@ -308,6 +308,163 @@ ORDER BY proname, proargtypes, pronamespace;
 ---------+-------------+--------------
 (0 rows)
 
+--
+-- Test RowCompare handling within _bt_set_startikey, which decides whether
+-- every tuple on a page (a page beyond the scan's first) must satisfy the
+-- scan's RowCompare qual.
+--
+-- The index mixes an ASC column with a DESC column (so RowCompare members
+-- don't all use the same inequality strategy and are not marked required),
+-- uses a low fillfactor (so scans read several pages), and disables
+-- deduplication (so the "b" NULLs span more than one page).
+create temp table btree_rowcompare_tab (a int, b int, c int);
+insert into btree_rowcompare_tab
+  select a, b, b from generate_series(1, 3) a, generate_series(1, 150) b;
+insert into btree_rowcompare_tab
+  select 2, null, null from generate_series(1, 50);
+create index btree_rowcompare_idx on btree_rowcompare_tab (a, b desc, c)
+  with (fillfactor = 10, deduplicate_items = off);
+vacuum analyze btree_rowcompare_tab;
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+-- RowCompare satisfied by every tuple on many pages (decided by its first
+-- member on "a = 3" pages, and by its final member on "a = 2" pages)
+explain (costs off)
+select count(*) from btree_rowcompare_tab where (a, b) >= (2, 75);
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using btree_rowcompare_idx on btree_rowcompare_tab
+         Index Cond: (ROW(a, b) >= ROW(2, 75))
+(3 rows)
+
+select count(*) from btree_rowcompare_tab where (a, b) >= (2, 75);
+ count 
+-------
+   226
+(1 row)
+
+-- Reaches the RowCompare's unsatisfiable NULL member argument on "a = 2"
+-- pages (the "a = 2" key positions the scan within the "a = 2" group, which
+-- the RowCompare qual alone would not).  The combined quals are
+-- contradictory, but preprocessing cannot detect that.
+explain (costs off)
+select count(*) from btree_rowcompare_tab where a = 2 and (a, b) >= (2, null);
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using btree_rowcompare_idx on btree_rowcompare_tab
+         Index Cond: ((ROW(a, b) >= ROW(2, NULL::integer)) AND (a = 2))
+(3 rows)
+
+select count(*) from btree_rowcompare_tab where a = 2 and (a, b) >= (2, null);
+ count 
+-------
+     0
+(1 row)
+
+-- RowCompare's row omits the index's second column, so on pages whose "b"
+-- values change _bt_set_startikey can't prove that every tuple satisfies the
+-- RowCompare.
+explain (costs off)
+select count(*) from btree_rowcompare_tab where (a, c) >= (2, 100);
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using btree_rowcompare_idx on btree_rowcompare_tab
+         Index Cond: (ROW(a, c) >= ROW(2, 100))
+(3 rows)
+
+select count(*) from btree_rowcompare_tab where (a, c) >= (2, 100);
+ count 
+-------
+   201
+(1 row)
+
+-- Variant that uses the remaining inequality strategies
+explain (costs off)
+select count(*) from btree_rowcompare_tab where (a, b) < (2, 10);
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using btree_rowcompare_idx on btree_rowcompare_tab
+         Index Cond: (ROW(a, b) < ROW(2, 10))
+(3 rows)
+
+select count(*) from btree_rowcompare_tab where (a, b) < (2, 10);
+ count 
+-------
+   159
+(1 row)
+
+drop table btree_rowcompare_tab;
+--
+-- Test SAOP array handling within _bt_set_startikey
+--
+create temp table btree_saop_tab (a int, b int, c int);
+insert into btree_saop_tab
+  select a, b, b from generate_series(1, 3) a, generate_series(1, 150) b;
+insert into btree_saop_tab
+  select 2, 0, 7 from generate_series(1, 60);
+create index btree_saop_idx on btree_saop_tab (a, b desc, c)
+  with (fillfactor = 10, deduplicate_items = off);
+vacuum analyze btree_saop_tab;
+-- SAOP on the leading column: pages beyond each primitive scan's first page
+-- have a single "a" value that a binary search finds in the array, so the
+-- scan starts past the SAOP key (forcing the nonrequired key protocol)
+explain (costs off)
+select count(*) from btree_saop_tab where a in (1, 3) and b >= 100;
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using btree_saop_idx on btree_saop_tab
+         Index Cond: ((a = ANY ('{1,3}'::integer[])) AND (b >= 100))
+(3 rows)
+
+select count(*) from btree_saop_tab where a in (1, 3) and b >= 100;
+ count 
+-------
+   102
+(1 row)
+
+-- Skip array on "b" precedes the "c" SAOP; pages whose "b" values change
+-- prevent starting past the "c" SAOP key
+explain (costs off)
+select count(*) from btree_saop_tab where a = 2 and c in (101, 105);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using btree_saop_idx on btree_saop_tab
+         Index Cond: ((a = 2) AND (c = ANY ('{101,105}'::integer[])))
+(3 rows)
+
+select count(*) from btree_saop_tab where a = 2 and c in (101, 105);
+ count 
+-------
+     2
+(1 row)
+
+-- "c" SAOP follows the "b" inequality; on pages that lie wholly within the
+-- duplicate "(2, 0, 7)" run, the scan starts past all of its scan keys,
+-- including the SAOP key
+explain (costs off)
+select count(*) from btree_saop_tab where a = 2 and b < 1 and c in (6, 7);
+                                  QUERY PLAN                                  
+------------------------------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using btree_saop_idx on btree_saop_tab
+         Index Cond: ((a = 2) AND (b < 1) AND (c = ANY ('{6,7}'::integer[])))
+(3 rows)
+
+select count(*) from btree_saop_tab where a = 2 and b < 1 and c in (6, 7);
+ count 
+-------
+    60
+(1 row)
+
+reset enable_seqscan;
+reset enable_bitmapscan;
+drop table btree_saop_tab;
 --
 -- Performs a recheck of > key following array advancement on previous (left
 -- sibling) page that used a high key whose attribute value corresponding to
index 6aaaa386abcec3bbe3b42711a833f40f358c47f3..a08bb101c20205e89b5c6d30fb17d0a08f529f5f 100644 (file)
@@ -216,6 +216,91 @@ SELECT proname, proargtypes, pronamespace
    AND pronamespace IN (1, 2, 3) AND proargtypes IN ('26 23', '5077')
 ORDER BY proname, proargtypes, pronamespace;
 
+--
+-- Test RowCompare handling within _bt_set_startikey, which decides whether
+-- every tuple on a page (a page beyond the scan's first) must satisfy the
+-- scan's RowCompare qual.
+--
+-- The index mixes an ASC column with a DESC column (so RowCompare members
+-- don't all use the same inequality strategy and are not marked required),
+-- uses a low fillfactor (so scans read several pages), and disables
+-- deduplication (so the "b" NULLs span more than one page).
+create temp table btree_rowcompare_tab (a int, b int, c int);
+insert into btree_rowcompare_tab
+  select a, b, b from generate_series(1, 3) a, generate_series(1, 150) b;
+insert into btree_rowcompare_tab
+  select 2, null, null from generate_series(1, 50);
+create index btree_rowcompare_idx on btree_rowcompare_tab (a, b desc, c)
+  with (fillfactor = 10, deduplicate_items = off);
+vacuum analyze btree_rowcompare_tab;
+
+set enable_seqscan to false;
+set enable_bitmapscan to false;
+
+-- RowCompare satisfied by every tuple on many pages (decided by its first
+-- member on "a = 3" pages, and by its final member on "a = 2" pages)
+explain (costs off)
+select count(*) from btree_rowcompare_tab where (a, b) >= (2, 75);
+select count(*) from btree_rowcompare_tab where (a, b) >= (2, 75);
+
+-- Reaches the RowCompare's unsatisfiable NULL member argument on "a = 2"
+-- pages (the "a = 2" key positions the scan within the "a = 2" group, which
+-- the RowCompare qual alone would not).  The combined quals are
+-- contradictory, but preprocessing cannot detect that.
+explain (costs off)
+select count(*) from btree_rowcompare_tab where a = 2 and (a, b) >= (2, null);
+select count(*) from btree_rowcompare_tab where a = 2 and (a, b) >= (2, null);
+
+-- RowCompare's row omits the index's second column, so on pages whose "b"
+-- values change _bt_set_startikey can't prove that every tuple satisfies the
+-- RowCompare.
+explain (costs off)
+select count(*) from btree_rowcompare_tab where (a, c) >= (2, 100);
+select count(*) from btree_rowcompare_tab where (a, c) >= (2, 100);
+
+-- Variant that uses the remaining inequality strategies
+explain (costs off)
+select count(*) from btree_rowcompare_tab where (a, b) < (2, 10);
+select count(*) from btree_rowcompare_tab where (a, b) < (2, 10);
+
+drop table btree_rowcompare_tab;
+
+--
+-- Test SAOP array handling within _bt_set_startikey
+--
+create temp table btree_saop_tab (a int, b int, c int);
+insert into btree_saop_tab
+  select a, b, b from generate_series(1, 3) a, generate_series(1, 150) b;
+insert into btree_saop_tab
+  select 2, 0, 7 from generate_series(1, 60);
+create index btree_saop_idx on btree_saop_tab (a, b desc, c)
+  with (fillfactor = 10, deduplicate_items = off);
+vacuum analyze btree_saop_tab;
+
+-- SAOP on the leading column: pages beyond each primitive scan's first page
+-- have a single "a" value that a binary search finds in the array, so the
+-- scan starts past the SAOP key (forcing the nonrequired key protocol)
+explain (costs off)
+select count(*) from btree_saop_tab where a in (1, 3) and b >= 100;
+select count(*) from btree_saop_tab where a in (1, 3) and b >= 100;
+
+-- Skip array on "b" precedes the "c" SAOP; pages whose "b" values change
+-- prevent starting past the "c" SAOP key
+explain (costs off)
+select count(*) from btree_saop_tab where a = 2 and c in (101, 105);
+select count(*) from btree_saop_tab where a = 2 and c in (101, 105);
+
+-- "c" SAOP follows the "b" inequality; on pages that lie wholly within the
+-- duplicate "(2, 0, 7)" run, the scan starts past all of its scan keys,
+-- including the SAOP key
+explain (costs off)
+select count(*) from btree_saop_tab where a = 2 and b < 1 and c in (6, 7);
+select count(*) from btree_saop_tab where a = 2 and b < 1 and c in (6, 7);
+
+reset enable_seqscan;
+reset enable_bitmapscan;
+drop table btree_saop_tab;
+
 --
 -- Performs a recheck of > key following array advancement on previous (left
 -- sibling) page that used a high key whose attribute value corresponding to