KDB knowledge

Regular expressions

Find, count, cut out and replace text with patterns like \d+ (one or more digits).

A regular expression (regex) is a small pattern that describes text. \d+ means "one or more digits". ^[A-Z]{2,8}$ means "only 2 to 8 capital letters". Use them to check what a user typed, to find numbers in a news article, or to clean text.

Example: Percent numbers people wrote in forum posts

Setup:

CREATE TABLE forum_posts (id LONG PRIMARY KEY AUTO, body STRING NOT NULL);
INSERT INTO forum_posts (body) VALUES
  ('Profit is up 18%. I think the price will go up.'),
  ('But NPL is still 2.6%. Wait for the next quarter.'),
  ('नाफा राम्रो छ, तर मूल्य पहिले नै बढिसकेको छ।'),
  ('NPL went down from 2.91% to 2.6%. That is good news.');
SELECT id,
       regexp_substr(body, '\d+(\.\d+)?%') AS first_percent,
       regexp_count(body, '\d+(\.\d+)?%') AS percents
FROM forum_posts
WHERE regexp_like(body, '\d%')
ORDER BY id;

Result:

id (LONG)first_percent (STRING)percents (LONG)
118%1
22.6%1
42.91%2

(3 rows)

regexp_like

Syntax:

regexp_like(text, pattern)
regexp_like(text, pattern, flags)

true when the pattern is found somewhere in the text.

ArgumentTypeMeaning
textSTRING (a number is turned into text)the text to search in
patternSTRINGthe regular expression
flagsSTRING'i' ignore upper and lower case of English letters; 'c' case must match (the default)

Example

SELECT regexp_like('CITY', '^[A-Z]{2,8}$') AS a, regexp_like('city', '^[A-Z]{2,8}$') AS b, regexp_like('city', '^[A-Z]{2,8}$', 'i') AS ignore_case, regexp_like('CITY', 'IT') AS anywhere;

Result:

a (BOOL)b (BOOL)ignore_case (BOOL)anywhere (BOOL)
truefalsetruetrue

(1 row)

Example: Check an email and a mobile number (Nepal format) when a user registers

SELECT email, phone,
       regexp_like(email, '^[\w.+-]+@[\w-]+(\.[\w-]+)+$') AS email_ok,
       regexp_like(phone, '^9[78]\d{8}$') AS phone_ok
FROM (SELECT 'ram.sharma@example.com' AS email, '9841234567' AS phone
      UNION ALL SELECT 'sita@@mail', '98412345'
      UNION ALL SELECT 'hari@northwind.example.com', '9761234567') AS signups;

Result:

email (STRING)phone (STRING)email_ok (BOOL)phone_ok (BOOL)
ram.sharma@example.com9841234567truetrue
sita@@mail98412345falsefalse
hari@northwind.example.com9761234567truetrue

(3 rows)

Example: Forum posts written in Nepali (Devanagari letters)

Setup:

CREATE TABLE forum_posts (id LONG PRIMARY KEY AUTO, body STRING NOT NULL);
INSERT INTO forum_posts (body) VALUES ('Profit is up 18%.'), ('नाफा राम्रो छ।'), ('CITY को मूल्य बढ्यो');
SELECT id, body FROM forum_posts WHERE regexp_like(body, '[ऀ-ॿ]') ORDER BY id;

Result:

id (LONG)body (STRING)
2नाफा राम्रो छ।
3CITY को मूल्य बढ्यो

(2 rows)

Example: Look-ahead is not supported (shows an error)

SELECT regexp_like('CITY', 'CI(?=TY)');

Result:

Error: line 1, column 8: regexp_like: regular expression 'CI(?=TY)': (?...) forms other than (?:...) are not supported

regexp_substr

Syntax:

regexp_substr(text, pattern)
regexp_substr(text, pattern, flags)

The first part of the text that matches the pattern. NULL when nothing matches.

ArgumentTypeMeaning
textSTRING (a number is turned into text)the text to search in
patternSTRINGthe regular expression
flagsSTRING'i' ignore case; 'c' case must match (the default)

Example

SELECT regexp_substr('EPS 12.50, BVPS 245.3', '\d+\.\d+') AS first_number, regexp_substr('no digits', '\d+') AS none, regexp_substr('City city', 'CITY', 'i') AS ignore_case;

Result:

first_number (STRING)none (STRING)ignore_case (STRING)
12.50NULLCity

(1 row)

Example: Find the email address in news articles

Setup:

