From 0a4ff9e4b77591bfd7bf084897e5d1d302139bf4 Mon Sep 17 00:00:00 2001 From: Vaibhav Dalvi Date: Wed, 2 Sep 2026 10:41:59 +0000 Subject: [PATCH] Add gist_trgm_ops_noeq, a gist_trgm_ops without '=' gist_trgm_ops's '=' support can lead the planner to pick a GiST index over a much cheaper btree index for a plain equality lookup, because the generic, selectivity-based cost estimate doesn't reflect how that operator is actually checked (a superset-of-trigrams test that can be a false positive regardless of how selective the value is). Rather than changing gist_trgm_ops itself, or the planner's costing, this adds a second, independently-named opclass that is otherwise identical to gist_trgm_ops, minus operator 11 ('='). Existing gist_trgm_ops indexes are completely unaffected. Anyone who wants the extra safety can create new indexes with gist_trgm_ops_noeq, or rebuild an existing index onto it, and the planner will then simply never have '=' available on that index at all. No core or C code changes; this is pure SQL/DDL, so there's no new cost-model behavior to reason about or get wrong. Tested that '=' cannot use the new opclass under any planner settings, and that the other operators still return correct results through it. A new regression test covers both. Discussion: https://postgr.es/m/CAOBaU_YWwtT7tdggtROacjdOdeYHCz-tmSwuC-j-TOG-g97J0w@mail.gmail.com --- contrib/pg_trgm/Makefile | 8 ++-- contrib/pg_trgm/expected/pg_trgm_noeq.out | 53 +++++++++++++++++++++++ contrib/pg_trgm/pg_trgm--1.6--1.7.sql | 31 +++++++++++++ contrib/pg_trgm/pg_trgm.control | 2 +- contrib/pg_trgm/sql/pg_trgm_noeq.sql | 30 +++++++++++++ doc/src/sgml/pgtrgm.sgml | 8 ++++ 6 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 contrib/pg_trgm/expected/pg_trgm_noeq.out create mode 100644 contrib/pg_trgm/pg_trgm--1.6--1.7.sql create mode 100644 contrib/pg_trgm/sql/pg_trgm_noeq.sql diff --git a/contrib/pg_trgm/Makefile b/contrib/pg_trgm/Makefile index c1756993ec7..cd19ea5c396 100644 --- a/contrib/pg_trgm/Makefile +++ b/contrib/pg_trgm/Makefile @@ -9,12 +9,12 @@ OBJS = \ trgm_regexp.o EXTENSION = pg_trgm -DATA = pg_trgm--1.5--1.6.sql pg_trgm--1.4--1.5.sql pg_trgm--1.3--1.4.sql \ - pg_trgm--1.3.sql pg_trgm--1.2--1.3.sql pg_trgm--1.1--1.2.sql \ - pg_trgm--1.0--1.1.sql +DATA = pg_trgm--1.6--1.7.sql pg_trgm--1.5--1.6.sql pg_trgm--1.4--1.5.sql \ + pg_trgm--1.3--1.4.sql pg_trgm--1.3.sql pg_trgm--1.2--1.3.sql \ + pg_trgm--1.1--1.2.sql pg_trgm--1.0--1.1.sql PGFILEDESC = "pg_trgm - trigram matching" -REGRESS = pg_trgm pg_utf8_trgm pg_word_trgm pg_strict_word_trgm +REGRESS = pg_trgm pg_utf8_trgm pg_word_trgm pg_strict_word_trgm pg_trgm_noeq ifdef USE_PGXS PG_CONFIG = pg_config diff --git a/contrib/pg_trgm/expected/pg_trgm_noeq.out b/contrib/pg_trgm/expected/pg_trgm_noeq.out new file mode 100644 index 00000000000..89072ea4721 --- /dev/null +++ b/contrib/pg_trgm/expected/pg_trgm_noeq.out @@ -0,0 +1,53 @@ +-- Tests for the gist_trgm_ops_noeq operator class. +-- Should validate cleanly, same as any other opclass. +SELECT amname, opcname +FROM pg_opclass opc LEFT JOIN pg_am am ON am.oid = opcmethod +WHERE opcname = 'gist_trgm_ops_noeq' AND NOT amvalidate(opc.oid); + amname | opcname +--------+--------- +(0 rows) + +create table noeq_test (a text); +insert into noeq_test select md5(g::text) from generate_series(1, 100) g; +create index noeq_test_gist on noeq_test using gist (a gist_trgm_ops_noeq); +-- The opclass doesn't offer '=' at all, so it must never be used for it, +-- even with every alternative disabled. +set enable_seqscan = off; +set enable_bitmapscan = off; +set enable_indexonlyscan = off; +explain (costs off) + select * from noeq_test where a = '1234'; + QUERY PLAN +------------------------------ + Seq Scan on noeq_test + Disabled: true + Filter: (a = '1234'::text) +(3 rows) + +-- Other operators still work, and give correct results. +reset enable_seqscan; +reset enable_bitmapscan; +select count(*) from noeq_test where a like '%ab%'; + count +------- + 16 +(1 row) + +set enable_seqscan = off; +explain (costs off) + select count(*) from noeq_test where a like '%ab%'; + QUERY PLAN +---------------------------------------------------- + Aggregate + -> Index Scan using noeq_test_gist on noeq_test + Index Cond: (a ~~ '%ab%'::text) +(3 rows) + +select count(*) from noeq_test where a like '%ab%'; + count +------- + 16 +(1 row) + +reset enable_seqscan; +drop table noeq_test; diff --git a/contrib/pg_trgm/pg_trgm--1.6--1.7.sql b/contrib/pg_trgm/pg_trgm--1.6--1.7.sql new file mode 100644 index 00000000000..7cbe2a1ae17 --- /dev/null +++ b/contrib/pg_trgm/pg_trgm--1.6--1.7.sql @@ -0,0 +1,31 @@ +/* contrib/pg_trgm/pg_trgm--1.6--1.7.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION pg_trgm UPDATE TO '1.7'" to load this file. \quit + +-- A GiST opclass identical to gist_trgm_ops, minus the '=' operator. +CREATE OPERATOR FAMILY gist_trgm_ops_noeq USING gist; + +CREATE OPERATOR CLASS gist_trgm_ops_noeq +FOR TYPE text USING gist FAMILY gist_trgm_ops_noeq AS + OPERATOR 1 % (text, text), + OPERATOR 2 <-> (text, text) FOR ORDER BY pg_catalog.float_ops, + OPERATOR 3 pg_catalog.~~ (text, text), + OPERATOR 4 pg_catalog.~~* (text, text), + OPERATOR 5 pg_catalog.~ (text, text), + OPERATOR 6 pg_catalog.~* (text, text), + OPERATOR 7 %> (text, text), + OPERATOR 8 <->> (text, text) FOR ORDER BY pg_catalog.float_ops, + OPERATOR 9 %>> (text, text), + OPERATOR 10 <->>> (text, text) FOR ORDER BY pg_catalog.float_ops, + -- deliberately no OPERATOR 11 ( pg_catalog.= ) + FUNCTION 1 gtrgm_consistent (internal, text, smallint, oid, internal), + FUNCTION 2 gtrgm_union (internal, internal), + FUNCTION 3 gtrgm_compress (internal), + FUNCTION 4 gtrgm_decompress (internal), + FUNCTION 5 gtrgm_penalty (internal, internal, internal), + FUNCTION 6 gtrgm_picksplit (internal, internal), + FUNCTION 7 gtrgm_same (gtrgm, gtrgm, internal), + FUNCTION 8 gtrgm_distance (internal, text, smallint, oid, internal), + FUNCTION 10 gtrgm_options (internal), + STORAGE gtrgm; diff --git a/contrib/pg_trgm/pg_trgm.control b/contrib/pg_trgm/pg_trgm.control index 1d6a9ddf259..6e3ee43c510 100644 --- a/contrib/pg_trgm/pg_trgm.control +++ b/contrib/pg_trgm/pg_trgm.control @@ -1,6 +1,6 @@ # pg_trgm extension comment = 'text similarity measurement and index searching based on trigrams' -default_version = '1.6' +default_version = '1.7' module_pathname = '$libdir/pg_trgm' relocatable = true trusted = true diff --git a/contrib/pg_trgm/sql/pg_trgm_noeq.sql b/contrib/pg_trgm/sql/pg_trgm_noeq.sql new file mode 100644 index 00000000000..9bd777cacc3 --- /dev/null +++ b/contrib/pg_trgm/sql/pg_trgm_noeq.sql @@ -0,0 +1,30 @@ +-- Tests for the gist_trgm_ops_noeq operator class. + +-- Should validate cleanly, same as any other opclass. +SELECT amname, opcname +FROM pg_opclass opc LEFT JOIN pg_am am ON am.oid = opcmethod +WHERE opcname = 'gist_trgm_ops_noeq' AND NOT amvalidate(opc.oid); + +create table noeq_test (a text); +insert into noeq_test select md5(g::text) from generate_series(1, 100) g; +create index noeq_test_gist on noeq_test using gist (a gist_trgm_ops_noeq); + +-- The opclass doesn't offer '=' at all, so it must never be used for it, +-- even with every alternative disabled. +set enable_seqscan = off; +set enable_bitmapscan = off; +set enable_indexonlyscan = off; +explain (costs off) + select * from noeq_test where a = '1234'; + +-- Other operators still work, and give correct results. +reset enable_seqscan; +reset enable_bitmapscan; +select count(*) from noeq_test where a like '%ab%'; +set enable_seqscan = off; +explain (costs off) + select count(*) from noeq_test where a like '%ab%'; +select count(*) from noeq_test where a like '%ab%'; +reset enable_seqscan; + +drop table noeq_test; diff --git a/doc/src/sgml/pgtrgm.sgml b/doc/src/sgml/pgtrgm.sgml index 07bfcac9319..da4da138808 100644 --- a/doc/src/sgml/pgtrgm.sgml +++ b/doc/src/sgml/pgtrgm.sgml @@ -424,6 +424,14 @@ for equality operator. + + The gist_trgm_ops_noeq operator class is identical to + gist_trgm_ops, except that it does not support + =. It is meant for cases where a table already has a + B-tree index for equality lookups, and the trigram index is only needed + for the other operators above. + + Example: -- 2.43.0