From 2caffa42c8520408d3a40973bb794feece757ccf Mon Sep 17 00:00:00 2001 From: Krishna Sarabu Date: Thu, 5 Nov 2026 22:37:14 -0500 Subject: [PATCH] Avoid redundant BitmapAnd with a clauseless partial index choose_bitmap_and() rejects a partial index whose predicate is implied by the quals already chosen, but not the reverse case: a clauseless partial index scan leading the AND group, followed by an index whose quals imply its predicate. The BitmapAnd then can't remove any rows, but the predicate's selectivity gets counted twice, so it can look cheap enough to be chosen over the other index alone. Add the mirror-image check. It requires the whole predicate to be implied and only looks at plain IndexPaths, since find_indexpath_quals() flattens BitmapOr arms into a single list. --- src/backend/optimizer/path/indxpath.c | 20 ++++ src/test/regress/expected/create_index.out | 119 +++++++++++++++++++++ src/test/regress/sql/create_index.sql | 75 +++++++++++++ 3 files changed, 214 insertions(+) diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c index 3f5d4fa3182..3f2f9cb375a 100644 --- a/src/backend/optimizer/path/indxpath.c +++ b/src/backend/optimizer/path/indxpath.c @@ -1949,6 +1949,26 @@ choose_bitmap_and(PlannerInfo *root, RelOptInfo *rel, List *paths) if (redundant) continue; } + + /* + * Conversely, if the leader is a clauseless scan of a partial + * index and this path's quals imply its whole predicate, the + * partial index can't exclude anything this path returns, and + * ANDing them would just double-count the predicate's + * selectivity. Skip this path; it still gets considered as a + * leader itself. Only plain IndexPaths are checked, since + * find_indexpath_quals() flattens BitmapOr arms into a list that + * would be read as an AND. + */ + if (IsA(pathinfoarray[i]->path, IndexPath) && + pathinfoarray[i]->quals == NIL && + pathinfoarray[i]->preds != NIL && + IsA(pathinfo->path, IndexPath) && + pathinfo->quals != NIL && + predicate_implied_by(pathinfoarray[i]->preds, pathinfo->quals, + false)) + continue; + /* tentatively add new path to paths, so we can estimate cost */ paths = lappend(paths, pathinfo->path); newcost = bitmap_and_cost_est(root, rel, paths); diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out index e914c809b82..56c4c84e51b 100644 --- a/src/test/regress/expected/create_index.out +++ b/src/test/regress/expected/create_index.out @@ -2274,6 +2274,125 @@ SELECT count(*) FROM tenk1 LEFT JOIN tenk2 ON Filter: ((thousand = 42) OR (thousand = 41) OR (tenthous = 2)) (10 rows) +-- A clauseless partial index scan shouldn't be ANDed with an index whose +-- quals imply its predicate. +CREATE TABLE bitmap_implied_partial + (id int, state text, depot_id int, customer_id int, pad text); +INSERT INTO bitmap_implied_partial + SELECT g, + CASE WHEN g % 2000 = 0 THEN 'PENDING' + WHEN g % 200 = 0 THEN 'PACKING' + WHEN g % 500 = 0 THEN 'ON_HOLD' + WHEN g % 10 = 0 THEN 'VOIDED' + ELSE 'SHIPPED' END, + CASE WHEN g % 500 = 0 THEN NULL ELSE (g % 300) + 1 END, + g % 50, + repeat('x', 200) + FROM generate_series(1, 10000) g; +CREATE INDEX bip_partial ON bitmap_implied_partial (id) + WHERE state = 'PENDING' OR state = 'PACKING' OR state = 'ON_HOLD'; +CREATE INDEX bip_state_depot ON bitmap_implied_partial (state, depot_id); +CREATE INDEX bip_customer ON bitmap_implied_partial (customer_id); +ANALYZE bitmap_implied_partial; +SET enable_seqscan = OFF; +SET enable_indexscan = OFF; +-- The composite index's quals imply the partial predicate: no BitmapAnd. +EXPLAIN (COSTS OFF) +SELECT * FROM bitmap_implied_partial + WHERE state IN ('PENDING', 'PACKING') AND depot_id IS NOT NULL; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Bitmap Heap Scan on bitmap_implied_partial + Recheck Cond: ((state = ANY ('{PENDING,PACKING}'::text[])) AND (depot_id IS NOT NULL)) + -> Bitmap Index Scan on bip_state_depot + Index Cond: ((state = ANY ('{PENDING,PACKING}'::text[])) AND (depot_id IS NOT NULL)) +(4 rows) + +-- The partial index has a qual of its own here, so it is still ANDed. +EXPLAIN (COSTS OFF) +SELECT * FROM bitmap_implied_partial + WHERE state IN ('PENDING', 'PACKING') AND id BETWEEN 1 AND 4000 + AND depot_id IS NOT NULL; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Bitmap Heap Scan on bitmap_implied_partial + Recheck Cond: ((id >= 1) AND (id <= 4000) AND ((state = 'PENDING'::text) OR (state = 'PACKING'::text) OR (state = 'ON_HOLD'::text)) AND (state = ANY ('{PENDING,PACKING}'::text[])) AND (depot_id IS NOT NULL)) + -> BitmapAnd + -> Bitmap Index Scan on bip_partial + Index Cond: ((id >= 1) AND (id <= 4000)) + -> Bitmap Index Scan on bip_state_depot + Index Cond: ((state = ANY ('{PENDING,PACKING}'::text[])) AND (depot_id IS NOT NULL)) +(7 rows) + +-- Three indexes: customer_id = 0 doesn't imply the predicate, so it is still +-- ANDed with the partial index; with customer_id < 25 the composite is used. +EXPLAIN (COSTS OFF) +SELECT * FROM bitmap_implied_partial + WHERE state IN ('PENDING', 'PACKING') AND depot_id IS NOT NULL + AND customer_id = 0; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------- + Bitmap Heap Scan on bitmap_implied_partial + Recheck Cond: (((state = 'PENDING'::text) OR (state = 'PACKING'::text) OR (state = 'ON_HOLD'::text)) AND (customer_id = 0)) + Filter: ((depot_id IS NOT NULL) AND (state = ANY ('{PENDING,PACKING}'::text[]))) + -> BitmapAnd + -> Bitmap Index Scan on bip_partial + -> Bitmap Index Scan on bip_customer + Index Cond: (customer_id = 0) +(7 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM bitmap_implied_partial + WHERE state IN ('PENDING', 'PACKING') AND depot_id IS NOT NULL + AND customer_id < 25; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Bitmap Heap Scan on bitmap_implied_partial + Recheck Cond: ((state = ANY ('{PENDING,PACKING}'::text[])) AND (depot_id IS NOT NULL)) + Filter: (customer_id < 25) + -> Bitmap Index Scan on bip_state_depot + Index Cond: ((state = ANY ('{PENDING,PACKING}'::text[])) AND (depot_id IS NOT NULL)) +(5 rows) + +DROP TABLE bitmap_implied_partial; +-- A BitmapOr isn't checked: "state = 'PENDING' OR customer_id = 5" doesn't +-- imply the predicate, so the partial index still helps here. +CREATE TABLE bitmap_implied_partial_or + (id int, state text, customer_id int, pad text); +INSERT INTO bitmap_implied_partial_or + SELECT g, + CASE WHEN g % 1000 = 0 THEN 'PENDING' + WHEN g % 100 = 0 THEN 'PACKING' + WHEN g % 250 = 0 THEN 'ON_HOLD' + ELSE 'SHIPPED' END, + g % 50, repeat('x', 100) + FROM generate_series(1, 10000) g; +CREATE INDEX bipo_partial ON bitmap_implied_partial_or (id) + WHERE state = 'PENDING' OR state = 'PACKING' OR state = 'ON_HOLD'; +CREATE INDEX bipo_customer ON bitmap_implied_partial_or (customer_id); +CREATE INDEX bipo_state ON bitmap_implied_partial_or (state); +ANALYZE bitmap_implied_partial_or; +EXPLAIN (COSTS OFF) +SELECT * FROM bitmap_implied_partial_or + WHERE state IN ('PENDING', 'PACKING') + AND (state = 'PENDING' OR customer_id = 5); + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Bitmap Heap Scan on bitmap_implied_partial_or + Recheck Cond: (((state = 'PENDING'::text) OR (state = 'PACKING'::text) OR (state = 'ON_HOLD'::text)) AND (((state = 'PENDING'::text) AND (state = ANY ('{PENDING,PACKING}'::text[]))) OR (customer_id = 5))) + Filter: (state = ANY ('{PENDING,PACKING}'::text[])) + -> BitmapAnd + -> Bitmap Index Scan on bipo_partial + -> BitmapOr + -> Bitmap Index Scan on bipo_state + Index Cond: ((state = 'PENDING'::text) AND (state = ANY ('{PENDING,PACKING}'::text[]))) + -> Bitmap Index Scan on bipo_customer + Index Cond: (customer_id = 5) +(10 rows) + +RESET enable_seqscan; +RESET enable_indexscan; +DROP TABLE bitmap_implied_partial_or; -- -- Check behavior with duplicate index column contents -- diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql index e4b8cee1e1c..9083dd59275 100644 --- a/src/test/regress/sql/create_index.sql +++ b/src/test/regress/sql/create_index.sql @@ -850,6 +850,81 @@ EXPLAIN (COSTS OFF) SELECT count(*) FROM tenk1 LEFT JOIN tenk2 ON tenk1.hundred = 42 AND (tenk2.thousand = 42 OR tenk2.thousand = 41 OR tenk2.tenthous = 2) AND tenk2.hundred = tenk1.hundred; + +-- A clauseless partial index scan shouldn't be ANDed with an index whose +-- quals imply its predicate. +CREATE TABLE bitmap_implied_partial + (id int, state text, depot_id int, customer_id int, pad text); +INSERT INTO bitmap_implied_partial + SELECT g, + CASE WHEN g % 2000 = 0 THEN 'PENDING' + WHEN g % 200 = 0 THEN 'PACKING' + WHEN g % 500 = 0 THEN 'ON_HOLD' + WHEN g % 10 = 0 THEN 'VOIDED' + ELSE 'SHIPPED' END, + CASE WHEN g % 500 = 0 THEN NULL ELSE (g % 300) + 1 END, + g % 50, + repeat('x', 200) + FROM generate_series(1, 10000) g; +CREATE INDEX bip_partial ON bitmap_implied_partial (id) + WHERE state = 'PENDING' OR state = 'PACKING' OR state = 'ON_HOLD'; +CREATE INDEX bip_state_depot ON bitmap_implied_partial (state, depot_id); +CREATE INDEX bip_customer ON bitmap_implied_partial (customer_id); +ANALYZE bitmap_implied_partial; + +SET enable_seqscan = OFF; +SET enable_indexscan = OFF; + +-- The composite index's quals imply the partial predicate: no BitmapAnd. +EXPLAIN (COSTS OFF) +SELECT * FROM bitmap_implied_partial + WHERE state IN ('PENDING', 'PACKING') AND depot_id IS NOT NULL; + +-- The partial index has a qual of its own here, so it is still ANDed. +EXPLAIN (COSTS OFF) +SELECT * FROM bitmap_implied_partial + WHERE state IN ('PENDING', 'PACKING') AND id BETWEEN 1 AND 4000 + AND depot_id IS NOT NULL; + +-- Three indexes: customer_id = 0 doesn't imply the predicate, so it is still +-- ANDed with the partial index; with customer_id < 25 the composite is used. +EXPLAIN (COSTS OFF) +SELECT * FROM bitmap_implied_partial + WHERE state IN ('PENDING', 'PACKING') AND depot_id IS NOT NULL + AND customer_id = 0; +EXPLAIN (COSTS OFF) +SELECT * FROM bitmap_implied_partial + WHERE state IN ('PENDING', 'PACKING') AND depot_id IS NOT NULL + AND customer_id < 25; + +DROP TABLE bitmap_implied_partial; + +-- A BitmapOr isn't checked: "state = 'PENDING' OR customer_id = 5" doesn't +-- imply the predicate, so the partial index still helps here. +CREATE TABLE bitmap_implied_partial_or + (id int, state text, customer_id int, pad text); +INSERT INTO bitmap_implied_partial_or + SELECT g, + CASE WHEN g % 1000 = 0 THEN 'PENDING' + WHEN g % 100 = 0 THEN 'PACKING' + WHEN g % 250 = 0 THEN 'ON_HOLD' + ELSE 'SHIPPED' END, + g % 50, repeat('x', 100) + FROM generate_series(1, 10000) g; +CREATE INDEX bipo_partial ON bitmap_implied_partial_or (id) + WHERE state = 'PENDING' OR state = 'PACKING' OR state = 'ON_HOLD'; +CREATE INDEX bipo_customer ON bitmap_implied_partial_or (customer_id); +CREATE INDEX bipo_state ON bitmap_implied_partial_or (state); +ANALYZE bitmap_implied_partial_or; + +EXPLAIN (COSTS OFF) +SELECT * FROM bitmap_implied_partial_or + WHERE state IN ('PENDING', 'PACKING') + AND (state = 'PENDING' OR customer_id = 5); + +RESET enable_seqscan; +RESET enable_indexscan; +DROP TABLE bitmap_implied_partial_or; -- -- Check behavior with duplicate index column contents --