]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix LIKE/regex optimization for indexscan with exact-match pattern.
authorTom Lane <tgl@sss.pgh.pa.us>
Mon, 6 Jul 2026 17:06:21 +0000 (13:06 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Mon, 6 Jul 2026 17:06:25 +0000 (13:06 -0400)
Commit 85b7efa1c introduced support for LIKE with non-deterministic
collations.  By moving some conditionals around, it accidentally broke
the optimization for converting a LIKE or regex exact-match pattern
to an equality indexqual when the index collation doesn't match the
expression collation.  That should be allowed if the expression
collation is deterministic.  This patch re-introduces the optimization
for that common case.

One important beneficiary of this optimization is the "\d tablename"
command in psql.  Without this fix that will do a seqscan on pg_class
instead of an index point lookup.

Reported-by: Andres Freund <andres@anarazel.de>
Author: Jelte Fennema-Nio <postgres@jeltef.nl>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://postgr.es/m/DHBQIZX8SZVI.ZX614ZMFL645@jeltef.nl
Backpatch-through: 18

src/backend/utils/adt/like_support.c
src/test/regress/expected/collate.icu.utf8.out
src/test/regress/expected/collate.out
src/test/regress/sql/collate.icu.utf8.sql
src/test/regress/sql/collate.sql

index 8fdc677371f4d32d0088579c18c45108e277bff0..eb1dcc12dfe2c83b3401972d8934b68560ebf51d 100644 (file)
@@ -69,6 +69,10 @@ typedef enum
        Pattern_Prefix_None, Pattern_Prefix_Partial, Pattern_Prefix_Exact,
 } Pattern_Prefix_Status;
 
