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
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,
* 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,
* 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;
/*
{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
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
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
-- 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