From 894c37ed58d79a208ac985020e8e8c22d47c4bea Mon Sep 17 00:00:00 2001 From: Zhong ShiHao Date: Tue, 28 Jul 2026 23:47:12 -0400 Subject: [PATCH] Add planner support function to two-argument regexp_like() The ~ operator's underlying function textregexeq() has a planner support function that lets the planner derive index conditions from a pattern with a fixed prefix. regexp_like(), added in PG 15 as the SQL-standard spelling of the same test, was never given one, so the standard spelling silently loses the optimization. For the two-argument form this is a pure oversight: regexp_like_no_flags() calls regexp_like(), which with a NULL flags argument leaves parse_re_flags() at its REG_ADVANCED default and then invokes RE_compile_and_execute() with exactly the arguments textregexeq() uses. The two are the same test. like_regex_support() already handles being invoked on a FuncExpr rather than an OpExpr -- see the is_funcclause() branch, which asserts a two-argument call -- so no new C code is needed; the function just has to be pointed at the existing support function. On a 20000-row table with a btree index on the column, this turns WHERE regexp_like(s, '^item19999') from a sequential scan into an index scan with Index Cond: s >= 'item19999' AND s < 'item1999:', matching what the equivalent s ~ '^item19999' has always produced. Selectivity estimation now also matches the operator spelling, since patternsel_common() is reached through the same support function. The three-argument form is deliberately left alone. It cannot share textregexeq_support() as-is: like_regex_support() asserts a two-argument call, and more importantly several flags would make prefix extraction unsound. With 'n', for example, '^' matches after a newline, so E'xyz\nabc' satisfies '^abc' while sorting outside the ['abc','abd') range the index condition would scan. Supporting flags requires a separate support function that inspects them and declines on anything other than 'i'/'c'. Note that regexp_like() only has a (text, text) signature, so a column of a type that is not binary-coercible to text -- name, for instance -- still gets no index condition, because the cast keeps the Var from matching the index column. varchar works, being binary-coercible. This changes pg_proc, so catversion needs a bump. --- src/include/catalog/pg_proc.dat | 3 +- src/test/regress/expected/btree_index.out | 51 +++++++++++++++++++++++ src/test/regress/sql/btree_index.sql | 26 ++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index f8a021987b5..6d142bf9564 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3856,7 +3856,8 @@ proargnames => '{string, pattern, start, N, endoption, flags, subexpr}', prosrc => 'regexp_instr' }, { oid => '6263', descr => 'test for regexp match', - proname => 'regexp_like', prorettype => 'bool', proargtypes => 'text text', + proname => 'regexp_like', prosupport => 'textregexeq_support', + prorettype => 'bool', proargtypes => 'text text', proargnames => '{string, pattern}', prosrc => 'regexp_like_no_flags' }, { oid => '6264', descr => 'test for regexp match', proname => 'regexp_like', prorettype => 'bool', diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out index 3a83e9a0534..9ab0b15e2fc 100644 --- a/src/test/regress/expected/btree_index.out +++ b/src/test/regress/expected/btree_index.out @@ -767,6 +767,57 @@ select * from btree_bpchar where f1::bpchar like 'foo%'; -- get test coverage for "single value" deduplication strategy: insert into btree_bpchar select 'foo' from generate_series(1,1500); +-- Check that the two-argument form of regexp_like() gets the same index +-- optimization as the equivalent ~ operator, via the planner support +-- function attached to it. +create temp table btree_regexp_like (f1 text collate "C"); +create index on btree_regexp_like(f1); +insert into btree_regexp_like + select 'item' || g from generate_series(1, 1000) g; +analyze btree_regexp_like; +set enable_seqscan to false; +explain (costs off) +select * from btree_regexp_like where regexp_like(f1, '^item999'); + QUERY PLAN +--------------------------------------------------------------------- + Index Only Scan using btree_regexp_like_f1_idx on btree_regexp_like + Index Cond: ((f1 >= 'item999'::text) AND (f1 < 'item99:'::text)) + Filter: regexp_like(f1, '^item999'::text) +(3 rows) + +select * from btree_regexp_like where regexp_like(f1, '^item999') order by 1; + f1 +--------- + item999 +(1 row) + +-- must agree with the operator spelling +select * from btree_regexp_like where f1 ~ '^item999' order by 1; + f1 +--------- + item999 +(1 row) + +reset enable_seqscan; +-- a pattern with no fixed prefix yields no index condition +explain (costs off) +select * from btree_regexp_like where regexp_like(f1, 'item999$'); + QUERY PLAN +--------------------------------------------- + Seq Scan on btree_regexp_like + Filter: regexp_like(f1, 'item999$'::text) +(2 rows) + +-- the three-argument form has no support function, since some flags would +-- make prefix extraction unsound +explain (costs off) +select * from btree_regexp_like where regexp_like(f1, '^item999', 'c'); + QUERY PLAN +-------------------------------------------------------- + Seq Scan on btree_regexp_like + Filter: regexp_like(f1, '^item999'::text, 'c'::text) +(2 rows) + -- -- Perform unique checking, with and without the use of deduplication -- diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql index a08bb101c20..0a1525a8bdc 100644 --- a/src/test/regress/sql/btree_index.sql +++ b/src/test/regress/sql/btree_index.sql @@ -414,6 +414,32 @@ select * from btree_bpchar where f1::bpchar like 'foo%'; -- get test coverage for "single value" deduplication strategy: insert into btree_bpchar select 'foo' from generate_series(1,1500); +-- Check that the two-argument form of regexp_like() gets the same index +-- optimization as the equivalent ~ operator, via the planner support +-- function attached to it. + +create temp table btree_regexp_like (f1 text collate "C"); +create index on btree_regexp_like(f1); +insert into btree_regexp_like + select 'item' || g from generate_series(1, 1000) g; +analyze btree_regexp_like; + +set enable_seqscan to false; +explain (costs off) +select * from btree_regexp_like where regexp_like(f1, '^item999'); +select * from btree_regexp_like where regexp_like(f1, '^item999') order by 1; +-- must agree with the operator spelling +select * from btree_regexp_like where f1 ~ '^item999' order by 1; +reset enable_seqscan; + +-- a pattern with no fixed prefix yields no index condition +explain (costs off) +select * from btree_regexp_like where regexp_like(f1, 'item999$'); +-- the three-argument form has no support function, since some flags would +-- make prefix extraction unsound +explain (costs off) +select * from btree_regexp_like where regexp_like(f1, '^item999', 'c'); + -- -- Perform unique checking, with and without the use of deduplication -- -- 2.37.1 (Apple Git-137.1)