CREATE TABLE news (id LONG PRIMARY KEY AUTO, title STRING NOT NULL, body STRING NOT NULL);
INSERT INTO news (title, body) VALUES
  ('City Bank profit rises 18%', 'City Bank reported a net profit of $3.27 billion for Q4 of 2082/83.'),
  ('Volt River sells record energy', 'The plant produced 2105 GWh this year. Contact: info@voltriver.example.com');
SELECT id, regexp_substr(body, '[\w.]+@[\w.]+\w') AS email
FROM news
WHERE regexp_like(body, '\w+@\w+');

Result:

id (LONG)email (STRING)
2info@voltriver.example.com

(1 row)

Example: Only the number, without the % sign (a group)

SELECT regexp_substr('Profit is up 18.5% this year', '\d+(\.\d+)?%') AS with_sign,
       regexp_replace('Profit is up 18.5% this year', '^.*?(\d+(\.\d+)?)%.*$', '\1') AS group_1;

Result:

with_sign (STRING)group_1 (STRING)
18.5%18.5

(1 row)

regexp_count

Syntax:

regexp_count(text, pattern)
regexp_count(text, pattern, flags)

How many times the pattern is found in the text. The matches do not overlap: after a match, the search goes on after its end.

ArgumentTypeMeaning
textSTRING (a number is turned into text)the text to search in
patternSTRINGthe regular expression
flagsSTRING'i' ignore case; 'c' case must match (the default)

Example

SELECT regexp_count('a1b22c333', '\d+') AS numbers, regexp_count('a1b22c333', '\d') AS digits, regexp_count('CITY city', 'city', 'i') AS ignore_case, regexp_count('abc', '') AS empty_pattern;

Result:

numbers (LONG)digits (LONG)ignore_case (LONG)empty_pattern (LONG)
3624

(1 row)

Example: Words in English and Nepali posts

Setup:

CREATE TABLE forum_posts (id LONG PRIMARY KEY AUTO, body STRING NOT NULL);
INSERT INTO forum_posts (body) VALUES ('Profit is up 18%. I think the price will go up.'), ('सिटी बैंकको नाफा १८ प्रतिशतले बढ्यो।');
SELECT id, regexp_count(body, '\w+') AS words, length(body) AS letters FROM forum_posts ORDER BY id;

Result:

id (LONG)words (LONG)letters (LONG)
11147
2636

(2 rows)

Example: Hashtags in a post

SELECT regexp_count('Buy #CITY and #VOLT now #market', '#\w+') AS tags,
       regexp_substr('Buy #CITY and #VOLT now #market', '#\w+') AS first_tag;

Result:

tags (LONG)first_tag (STRING)
3#CITY

(1 row)

regexp_replace

Syntax:

regexp_replace(text, pattern, replacement)
regexp_replace(text, pattern, replacement, flags)

Replaces the first match of the pattern with replacement. With the flag 'g' (global), it replaces every match.

ArgumentTypeMeaning
textSTRING (a number is turned into text)the text to change
patternSTRINGthe regular expression
replacementSTRINGthe new text. \1 to \9 = the text of group 1 to 9; \0 or \& = the whole match; a backslash written two times = one backslash
flagsSTRINGany mix of 'g' every match, 'i' ignore case, 'c' case must match. Example: 'gi'

Example

SELECT regexp_replace('a1b2c3', '\d', '#') AS first_only, regexp_replace('a1b2c3', '\d', '#', 'g') AS every, regexp_replace('City CITY', 'city', 'X', 'gi') AS ignore_case, regexp_replace('CITY', 'x', 'y') AS no_match;

Result:

first_only (STRING)every (STRING)ignore_case (STRING)no_match (STRING)
a#b2c3a#b#c#X XCITY

(1 row)

Example: Clean spaces in text a user typed

SELECT '[' || regexp_replace(trim('  City    Bank   Limited '), '\s+', ' ', 'g') || ']' AS cleaned;

Result:

cleaned (STRING)
[City Bank Limited]

(1 row)

Example: Groups: turn 2082/83 into 2082-2083

SELECT regexp_replace('FY 2082/83', '(\d{2})(\d{2})/(\d{2})', '\1\2-\1\3') AS fiscal_year,
       regexp_replace('CITY', '.', '[\&]', 'g') AS whole_match;

Result:

fiscal_year (STRING)whole_match (STRING)
FY 2082-2083[C][I][T][Y]

(1 row)

Example: A URL part (slug) from a news title, and text without HTML tags

SELECT lower(trim(regexp_replace('City Bank: Q4 Results!', '[^a-z0-9]+', '-', 'gi'), '-')) AS slug,
       regexp_replace('<p>Index <b>closes</b> at 2,840</p>', '<[^>]*>', '', 'g') AS plain_text,
       CAST(regexp_replace('$1,234,567', '[^0-9]', '', 'g') AS LONG) AS amount;

