SQLMentor // learn sql

Regular Expressions

Oracle's regular expression (regex) functions let you match, search, replace, and extract substrings using pattern syntax — far more powerful than the basic LIKE operator. They're the right tool for validating emails, extracting phone numbers from free text, finding rows that match complex patterns, or cleaning messy data.

Oracle's regex flavour is POSIX Extended Regular Expressions (with some Perl-compatible extras since 10gR2). If you know regex from Python, JavaScript, or grep, most patterns translate directly.

The Five REGEXP Functions

Oracle provides five regex functions. Each takes a string and a pattern, plus optional modifiers:

Function Purpose Returns
REGEXP_LIKE Test if a string matches a pattern Boolean (used in WHERE)
REGEXP_SUBSTR Extract the matching substring The match (or NULL if no match)
REGEXP_REPLACE Replace matches with another string Modified string
REGEXP_INSTR Position of the match Integer (or 0 if no match)
REGEXP_COUNT How many times the pattern matches Integer

All five accept a final match parameter that controls case sensitivity, multiline behaviour, and more (covered below).

REGEXP_LIKE — Pattern Matching in WHERE

REGEXP_LIKE is the regex equivalent of LIKE. It returns true when the source string matches the pattern.

-- Find employees whose first_name starts with 'A' or 'a'
SELECT first_name, last_name
FROM   employees
WHERE  REGEXP_LIKE(first_name, '^[Aa]');

Result:

first_name last_name
Adam Fripp
Alana Walsh
Alexander Hunold
Alyssa Hutton

^[Aa] reads as "start of string, then either A or a".

Compare with the basic LIKE equivalent:

-- LIKE works for simple patterns but can't express "A or a" without ORs
WHERE first_name LIKE 'A%' OR first_name LIKE 'a%'

Regex shines once patterns become non-trivial:

-- Employees whose phone number is in (123) 456-7890 format:
SELECT first_name, phone_number
FROM   employees
WHERE  REGEXP_LIKE(phone_number, '^\([0-9]{3}\) [0-9]{3}-[0-9]{4}$');

That single pattern would take 20 LIKE clauses.

REGEXP_SUBSTR — Extract a Match

REGEXP_SUBSTR returns the substring that matched the pattern, or NULL if there was no match.

-- Extract the area code from a phone number
SELECT REGEXP_SUBSTR('(415) 555-1234', '\(([0-9]{3})\)') AS area_code
FROM   dual;

Result: (415)

Without parentheses around the digits, that returns the whole match including the literal ( and ). To return only the captured group (the digits inside), add the subexpression parameter:

SELECT REGEXP_SUBSTR('(415) 555-1234',
                     '\(([0-9]{3})\)',
                     1, 1, NULL, 1) AS area_code
FROM   dual;

Result: 415

The 6 arguments are: (source, pattern, position, occurrence, match_param, subexpression). The last one — subexpression — picks which (...) capture group to return (1 for the first, 2 for the second, etc.).

Common Extraction Patterns

-- Extract everything before the @ in an email
SELECT REGEXP_SUBSTR('alice@company.com', '^[^@]+') FROM dual;
-- Result: alice

-- Extract digits only from a noisy string
SELECT REGEXP_SUBSTR('Order #12345 (priority)', '\d+') FROM dual;
-- Result: 12345

-- Extract the 3rd word
SELECT REGEXP_SUBSTR('The quick brown fox', '\w+', 1, 3) FROM dual;
-- Result: brown

-- Extract every word as a separate row (combine with CONNECT BY)
SELECT REGEXP_SUBSTR('The quick brown fox', '\w+', 1, LEVEL) AS word
FROM   dual
CONNECT BY REGEXP_SUBSTR('The quick brown fox', '\w+', 1, LEVEL) IS NOT NULL;

REGEXP_REPLACE — Search and Replace

REGEXP_REPLACE replaces every match of the pattern with a replacement string.

-- Strip all non-digit characters from a phone number
SELECT REGEXP_REPLACE('(415) 555-1234', '[^0-9]', '') AS cleaned
FROM   dual;

Result: 4155551234

