Regular Expressions in SQL
A regular expression describes the shape of text: digits, then letters. regexp_matches finds rows that fit, and regexp_extract pulls out the part you need.
LIKE knows two wildcards: any one character, and any run of characters. It can't say "one or more digits" or "a number followed by letters". A regular expression can. It's a small language for describing the shape of text, and DuckDB has three functions that use one:
-
regexp_matches(text, pattern)is true when the pattern appears anywhere in the text. -
regexp_extract(text, pattern, group)returns the part of the text the pattern found. -
regexp_replace(text, pattern, replacement, 'g')swaps what it found for something else.
\d
means one digit, so this finds the six products with a number in their name:
select product_name
from products
where regexp_matches(product_name, '\d')
order by product_id The pieces of a pattern
Most patterns are built from a handful of pieces. Every other character matches itself.
| Piece | Matches |
|---|---|
\d | One digit, 0 to 9 |
[a-z], [a-zA-Z] | One character from the range |
[^0-9] | One character that is not in the range |
. | Any one character |
+, *, ? | The piece before it, one or more times, any number of times, or optionally |
{3} | The piece before it, exactly three times |
^, $ | The start and the end of the text |
(pot|plant) | Either alternative, and a group you can extract |
\. | A real dot. A backslash makes any special character literal |
So
\d+[a-zA-Z]+
reads as "one or more digits, then one or more letters", and matches
20L
and
25ft.
Pulling a value out
Wrap the part you want in parentheses to make it a group,
then pass its number to
regexp_extract. Group 0, the default, is the whole match. This takes the name between the
@
and the first dot of each supplier's email:
select supplier_name,
contact_email,
regexp_extract(contact_email, '@([a-z]+)\.', 1) as mail_domain
from suppliers
order by supplier_id
Two suppliers have no email. A
NULL
in gives
NULL
out. Text that doesn't match is different: DuckDB returns an empty string,
'', not
NULL.
Replacing what matches
regexp_replace
changes only the first match unless you pass
'g'
(global) as the fourth argument. Here
[^0-9]
matches every character that isn't a digit, so the phone numbers keep only
their digits:
select phone,
regexp_replace(phone, '[^0-9]', '', 'g') as digits
from customers
where phone is not null
order by customer_id
limit 3
Leave out the
'g'
and 215-555-0142 becomes 215555-0142. See
why regexp_replace changes only the first match.
Ignoring case
Patterns are case-sensitive. Pass
'i'
as the last argument to ignore case. Every product name is capitalized, so
without it,
(pot|plant)
finds nothing. With it, the pattern finds six, from Snake Plant to Potting
Soil 20L:
select product_name
from products
where regexp_matches(product_name, '(pot|plant)', 'i')
order by product_id When an extract feeds a cast
Because a missed match is
'', casting the result of every row fails on the first product without a
number:
select product_name,
cast(regexp_extract(product_name, '\d+') as integer) as size_amount
from products
DuckDB stops with
Could not convert string '' to INT32. Keep only the rows that match first, with
regexp_matches
in the
WHERE
clause, or use
try_cast, which turns
''
into
NULL.
Every engine spells these differently. PostgreSQL uses ~ for
"matches anywhere" and substring(text from pattern) to extract.
In DuckDB, ~ means the whole text must match, so
'Potting Soil 20L' ~ '\d+' is false. MySQL and Oracle have
REGEXP_LIKE and REGEXP_SUBSTR. The
regular expressions reference
lists each engine's names.
Schema · Garden Shop Table · products 9 columns · 24 rows
One row per product, with price, cost, and inventory levels.
Some products carry a size in their name, such as Potting
Soil 20L or Drip Hose 25ft: a number followed straight away by letters. For
each product whose name has a size, return product_id,
product_name, size_amount (the number, as an
integer), and size_unit (the letters). Sort by
product_id.
Run your query to inspect the result.
Expected answer
- Columns: product_id, product_name, size_amount, size_unit.
- Rows: 6 sized products, by product_id.
- Potting Soil 20L becomes 20 and L; Tulip Bulbs (10pk) becomes 10 and pk.
Related
Match, extract, and replace text by its shape.
Pass 'g' to replace every match, not just the first.
Length, case, trimming, slicing, and concatenation.
Escape _ and % to match them literally.
Spaces and capitals make one city two groups.
Find the customer behind a name typed with a typo.