Applies to VAST Cluster 5.5.
Functions › Regular expressions › Regular expressions. regexp_extract(s, pattern) returns the first part of s that the regular expression pattern matches, or an empty string if nothing matches. With a group number it returns that group of the match; with a list of names it returns a STRUCT with one field per group.
Syntax
regexp_extract(s, pattern)
regexp_extract(s, pattern, group)
regexp_extract(s, pattern, group, options)
regexp_extract(s, pattern, names)
regexp_extract(s, pattern, names, options)
| Argument | Type | Description |
|---|---|---|
s |
VARCHAR |
The string to search. |
pattern |
VARCHAR |
The regular expression (syntax). A constant or a column. |
group |
INTEGER, a constant from 0 to 9 |
Optional. 0 (the default) returns the whole match; 1 to 9 the group with that number. |
names |
VARCHAR[], a constant |
Instead of group: one field name for each group, in order. |
options |
VARCHAR |
Optional, a constant. Flags such as 'i' (ignore case); see options. |
Returns: VARCHAR; with names, a STRUCT with one VARCHAR field per name.
Set up the examples
The examples connect with the ADBC driver, as in VAST Query Engine (ADBC), and work in a schema called docs_sql_fn_string_regexp_extract. Set QE_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and VASTDB_BUCKET (SQL reference), and run the blocks in order in one Python session. Below each block is the output you should see.
This block connects, defines the run() and plan() helpers, and creates a table of four file paths. Path 3 has no date, and the last row has no values:
import os
import re
import adbc_driver_manager
import adbc_driver_manager.dbapi as dbapi
import adbc_driver_vastdb
BUCKET = os.environ["VASTDB_BUCKET"]
SCHEMA = f'"{BUCKET}/docs_sql_fn_string_regexp_extract"'
conn = dbapi.connect(
driver=adbc_driver_vastdb.get_driver_path(),
db_kwargs={
"vast.db.endpoint": os.environ["QE_ENDPOINT"],
"vast.db.access_key": os.environ["AWS_ACCESS_KEY_ID"],
"vast.db.secret_key": os.environ["AWS_SECRET_ACCESS_KEY"],
},
autocommit=True,
)
def server_message(e):
"""The server's message in a driver error, without the request ID or the echoed statement."""
text = re.sub(r"\[req=0x[0-9a-f]+\]: ", "", str(e))
found = (re.search(r'message: "(.*?)", request_id', text, re.S)
or re.search(r"description: (.*)\)", text, re.S))
message = found.group(1) if found else text.strip().splitlines()[-1]
message = re.sub(r"^Plan query: '.*?' failed with: ", "", message, flags=re.S)
message = re.split(r"\\n|\n", message)[0].replace('\\"', '"')
return message.replace(BUCKET, "<bucket>")
def run(sql):
"""Run one statement. Print the rows it returns, or the error."""
try:
with conn.cursor() as cur:
cur.execute(sql)
rows = cur.fetchall() if cur.description else []
except (adbc_driver_manager.Error, OSError) as e:
print(f"{type(e).__name__}: {server_message(e)}")
else:
print(rows)
def plan(sql):
"""Print the plan without its ~N Rows lines: the table's row count, which can still be 0 just after an insert."""
with conn.cursor() as cur:
cur.execute("EXPLAIN " + sql)
steps = cur.fetchall()[0][1]
print("\n".join(line for line in steps.splitlines() if "Rows" not in line))
TABLE = f"{SCHEMA}.files"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, path VARCHAR)")
run(f"INSERT INTO {TABLE} VALUES "
"(1, '/data/2026-10-16/app.log'), "
"(2, '/data/2026-10-17/db.csv'), "
"(3, '/tmp/README'), "
"(4, NULL)")
[]
[]
[(4,)]
What it does
regexp_extract finds the first match of pattern in s, then returns:
| Form | Returns | Example | Result |
|---|---|---|---|
no group (or 0) |
the whole match | regexp_extract('a1b22', '\d+') |
'1' |
group 1 to 9 |
that group of the match | regexp_extract('abc', '(a)(b)', 2) |
'b' |
names |
a STRUCT of the groups, named in order |
regexp_extract('2026-10', '(\d+)-(\d+)', ['y', 'm']) |
{'y': '2026', 'm': '10'} |
Without a match, the result is an empty string, not NULL; with names, every field is an empty string. A group that doesn't take part in the match, such as (x)? in '(x)?b', also gives ''. Groups are numbered by their opening parenthesis; (?:...) doesn't count, and a named group (?P<n>...) counts like any other. Read a field of the STRUCT with .name or ['name'].
NULLs and edge cases
| Input | Result |
|---|---|
A NULL string or pattern |
NULL |
A NULL group, or a NULL name list |
'' |
A NULL options string |
Error: Regex options field must not be NULL |
regexp_extract('abc', 'x') (no match) |
'' |
regexp_extract('abc', '(a)', 2) (the pattern has one group) |
'' |
regexp_extract('abc', '(a)', 10) or -1 |
Error: Group index must be between 0 and 9! |
regexp_extract('e\u0301x', '^.') |
'e' (a . is one code point) |
regexp_extract('abc', '(') (an invalid pattern) |
'', with no error |
regexp_extract('2026-10', '(\d+)', ['y', 'm']) (more names than groups) |
Error: Not enough group names in regexp_extract |
regexp_extract('2026-10', '(\d+)-(\d+)', ['y']) (fewer names) |
{'y': '2026'} |
The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:
run("SELECT regexp_extract(NULL, 'a'), regexp_extract('abc', NULL), regexp_extract('abc', '(a)', NULL), "
"regexp_extract('abc', 'x'), regexp_extract('abc', '(a)', 2)")
run("SELECT regexp_extract('abc', '(b)', 1, NULL)")
run("SELECT regexp_extract('abc', '(a)', 10)")
run("SELECT regexp_extract('e\u0301x', '^.'), regexp_extract('abc', '(')")
run("SELECT regexp_extract('2026-10', '(\\d+)', ['y', 'm'])")
run("SELECT regexp_extract('2026-10', '(\\d+)-(\\d+)', ['y'])")
[(None, None, '', '', '')]
InternalError: Invalid Input Error: Regex options field must not be NULL
InternalError: Invalid Input Error: Group index must be between 0 and 9!
[('e', '')]
InternalError: Binder Error: Not enough group names in regexp_extract
[({'y': '2026'},)]
Examples
The date in each path, its month (group 2), and the file extension. Path 3 has no date, so the first two are empty strings:
run(f"SELECT id, regexp_extract(path, '\\d{{4}}-\\d{{2}}-\\d{{2}}'), "
f"regexp_extract(path, '(\\d{{4}})-(\\d{{2}})', 2), regexp_extract(path, '\\.(\\w+)$', 1) "
f"FROM {TABLE} ORDER BY id LIMIT 10")
[(1, '2026-10-16', '10', 'log'), (2, '2026-10-17', '10', 'csv'), (3, '', '', ''), (4, None, None, None)]
Split the date into a STRUCT with named fields, and read one field:
run(f"SELECT id, regexp_extract(path, '(\\d+)-(\\d+)-(\\d+)', ['y', 'm', 'd']), "
f"regexp_extract(path, '(\\d+)-(\\d+)-(\\d+)', ['y', 'm', 'd']).m FROM {TABLE} ORDER BY id LIMIT 10")
[(1, {'y': '2026', 'm': '10', 'd': '16'}, '10'), (2, {'y': '2026', 'm': '10', 'd': '17'}, '10'), (3, {'y': '', 'm': '', 'd': ''}, ''), (4, None, None)]
Ignore case with option 'i':
run(f"SELECT id, regexp_extract(path, '/([a-z]+)$', 1), regexp_extract(path, '/([a-z]+)$', 1, 'i') "
f"FROM {TABLE} ORDER BY id LIMIT 10")
[(1, '', ''), (2, '', ''), (3, '', 'README'), (4, None, None)]
How it runs in VAST
The Query Engine runs on the cluster's CNodes, and a query on a table starts with a scan in VAST DataBase (VAST_SCAN). regexp_extract is always evaluated in the Query Engine:
- In the
SELECTlist, the scan reads the column and the Query Engine extracts from each value it returns. - In
WHERE, the scan sends the column for every row, and aFILTERstep computesregexp_extract()and keeps the rows that match.
plan(f"SELECT id FROM {TABLE} WHERE regexp_extract(path, '\\d{{4}}') = '2026'")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ #1 │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ (regexp_extract(path, ' │
│ \d{4}') = '2026') │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ path │
│ id │
│ │
└───────────────────────────┘
Make it faster
- Extract at load time. If you filter on an extracted part often, such as the date of a path, extract it when you write the data and store it in its own column. A comparison of that column with a constant is pushed down to VAST DataBase.
- Test with a pattern, not an extraction. To keep the rows that contain a match,
regexp_matches(path, '\d{4}')is rewritten into cheaper tests for simple patterns (regexp_matches);regexp_extract(path, ...) <> ''isn't. - Extract several groups in one call. One call with a name list returns every group of one match.
Limitations and errors
- Group numbers 0 to 9 only, and a constant: a group from a column fails with
Group specification field must be a constant!, and so does a name list from a column. - Name a group with the list form. A string such as
'n'as the third argument fails withCould not choose a best candidate function; write['n']. - Name lists have rules. Empty:
regexp_extract requires non-empty lists of capture names. A repeated name:Duplicate group name "y" in regexp_extract. ANULLname:NULL group name in regexp_extract. A typedNULLlist such asNULL::VARCHAR[]fails with an internal error; pass a list literal. - An invalid constant pattern returns
''instead of failing. Check a pattern with regexp_matches, which reports the error. - No
regexp_substr. It fails withScalar Function with name regexp_substr does not exist!; useregexp_extract.
run("SELECT regexp_extract('abc', '(?P<n>b)', 'n')")
run("SELECT regexp_extract('abc', '(?P<n>b)', ['n'])")
run("SELECT regexp_extract('2026-10', '(\\d+)-(\\d+)', ['y', 'y'])")
run("SELECT regexp_matches('abc', '(')")
run("SELECT regexp_substr('abc', 'b')")
InternalError: Binder Error: Could not choose a best candidate function for the function call "regexp_extract(STRING_LITERAL, STRING_LITERAL, STRING_LITERAL)". In order to select one, please add explicit type casts.
[({'n': 'b'},)]
InternalError: Binder Error: Duplicate group name "y" in regexp_extract
InternalError: Invalid Input Error: missing ): (
InternalError: Catalog Error: Scalar Function with name regexp_substr does not exist!
Similar functions
| Function | Returns | Result for 'a,b,c' |
|---|---|---|
regexp_extract('a,b,c', '^[^,]*,([^,]*)', 1) |
one group of the first match | 'b' |
regexp_extract_all('a,b,c', '[^,]+') |
every match, as a list | ['a', 'b', 'c'] |
split_part('a,b,c', ',', 2) |
one field between literal separators | 'b' |
string_split('a,b,c', ',')[2] |
a list of fields, then one element | 'b' |
regexp_matches('a,b,c', ',') |
whether there's a match | true |
run("SELECT regexp_extract('a,b,c', '^[^,]*,([^,]*)', 1), regexp_extract_all('a,b,c', '[^,]+'), "
"split_part('a,b,c', ',', 2), string_split('a,b,c', ',')[2], regexp_matches('a,b,c', ',')")
[('b', ['a', 'b', 'c'], 'b', 'b', True)]
Clean up:
run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]
See also
- Regular-expression syntax and options.
- Regular expressions: all the regular-expression functions.
- Where filters run and pushdowns.
- EXPLAIN: how to read a plan.