Use \1, \2, etc. in the replacement to refer to captured groups:

-- Reformat a date from MM/DD/YYYY to YYYY-MM-DD
SELECT REGEXP_REPLACE('03/15/2024',
                      '^([0-9]{2})/([0-9]{2})/([0-9]{4})$',
                      '\3-\1-\2') AS iso_date
FROM   dual;

Result: 2024-03-15

Cleaning Whitespace

-- Collapse multiple spaces into one
SELECT REGEXP_REPLACE('  hello   world  ', '\s+', ' ') FROM dual;
-- Result: ' hello world '

-- Trim leading and trailing whitespace
SELECT REGEXP_REPLACE('  hello world  ', '^\s+|\s+$', '') FROM dual;
-- Result: 'hello world'

For trimming specifically, the built-in TRIM is faster and clearer than a regex.

REGEXP_INSTR — Position of a Match

Returns the 1-based character position where the pattern matches, or 0 if there's no match.

-- Find where the @ symbol starts in an email
SELECT REGEXP_INSTR('alice@company.com', '@') FROM dual;
-- Result: 6

-- Find the position AFTER the match using return_option = 1:
SELECT REGEXP_INSTR('alice@company.com', '@', 1, 1, 1) FROM dual;
-- Result: 7 (position right after @)

Useful for splitting strings or trimming based on a pattern.

REGEXP_COUNT — How Many Matches

-- Count vowels in a string
SELECT REGEXP_COUNT('Database', '[aeiouAEIOU]') FROM dual;
-- Result: 4 (a, a, a, e)

-- Count words
SELECT REGEXP_COUNT('The quick brown fox', '\w+') FROM dual;
-- Result: 4

Pattern Syntax Cheat Sheet

Character Classes

Pattern Matches
. Any single character (except newline by default)
\d A digit ([0-9])
\D A non-digit
\w Word character ([A-Za-z0-9_])
\W Non-word character
\s Whitespace (space, tab, newline, ...)
\S Non-whitespace
[abc] Any of a, b, or c
[^abc] Any character except a, b, or c
[a-z] Any lowercase letter
[A-Z0-9] Uppercase or digit

Quantifiers

Pattern Meaning
? 0 or 1 of the previous element
* 0 or more
+ 1 or more
{n} Exactly n
{n,} At least n
{n,m} Between n and m

Anchors

Pattern Meaning
^ Start of string (or start of line in multiline mode)
$ End of string (or end of line)
\b Word boundary
\B Non-word boundary

Grouping and Alternation

Pattern Meaning
(abc) Capture group — referenced later as \1
(?:abc) Non-capturing group (Perl-compatible)
`a b`

POSIX Character Classes

Oracle's POSIX flavour supports named classes inside [ ]:

Class Meaning
[:alpha:] Alphabetic characters
[:alnum:] Alphanumeric
[:digit:] Digits
[:lower:] Lowercase letters
[:upper:] Uppercase letters
[:space:] Whitespace
[:punct:] Punctuation
[:xdigit:] Hexadecimal digit

Note the double brackets when using them: [[:digit:]] matches a digit, just like \d.

-- Match if the string is entirely letters
WHERE REGEXP_LIKE(name, '^[[:alpha:]]+$')

Match Parameter (the Final Flag)

The optional match parameter is a string of flags that modify behaviour:

Flag Effect
i Case-insensitive
c Case-sensitive (default)
n . matches newlines too (DOTALL)
m Multiline mode: ^ and $ match line breaks
x Ignore whitespace in pattern (allows readable patterns)
-- Case-insensitive search
WHERE REGEXP_LIKE(comments, 'error', 'i');

-- Combine flags: ignore case AND let . match newlines
WHERE REGEXP_LIKE(log_text, 'failed.+retry', 'in');

Real-world Patterns

Email Validation (Pragmatic, Not Perfect)

WHERE REGEXP_LIKE(email,
  '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')

A "fully RFC-5322 compliant" email regex is hundreds of characters and not worth the complexity. The pattern above catches 99% of real-world email formatting bugs.

US Phone Number (Multiple Formats)

