Pattern Matching

Four functions over regular expressions: regexp_like asks whether a pattern matches, regexp_count how often, regexp_substr pulls out what matched, and regexp_replace writes something else in its place. Plain wildcard matching with % and _ is LIKE, on the Comparison Operators page.

The pattern itself is not translated

This is the one thing to know before writing a regular expression here. A mask for to_char is written once and rewritten for each database; a pattern is not. It goes to the database exactly as written, and the databases do not run the same regular-expression engine:

PostgreSQL   POSIX ARE          Oracle    POSIX
DuckDB       RE2                Trino     Joni
MariaDB      PCRE-style         SQLite    none at all
SQL Server   none before 2025

What that costs you in practice is small if you stay in the common part — literals, ., *, +, ?, ranges, anchors, groups, alternation. It bites at the edges: lazy quantifiers, look-around, named groups and back-references exist in some engines and not others, and where they are missing you rarely get an error, just a pattern that quietly matches nothing.

Prefer POSIX classes over backslash shorthands. [[:digit:]] works on every engine that has regular expressions at all; \d does not — and on MariaDB and Snowflake a backslash inside a string literal used to be eaten before the pattern ever reached the engine. That is fixed, but the class form is still the portable one.

Replacement covers every match

regexp_replace(s, p, r) replaces all matches, on every dialect. That is worth stating because PostgreSQL and DuckDB replace only the first by default — KQL levels them so the same query gives the same answer everywhere. There is deliberately no flags argument: it meant modifiers on two databases, a numeric position on two others, and nothing at all on a fifth.

Two dialects have no regular expressions

SQLite ships the REGEXP operator but no implementation for it — that needs a loadable extension — and SQL Server gained regular expressions only in the 2025 release. All four functions are therefore declared unsupported on both: a query using them is refused with a named message rather than being answered wrongly. Everything else on this page is available everywhere.

Case sensitivity comes from the column

Matching honours the column's collation, exactly as LIKE and = do. On a case-insensitive collation regexp_like(name, '^a') will match Alfreds; on a case-sensitive one it will not. If the answer must not depend on how the schema was created, normalise with upper() or lower() on both sides rather than relying on a case-insensitivity flag — those flags are not portable either.

regexp_like

regexp_like(string: TEXT, pattern: TEXT [, flags: TEXT]) → BOOLEAN

True if the string matches the regular expression pattern.

Argument Type Description
string TEXT the source text to test
pattern TEXT the regular expression to match against
flags TEXT (optional) match modifiers, e.g. 'i' for case-insensitivity

Sample query:

// regexp_like: does the company name start with A?
FIND customers c
FETCH regexp_like(c.company_name, '^A') starts_with_a

Generated SQL

oracle · postgresql · trino

-- regexp_like: does the company name start with A?
SELECT
  regexp_like(c.company_name, '^A') AS starts_with_a
FROM
 customers c

The remaining dialects differ only in this expression:

Dialect Expression
duckdb regexp_matches(c.company_name, '^A') AS starts_with_a
mariadb (c.company_name REGEXP '^A') AS starts_with_a
snowflake (REGEXP_INSTR(c.company_name, '^A') > 0) AS starts_with_a

Unsupported: mssql, sqlite

regexp_count

regexp_count(string: TEXT, pattern: TEXT [, start: INTEGER]) → INTEGER

Number of matches of pattern in the string.

Argument Type Description
string TEXT the source text to search
pattern TEXT the regular expression to count occurrences of
start INTEGER (optional) 1-based index to begin searching from

Sample query:

// regexp_count: occurrences of 'sales' in the notes.
FIND employees e
FETCH regexp_count(e.notes, 'sales') sales_mentions

Generated SQL

oracle · snowflake · postgresql · trino

-- regexp_count: occurrences of 'sales' in the notes.
SELECT
  regexp_count(e.notes, 'sales') AS sales_mentions
FROM
 employees e

The remaining dialects differ only in this expression:

Dialect Expression
duckdb len(regexp_extract_all(e.notes, 'sales')) AS sales_mentions

Unsupported: mariadb, mssql, sqlite

regexp_substr

regexp_substr(string: TEXT, pattern: TEXT) → TEXT

First substring matching the regular expression pattern.

Argument Type Description
string TEXT the source text to search
pattern TEXT the regular expression to match

Sample query:

// regexp_substr: first run of digits in a phone number.
FIND customers c
FETCH regexp_substr(c.phone, '[0-9]+') first_digits

Generated SQL

oracle · snowflake · postgresql · mariadb

-- regexp_substr: first run of digits in a phone number.
SELECT
  regexp_substr(c.phone, '[0-9]+') AS first_digits
FROM
 customers c

The remaining dialects differ only in this expression:

Dialect Expression
duckdb · trino regexp_extract(c.phone, '[0-9]+') AS first_digits

Unsupported: mssql, sqlite

regexp_replace

regexp_replace(string: TEXT, pattern: TEXT, replacement: TEXT) → TEXT

Replaces every substring matching the regular expression pattern with replacement.

Every match, not just the first — on every database. That guarantee costs something: PostgreSQL and DuckDB replace only the first match by default, so KQL renders them with the g modifier. MariaDB, Oracle and Trino already replace all. Measured before this was levelled, regexp_replace(phone, '[^0-9]', '') on (02) 201 24 67 gave 02) 201 24 67 on the first two and 022012467 on the other three.

Argument Type Description
string TEXT the source text to modify
pattern TEXT the regular expression to match
replacement TEXT the text substituted for each match

Sample query:

// regexp_replace: strip non-digits from a phone number.
// Every match is replaced, on every dialect — the levelling that makes this sample mean what it says.
FIND customers c
FETCH regexp_replace(c.phone, '[^0-9]', '') digits_only

Generated SQL

oracle · snowflake · mariadb · trino

-- regexp_replace: strip non-digits from a phone number.
-- Every match is replaced, on every dialect — the levelling that makes this sample mean what it says.
SELECT
  regexp_replace(c.phone, '[^0-9]', '') AS digits_only
FROM
 customers c

The remaining dialects differ only in this expression:

Dialect Expression
duckdb · postgresql regexp_replace(c.phone, '[^0-9]', '', 'g') AS digits_only

Unsupported: mssql, sqlite