Result:

slug (STRING)plain_text (STRING)amount (LONG)
city-bank-q4-resultsIndex closes at 2,8401234567

(1 row)

Example: Unknown flag (shows an error)

SELECT regexp_replace('a1b2', '\d', '#', 'x');

Result:

Error: line 1, column 8: regexp_replace: unknown flag 'x'; use 'i' to ignore case and 'g' to replace every match

Flags

Syntax:

regexp_like(text, pattern, 'i')
regexp_replace(text, pattern, replacement, 'gi')

The last argument of the regexp functions is a text of flags. Each letter is one flag.

FlagMeaningAllowed in
iignore upper and lower case (English letters A-Z only)all four functions
ccase must match (this is the default)all four functions
greplace every match, not only the firstonly regexp_replace

Example

SELECT regexp_like('market', 'MARKET') AS default_case, regexp_like('market', 'MARKET', 'i') AS i_flag, regexp_like('market', 'MARKET', 'ic') AS last_wins, regexp_replace('a-b-c', '-', '+', 'g') AS g_flag;

Result:

default_case (BOOL)i_flag (BOOL)last_wins (BOOL)g_flag (STRING)
falsetruefalsea+b+c

(1 row)

Example: The flag g only works in regexp_replace (shows an error)

SELECT regexp_like('CITY', 'T', 'g');

Result:

Error: line 1, column 8: regexp_like: unknown flag 'g'; use 'i' to ignore case

regexp_match

Syntax:

regexp_match(text, pattern)
regexp_match(text, pattern, flags)

The first match, as PostgreSQL gives it: an array of what each (...) group caught, or of the whole match when the pattern has no groups. NULL when nothing matches.

ArgumentTypeMeaning
textSTRINGthe text to search
patternSTRINGthe regular expression
flagsSTRINGoptional PostgreSQL flags; g is refused (use regexp_matches)

Example

SELECT regexp_match('abc123', '(\d+)') AS groups,
       (regexp_match('abc123', '(\d+)'))[1] AS digits,
       regexp_match('foobarbequebaz', 'bar.*que') AS whole,
       regexp_match('abc', 'x') AS no_match;

Result:

groups (TEXT[])digits (STRING)whole (TEXT[])no_match (TEXT[])
["123"]123["barbeque"]NULL

(1 row)

Example: Split a log line into its parts

SELECT m[1] AS method, m[2] AS path, m[3]::int AS status
FROM (SELECT regexp_match('GET /api/quotes 200', '^(\w+) (\S+) (\d+)$') AS m) t;

Result:

method (STRING)path (STRING)status (INT)
GET/api/quotes200

(1 row)

regexp_matches

Syntax:

regexp_matches(text, pattern)
regexp_matches(text, pattern, flags)

Like regexp_match, but it gives rows: one row (an array) per match with the flag g, at most one without it, and no row when nothing matches.

Example

SELECT m[1] AS key, m[2] AS value
FROM regexp_matches('k1=v1, k2=v2', '(\w+)=(\w+)', 'g') AS m;

Result:

key (STRING)value (STRING)
k1v1
k2v2

(2 rows)

Example

SELECT count(*) AS digits FROM regexp_matches('a1b2c3', '\d', 'g');

Result:

digits (LONG)
3

(1 row)

regexp_split_to_array, regexp_split_to_table

Syntax:

regexp_split_to_array(text, pattern [, flags])
regexp_split_to_table(text, pattern [, flags])

Cuts the text wherever the pattern matches. _to_array gives one list, _to_table one row per piece.

Example

SELECT regexp_split_to_array('a,b;c', '[,;]') AS parts,
       array_length(regexp_split_to_array('a,b;c', '[,;]'), 1) AS how_many;

Result:

parts (TEXT[])how_many (LONG)
["a","b","c"]3

(1 row)

Example

SELECT word FROM regexp_split_to_table('the quick  brown fox', '\s+') AS t(word);

Result:

word (STRING)
the
quick
brown
fox

(4 rows)

PostgreSQL flags

regexp_match, regexp_matches and the split functions take PostgreSQL's flag letters:

FlagMeaning
gevery match (regexp_matches only)
i / cignore case / case must match; the last one wins
qthe pattern is plain text: . is a dot
s, taccepted: what KDB always does (. matches a line break, no spaces-ignored syntax)

Example

SELECT regexp_match('ABC', 'b', 'i') AS i_flag, regexp_match('a.c', '.', 'q') AS q_flag;

Result:

i_flag (TEXT[])q_flag (TEXT[])
["B"]["."]

(1 row)