-- Accept (415) 555-1234, 415-555-1234, 415.555.1234, 4155551234
WHERE REGEXP_LIKE(phone,
  '^\(?[0-9]{3}\)?[ .-]?[0-9]{3}[ .-]?[0-9]{4}$')

US ZIP Code

-- 5-digit or ZIP+4 (12345 or 12345-6789)
WHERE REGEXP_LIKE(zipcode, '^[0-9]{5}(-[0-9]{4})?$')

Strong Password (8+ chars, mix of types)

WHERE REGEXP_LIKE(pwd, '^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$')

This uses lookaheads (?=...) which Oracle supports since 10gR2.

Extract URL Components

SELECT
  REGEXP_SUBSTR(url, '^([^:]+)',        1, 1, NULL, 1) AS protocol,
  REGEXP_SUBSTR(url, '://([^/]+)',      1, 1, NULL, 1) AS host,
  REGEXP_SUBSTR(url, '(/[^?]*)',        1, 1, NULL, 1) AS path
FROM   urls;

Performance Considerations

Regex functions are CPU-expensive compared to LIKE or simple string equality. Two principles:

  1. Use LIKE when LIKE is enough. LIKE 'A%' is much faster than REGEXP_LIKE(col, '^A').
  2. Add a B-tree index only on equality columns. Plain text indexes cannot accelerate regex searches.

Function-Based Indexes

If you frequently search on a regex-transformed version of a column (e.g., a lowercased name), a function-based index can help — but the function must match exactly:

-- Index on lowercased name:
CREATE INDEX emp_lname_lower_idx ON employees (LOWER(last_name));

-- This query uses the index:
WHERE LOWER(last_name) = 'smith';

-- This does NOT — REGEXP_LIKE can't use a B-tree index on LOWER(last_name):
WHERE REGEXP_LIKE(last_name, 'smith', 'i');

For true full-text searching, Oracle Text (the CONTEXT index type) is purpose-built and orders of magnitude faster than REGEXP_LIKE on large tables. Use Oracle Text when you need substring or proximity searches across many gigabytes.

Avoiding Function Calls in WHERE

-- Slow: regex runs on every row of the table
SELECT * FROM customer_messages
WHERE  REGEXP_LIKE(body, 'urgent.*follow.up', 'i');

-- Faster: narrow the candidate set first with an indexed condition
SELECT * FROM customer_messages
WHERE  status = 'OPEN'
  AND  created_at > SYSDATE - 7
  AND  REGEXP_LIKE(body, 'urgent.*follow.up', 'i');

The first condition can use an index on status or created_at; the regex only runs on the small candidate set that survives.

Common Patterns Across Languages

Goal Oracle Regex
Numbers only '^[0-9]+$'
Letters only '^[A-Za-z]+$'
Alphanumeric '^[A-Za-z0-9]+$'
Trim leading zeros REGEXP_REPLACE(col, '^0+', '')
Mask credit card (keep last 4) REGEXP_REPLACE(cc, '\d(?=\d{4})', '*')
First letter of each word REGEXP_SUBSTR(text, '\b\w', 1, LEVEL)

Common Errors

Error Cause Fix
ORA-12725: unmatched parentheses in regular expression Unbalanced ( and ) in pattern Escape literal parens with \( and \); check group nesting
ORA-12726: unmatched bracket in regular expression Unclosed [ ] Close the character class with ]
ORA-12728: invalid range in regular expression Range like [z-a] (end before start) Reverse to [a-z]
ORA-12733: regular expression too complex Pattern with deep nesting or many alternatives Simplify the pattern; split into multiple REGEXP_LIKE calls combined with OR
Empty result from REGEXP_SUBSTR Pattern didn't match — returns NULL, not empty string Wrap in NVL(REGEXP_SUBSTR(...), 'default')
Too many matches in REGEXP_REPLACE Forgot the global behaviour: REGEXP_REPLACE replaces all occurrences Pass occurrence as the 5th arg: REGEXP_REPLACE(s, p, r, 1, 1) replaces only the first
Performance is terrible Regex applied to every row; no other selective condition Combine with an indexed predicate to narrow the candidate rows