+/* non-collatable comparisons, eg for bytea, are always deterministic */
+#define NONDETERMINISTIC(coll) \
+       (OidIsValid(coll) && !get_collation_isdeterministic(coll))
+
 static Node *like_regex_support(Node *rawreq, Pattern_Type ptype);
 static List *match_pattern_prefix(Node *leftop,
                                                                  Node *rightop,
@@ -383,12 +387,22 @@ match_pattern_prefix(Node *leftop,
         * us to not be concerned with specific opclasses (except for the legacy
         * "pattern" cases); any index that correctly implements the operators
         * will work.
+        *
+        * This case will work for LIKE/regex expressions with nondeterministic
+        * collation, so long as the index's collation is the same.  If the
+        * expression's collation is deterministic, we can even use an index whose
+        * collation differs from the expression's.  All deterministic collations
+        * agree on equality (it's bitwise), while we assume that an index with
+        * nondeterministic collation will return a superset of the bitwise-equal
+        * entries.  Since the "=" indexqual is marked as lossy by default, we'll
+        * apply the LIKE/regex operator as a recheck, and that will filter out
+        * any non-matching entries.
         */
        if (pstatus == Pattern_Prefix_Exact)
        {
                if (!op_in_opfamily(eqopr, opfamily))
                        return NIL;
-               if (indexcollation != expr_coll)
+               if (indexcollation != expr_coll && NONDETERMINISTIC(expr_coll))
                        return NIL;
                expr = make_opclause(eqopr, BOOLOID, false,
                                                         (Expr *) leftop, (Expr *) prefix,
@@ -402,10 +416,8 @@ match_pattern_prefix(Node *leftop,
         * expression collation is nondeterministic.  The optimized equality or
         * prefix tests use bytewise comparisons, which is not consistent with
         * nondeterministic collations.
-        *
-        * expr_coll is not set for a non-collation-aware data type such as bytea.
         */
-       if (expr_coll && !get_collation_isdeterministic(expr_coll))
+       if (NONDETERMINISTIC(expr_coll))
                return NIL;
 
        /*
index ba24728247661b75df9fdb88f55b8e08cb878dca..f8e6531375206e32f11f0d93213eec1ee5a4c586 100644 (file)
@@ -2074,6 +2074,29 @@ SELECT string_to_array('ABCDEFGHI' COLLATE case_insensitive, NULL, 'b');
  {A,NULL,C,D,E,F,G,H,I}
 (1 row)
 
+-- These queries should be able to use the index on test1ci.x:
+SET enable_seqscan = off;
+SET enable_indexonlyscan = off;
+EXPLAIN (COSTS OFF)
+SELECT * FROM test1ci WHERE x ~ '^abc$' COLLATE "C";
+                QUERY PLAN                 
+-------------------------------------------
+ Index Scan using test1ci_x_idx on test1ci
+   Index Cond: (x = 'abc'::text)
+   Filter: (x ~ '^abc$'::text COLLATE "C")
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM test1ci WHERE x LIKE 'abc' COLLATE case_insensitive;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Index Scan using test1ci_x_idx on test1ci
+   Index Cond: (x = 'abc'::text)
+   Filter: (x ~~ 'abc'::text COLLATE case_insensitive)
+(3 rows)
+
+RESET enable_seqscan;
+RESET enable_indexonlyscan;
 -- Test HAVING-to-WHERE pushdown with nondeterministic collations.
 -- When a HAVING clause uses a different collation than the GROUP BY's
 -- nondeterministic collation, it must not be pushed to WHERE, otherwise
index bf72908fbd39b2e67415dfeb7551263b4179079b..a57c865bb0f2d4c2b57a2fcea10a9e516acf5503 100644 (file)
@@ -766,6 +766,29 @@ DETAIL:  LOCALE cannot be specified together with LC_COLLATE or LC_CTYPE.
 CREATE COLLATION coll_dup_chk (FROM = "C", VERSION = "1");
 ERROR:  conflicting or redundant options
 DETAIL:  FROM cannot be specified together with any other options.
+-- Regex exact-match optimization should use an index even when the expression
+-- and index have different collations, so long as the expression's collation
+-- is deterministic.  This example tests what we want because the optimizer
+-- does not perceive "C" collation (used by the system catalogs) as identical
+-- to "POSIX" collation.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pg_class WHERE relname ~ '^pg_class$' COLLATE "POSIX";
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Index Scan using pg_class_relname_nsp_index on pg_class
+   Index Cond: (relname = 'pg_class'::text)
+   Filter: (relname ~ '^pg_class$'::text COLLATE "POSIX")
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pg_class WHERE relname LIKE 'pg\_class' COLLATE "POSIX";
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Index Scan using pg_class_relname_nsp_index on pg_class
+   Index Cond: (relname = 'pg_class'::text)
+   Filter: (relname ~~ 'pg\_class'::text COLLATE "POSIX")
+(3 rows)
+
 --
 -- Clean up.  Many of these table names will be re-used if the user is
 -- trying to run any platform-specific collation tests later, so we
index 4e5ef47016c294ca78cf5c0b53d44eb2bf422eff..f4c4271fb34506138b8e2055aa46ab5700dffa60 100644 (file)
@@ -741,6 +741,16 @@ CREATE UNIQUE INDEX ON test3ci (x);  -- error
 SELECT string_to_array('ABC,DEF,GHI' COLLATE case_insensitive, ',', 'abc');
 SELECT string_to_array('ABCDEFGHI' COLLATE case_insensitive, NULL, 'b');
 
+-- These queries should be able to use the index on test1ci.x:
+SET enable_seqscan = off;
+SET enable_indexonlyscan = off;
+EXPLAIN (COSTS OFF)
+SELECT * FROM test1ci WHERE x ~ '^abc$' COLLATE "C";
+EXPLAIN (COSTS OFF)
+SELECT * FROM test1ci WHERE x LIKE 'abc' COLLATE case_insensitive;
+RESET enable_seqscan;
+RESET enable_indexonlyscan;
+
 -- Test HAVING-to-WHERE pushdown with nondeterministic collations.
 -- When a HAVING clause uses a different collation than the GROUP BY's
 -- nondeterministic collation, it must not be pushed to WHERE, otherwise
index 4b0e4472c3f6b35356da89485fcfbbd4d584eea9..b018da13f24a10e310c2633553a8e9189acba1b0 100644 (file)
@@ -302,6 +302,16 @@ CREATE COLLATION coll_dup_chk (LC_CTYPE = "POSIX", LOCALE = '');
 -- FROM conflicts with any other option
 CREATE COLLATION coll_dup_chk (FROM = "C", VERSION = "1");
 
+-- Regex exact-match optimization should use an index even when the expression
+-- and index have different collations, so long as the expression's collation
+-- is deterministic.  This example tests what we want because the optimizer
+-- does not perceive "C" collation (used by the system catalogs) as identical
+-- to "POSIX" collation.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pg_class WHERE relname ~ '^pg_class$' COLLATE "POSIX";
+EXPLAIN (COSTS OFF)
+SELECT * FROM pg_class WHERE relname LIKE 'pg\_class' COLLATE "POSIX";
+
 --
 -- Clean up.  Many of these table names will be re-used if the user is
 -- trying to run any platform-specific collation tests later, so we