Regular expression syntax

Syntax:

.   [abc]   [^abc]   [a-z]   [[:alpha:]]   \d \w \s \D \W \S
^   $   \b   \B
(group)   (?:group)   a|b
*   +   ?   {n}   {n,}   {n,m}   *?   +?   ??   {n,m}?
\.  \(  \n  \t  \r  \f  \v  \uXXXX

What you can write in a pattern. One character is one letter, also a Nepali letter or sign.

You writeIt matches
a, नthat character
.any one character, also a line break
[abc]one of a, b, c
[^abc]one character that is not a, b or c
[a-z], [०-९], [ऀ-ॿ]one character in the range (the last one is every Devanagari character)
[[:alpha:]] [[:digit:]] [[:alnum:]] [[:upper:]] [[:lower:]] [[:space:]] [[:punct:]]English letters, digits 0-9, both, capitals, small letters, spaces, punctuation (only inside [ ])
\d / \Da digit 0-9 / not a digit
\w / \Wa word character: letters of any language (also Nepali letters, signs and digits ०-९), 0-9 and _ / not one
\s / \Sa space, tab or line break / not one
^ / $the start / the end of the whole text
\b / \Ba word edge / not a word edge
(...)a group with a number (for \1 in regexp_replace)
(?:...)a group without a number
a|ba or b; the first one that matches wins (like Perl, Python and JavaScript)
* + ?0 or more, 1 or more, 0 or 1 times (as many as possible)
{n} {n,} {n,m}exactly n, n or more, n to m times
*? +? ?? {n,m}?the same, but as few as possible (lazy)
\. \( \[ \$ ...the punctuation character itself (a backslash written two times is one backslash)
\n \t \r \f \vline break, tab, carriage return, form feed, vertical tab
\u and 4 hex digitsthe character with that Unicode number

Not supported (you get a clear error):

You writeInstead
back references in the pattern: (a)\1not possible; \1 works only in the replacement of regexp_replace
look-ahead and look-behind: (?=...) (?!...) (?<=...)match more text, then cut it with a group in regexp_replace
named groups (?P<name>...), flags in the pattern (?i)numbered groups; the flags argument
\p{L} and other Unicode classes\w, or a range like [ऀ-ॿ]
\A \z \Z^ and $ (they always mean the start and end of the whole text)
[[:word:]], [[:xdigit:]] and other classes not in the table[\w], [0-9a-fA-F]

Example: Nepali and English digits

SELECT regexp_like('१८', '\d') AS d_nepali, regexp_like('18', '\d') AS d_english,
       regexp_substr('नाफा १८ प्रतिशत', '[०-९]+') AS nepali_number,
       regexp_count('सिटी बैंकको नाफा १८ प्रतिशतले बढ्यो।', '\w+') AS nepali_words;

Result:

d_nepali (BOOL)d_english (BOOL)nepali_number (STRING)nepali_words (LONG)
falsetrue१८6

(1 row)

Example: Greedy and lazy, and the first alternative wins

SELECT regexp_substr('<b>CITY</b> and <b>COAST</b>', '<b>.*</b>') AS greedy,
       regexp_substr('<b>CITY</b> and <b>COAST</b>', '<b>.*?</b>') AS lazy,
       regexp_substr('CITY', 'CI|CITY') AS first_alternative,
       regexp_like('CITY Bank', '\bBank') AS word_edge,
       regexp_like('CITY Bank', '\bITY') AS no_word_edge;

Result:

greedy (STRING)lazy (STRING)first_alternative (STRING)word_edge (BOOL)no_word_edge (BOOL)
CITY and COASTCITYCItruefalse

(1 row)

Example: ^ and $ are the start and end of the whole text

SELECT regexp_like('line one
line two', '^line two') AS start_of_line_2,
       regexp_like('line one
line two', 'one.line') AS dot_matches_line_break;

Result:

start_of_line_2 (BOOL)dot_matches_line_break (BOOL)
falsetrue

(1 row)

Example: Back references are not supported (shows an error)

SELECT regexp_like('CITY CITY', '(\w+) \1');

Result:

Error: line 1, column 8: regexp_like: regular expression '(\w+) \1': back references like \1 are not supported in a pattern

Example: A broken pattern (shows an error)

SELECT regexp_substr('CITY', '(CI');

Result:

Error: line 1, column 8: regexp_substr: regular expression '(CI': '(' has no matching ')'

Example: Counts above 1000 are refused (shows an error)

SELECT regexp_like('CITY', 'A{2000}');

Result:

Error: line 1, column 8: regexp_like: regular expression 'A{2000}': a count in {n,m} is larger than 1000