Interview Corner

IQ · Regex
When would you choose LIKE over REGEXP_LIKE?
▶ Show answer

Always prefer LIKE when its limited syntax is enough for the job. Three reasons:

  1. SpeedLIKE 'A%' can use a B-tree index on the column. REGEXP_LIKE(col, '^A') cannot use a standard index.
  2. ReadabilityLIKE '%@gmail.com' is instantly clear; the regex equivalent isn't.
  3. Index strategy — anchored LIKE patterns like 'A%' are sargable (can use the index for a range scan). REGEXP_LIKE never is.

Use REGEXP_LIKE when LIKE can't express what you need:

  • Multiple character classes: [Aa]bc
  • Quantifiers: {3,5} repetitions, \d+
  • Alternation: Mr|Mrs|Dr
  • Word boundaries: \bSQL\b
  • Anchored matching beyond start/end: line-based with m flag

Performance tip: Combine both. Use LIKE for a coarse filter, then REGEXP_LIKE for the precise pattern:

WHERE phone LIKE '%(415)%'                              -- index-friendly
  AND REGEXP_LIKE(phone, '^\(415\)\s\d{3}-\d{4}$')      -- exact format
IQ · Regex
Why might a REGEXP_LIKE query that returns correct results on small tables be unacceptably slow on large tables?
▶ Show answer

REGEXP_LIKE cannot use a standard B-tree index on the column it's matching against. On a 100-row table that doesn't matter; on a 100-million-row table, the regex runs once per row — a full table scan with CPU-heavy pattern matching on every value.

Three strategies to fix it:

  1. Combine with an indexed predicate so the regex only runs on a small candidate set:

    WHERE created_at > SYSDATE - 1                -- uses index
      AND REGEXP_LIKE(body, 'urgent', 'i');       -- runs on few rows
    
  2. Materialise the regex result as a column if you query the same pattern often:

    ALTER TABLE customers ADD (email_valid CHAR(1));
    UPDATE customers SET email_valid = 
      CASE WHEN REGEXP_LIKE(email, '^...$') THEN 'Y' ELSE 'N' END;
    CREATE INDEX cust_email_valid_idx ON customers(email_valid);
    
  3. Use Oracle Text for full-text search on free-form data — purpose-built for this and uses a specialised index.

Avoid regex on every row of a hot path unless absolutely necessary.

IQ · Regex
Write a REGEXP_REPLACE that takes a phone number in any common format and normalises it to digits only.
▶ Show answer

The pattern [^0-9] matches every character that is NOT a digit; replacing them with empty string leaves digits only:

SELECT REGEXP_REPLACE('+1 (415) 555-1234', '[^0-9]', '') FROM dual;
-- Result: 14155551234

SELECT REGEXP_REPLACE('415.555.1234',     '[^0-9]', '') FROM dual;
-- Result: 4155551234

SELECT REGEXP_REPLACE('(415) 555 1234',    '[^0-9]', '') FROM dual;
-- Result: 4155551234

Wait — what if input is NULL or empty? REGEXP_REPLACE on NULL returns NULL. If you want an empty string instead:

NVL(REGEXP_REPLACE(phone, '[^0-9]', ''), '')

More robust version that strips a leading country code if exactly 11 digits:

WITH digits_only AS (
  SELECT REGEXP_REPLACE('+1 (415) 555-1234', '[^0-9]', '') AS d FROM dual
)
SELECT CASE
         WHEN LENGTH(d) = 11 AND SUBSTR(d, 1, 1) = '1' THEN SUBSTR(d, 2)
         ELSE d
       END AS national_number
FROM digits_only;
-- Result: 4155551234

This pattern (normalise then operate) is the standard way to handle messy real-world data.

Related Topics

  • Filtering & ConditionsLIKE and IN for simpler pattern matches
  • Basic SQLSUBSTR, INSTR, REPLACE for non-regex string ops
  • DML CommandsUPDATE with REGEXP_REPLACE for bulk data cleaning
  • Performance — function-based indexes, Oracle Text
  • Indexes — why regex can't use B-tree indexes and what to do about it