Re: Use a hash table for the regexp cache

From: Tianyi Gao <gtydoit(at)gmail(dot)com>
To: jian he <jian(dot)universality(at)gmail(dot)com>
Cc: pgsql-hackers(at)lists(dot)postgresql(dot)org
Subject: Re: Use a hash table for the regexp cache
Date: 2026-11-02 02:41:20
Message-ID: CANZS+G8cDsrf34ukTUrSzmMsTxkPkmWxBePxOw96_NHEfuOQAw@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

On Sun, Nov 1, 2026 at 9:56 PM jian he <jian(dot)universality(at)gmail(dot)com> wrote:
> Currently, compiled regular expressions are cached in
> RegexpCacheMemoryContext, which is session-level. Another possible
> direction is to cache them in query-level instead.
>
> Nearly all callers of RE_compile_and_cache() can be reached from a
> function that has an fcinfo: the regex operators, the regexp_*
> functions, textregexsubstr() and textregexreplace(). We can cache the
> compiled regex in fcinfo->flinfo->fn_extra, the same way
> array_sort_internal() caches its ArraySortCachedInfo. The compiled
> regex would then be freed automatically when the query ends.

Thanks for looking!

I think a query-lifetime cache would make a common case slower, and it
can't cover all the callers.

The session cache lets a short query reuse a regexp compiled by an
earlier query. With a cache in fn_extra, every execution compiles it
again, including executions of a prepared statement, since the FmgrInfo
is rebuilt for each execution. To measure the cost of one pg_regcomp()
per query on a one-row table, I ran
"SELECT count(*) FROM t WHERE c ~ ('^(item|thing)[0-9]*' || :n || '$')"
with pgbench and one client: 20 us per query when :n is fixed (session
cache hit) vs. 26 us when :n is random (compiled once per query), or
about 30% slower.

Also, not all callers have a usable fn_extra:

- regexp_matches() and regexp_split_to_table() are set-returning
functions, and fn_extra already holds their FuncCallContext. That
state only lasts for one call, so in something like
"FROM t, regexp_matches(t.c, 'pat')" the regexp would be compiled
again for every row of t.

- jsonpath's like_regex is reached through the jsonb_path_*()
functions (some of which are SRFs too) and through JSON_EXISTS() /
JSON_VALUE() in execExprInterp.c, which don't go through an fcinfo at
all. One jsonpath can also contain several like_regex items, so
there is no single slot per call site.

- regexp_fixed_prefix() is called from the planner (like_support.c),
where there is no fcinfo. In fact, for "WHERE c ~ 'pat'", the
planner compiles 'pat' in regexp_fixed_prefix() and the executor
then reuses that entry from the session cache in textregexeq(); with
fn_extra, even a single query would compile it twice.

- DirectFunctionCall4Coll(textregexreplace, ...) in extension.c passes
a NULL flinfo, and RE_compile_and_cache() is exported in
regex/regex.h, so extensions may be calling it too.

And when the pattern comes from a column, one call site sees many
patterns, so each call site would still need a small cache of its own.

The case where fn_extra would help most is a constant pattern, and 0001
already handles that by checking the most recently used entry before
computing a hash, so a hit costs a single memcmp().

> In https://wiki.postgresql.org/wiki/Todo we also have an entry
> {Consider a special data type for regular expressions}

A regexp data type would avoid the cache for patterns stored as that
type, but it's a much bigger project with compatibility questions of
its own, and 0001 doesn't get in its way.

So I'd like to keep the session-level cache and go on with the sizing
question from my first mail. If others would rather explore the
query-level approach, I'm happy to prototype it so we can compare.

--
Tianyi Gao

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Fujii Masao 2026-11-02 02:41:56 Re: bug: query returns different result with and without memoization.
Previous Message mingjiegao 2026-11-02 02:37:02 Re: [PATCH v1] Fix initial sync of zero-column published tables