Обычная функция REPLACE хороша, когда мы точно знаем, какую подстроку нужно заменить.
Например:
SELECT REPLACE('hello world', 'world', 'SQL') AS result;
Результат:
hello SQL
Но в реальных данных часто всё грязнее. Телефон может быть записан так:
+1 (555) 123-45-67
или так:
1-555-123-4567
или так:
555 123 45 67
Если нужно оставить только цифры, обычный REPLACE уже неудобен. Придётся отдельно удалять пробелы, скобки, плюсы, дефисы и другие символы.
Вот здесь и помогает REGEXP_REPLACE.
Эта функция заменяет не конкретный кусок текста, а всё, что подходит под регулярное выражение.
Например:
SELECT REGEXP_REPLACE('+1 (555) 123-45-67', '[^0-9]', '', 'g') AS digits;
Результат:
15551234567
Мы сказали базе: «Найди все символы, которые не являются цифрами, и замени их на пустую строку». В итоге от телефона остались только цифры.
Чем REGEXP_REPLACE отличается от REPLACE
REPLACE ищет точное совпадение.
SELECT REPLACE('a-b-c', '-', '+') AS result;
Результат:
a+b+c
Здесь всё просто: каждый дефис заменили на плюс.
А REGEXP_REPLACE ищет совпадение по шаблону.
Например:
SELECT REGEXP_REPLACE('a1b2c3', '[0-9]', '', 'g') AS result;
Результат:
abc
Шаблон [0-9] означает «любая цифра от 0 до 9». Поэтому функция нашла 1, 2, 3 и удалила их.
Разница такая:
REPLACE -> заменить конкретный текст
REGEXP_REPLACE -> заменить всё, что подходит под шаблон
Если нужно заменить точную строку — берите REPLACE.
Если нужно заменить класс символов, разные варианты написания или сложный шаблон — берите REGEXP_REPLACE.
Базовый синтаксис в PostgreSQL
В PostgreSQL базовая форма выглядит так:
REGEXP_REPLACE(source, pattern, replacement [, flags])
Где:
source — исходная строка;
pattern — регулярное выражение, то есть шаблон поиска;
replacement — на что заменить найденный текст;
flags — дополнительные флаги, например g или i.
Простой пример:
SELECT REGEXP_REPLACE('hello world', '\s+', ' ', 'g') AS result;
Результат:
hello world
Здесь шаблон \s+ означает «один или несколько пробельных символов подряд». Мы заменили любую такую группу на один обычный пробел.
То есть строка:
hello world
превратилась в:
hello world
Самый важный флаг: g
В PostgreSQL у REGEXP_REPLACE есть важная особенность: без флага g функция заменяет только первое совпадение.
Например:
SELECT REGEXP_REPLACE('a-b-c', '-', '+') AS result;
Результат:
a+b-c
Заменился только первый дефис.
Чтобы заменить все дефисы, нужно добавить флаг g:
SELECT REGEXP_REPLACE('a-b-c', '-', '+', 'g') AS result;
Результат:
a+b+c
g означает global, то есть «по всей строке».
Для новичка это, пожалуй, главная ловушка. Если вы чистите телефоны, пробелы, мусорные символы или повторяющиеся разделители, почти всегда нужен флаг g.
Плохой пример:
SELECT REGEXP_REPLACE('+1 (555) 123-45-67', '[^0-9]', '') AS digits;
Результат будет не таким, как хотелось бы:
1 (555) 123-45-67
Удалился только первый символ, который не является цифрой, то есть +.
Правильный вариант:
SELECT REGEXP_REPLACE('+1 (555) 123-45-67', '[^0-9]', '', 'g') AS digits;
Результат:
15551234567
Удаление лишних символов из телефона
Один из самых понятных примеров — нормализация телефона.
В таблице могут быть такие значения:
phone
--------------------
+1 (555) 123-45-67
1-555-123-4567
555 123 45 67
Если нам нужна только последовательность цифр, можно написать:
SELECT
phone,
REGEXP_REPLACE(phone, '[^0-9]', '', 'g') AS digits_only
FROM users;
Результат:
phone | digits_only
---------------------+-------------
+1 (555) 123-45-67 | 15551234567
1-555-123-4567 | 15551234567
555 123 45 67 | 5551234567
Шаблон '[^0-9]' означает «любой символ, кроме цифры».
А пустая строка в замене '' означает «удалить найденное».
Получается простое правило: чтобы оставить только цифры, удаляем всё, что не цифра.
Схлопывание пробелов
Второй частый сценарий — убрать лишние пробелы.
Например, пользователь ввёл имя так:
Ivan Petrov
Внутри много лишних пробелов. Сначала можно схлопнуть несколько пробелов подряд в один:
SELECT REGEXP_REPLACE('Ivan Petrov', '\s+', ' ', 'g') AS clean_name;
Результат:
Ivan Petrov
Шаблон '\s+' означает «один или несколько пробельных символов подряд».
Это не только обычный пробел. В зависимости от данных туда могут попадать табы и другие пробельные символы.
Если нужно ещё убрать пробелы по краям строки, добавьте TRIM:
SELECT TRIM(REGEXP_REPLACE(' Ivan Petrov ', '\s+', ' ', 'g')) AS clean_name;
Результат:
Ivan Petrov
Здесь сначала REGEXP_REPLACE превращает все группы пробелов в один пробел, а потом TRIM убирает пробелы в начале и конце строки.
Очистка имён
Допустим, в таблице есть имена, куда случайно попали лишние пробелы:
name
----------------
Ivan Petrov
Maria Ivanova
Alex Smirnov
Можно привести их к более аккуратному виду:
SELECT
name AS old_name,
TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g')) AS clean_name
FROM users;
Результат:
old_name | clean_name
-----------------+--------------
Ivan Petrov | Ivan Petrov
Maria Ivanova | Maria Ivanova
Alex Smirnov | Alex Smirnov
Это хороший пример, где REGEXP_REPLACE не меняет смысл данных, а просто убирает технический мусор.
Но перед массовым UPDATE всё равно лучше сначала посмотреть результат через SELECT.
Перед UPDATE сначала сделайте SELECT
Если вы хотите почистить данные в таблице, не начинайте сразу с обновления.
Плохая идея:
UPDATE users
SET name = TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g'));
Такой запрос сразу перепишет данные. Если шаблон оказался неправильным, можно испортить много строк.
Лучше сначала посмотреть старое и новое значение рядом:
SELECT
id,
name AS old_name,
TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g')) AS new_name
FROM users
WHERE name <> TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g'))
ORDER BY id;
Так вы увидите, какие строки реально изменятся.
И только после проверки можно делать UPDATE:
UPDATE users
SET name = TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g'))
WHERE name <> TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g'));
Это простая, но очень важная привычка при чистке данных.
Флаг i: замена без учёта регистра
Флаг i включает поиск без учёта регистра.
Например, нужно привести все варианты gmail к одному виду.
В данных могут быть такие email:
alex@GMAIL.com
maria@gmail.com
ivan@Gmail.com
Запрос:
SELECT
email,
REGEXP_REPLACE(email, 'gmail', 'gmail', 'gi') AS normalized_email
FROM users;
Здесь флаги:
g -> заменить все совпадения
i -> искать без учёта регистра
Результат:
email | normalized_email
-----------------+------------------
alex@GMAIL.com | alex@gmail.com
maria@gmail.com | maria@gmail.com
ivan@Gmail.com | ivan@gmail.com
Флаги можно комбинировать в одной строке: 'gi'.
Порядок обычно не важен: gi и ig читаются одинаково.
Флаг i на простом примере
Без флага i регистр важен:
SELECT REGEXP_REPLACE('SQL sql Sql', 'sql', 'DB', 'g') AS result;
Результат:
SQL DB Sql
Заменилось только sql в нижнем регистре.
С флагом i:
SELECT REGEXP_REPLACE('SQL sql Sql', 'sql', 'DB', 'gi') AS result;
Результат:
DB DB DB
Теперь совпали все варианты: SQL, sql и Sql.
Что происходит, если совпадений нет
Если шаблон ничего не нашёл, REGEXP_REPLACE возвращает исходную строку без изменений.
SELECT REGEXP_REPLACE('hello', '[0-9]', '', 'g') AS result;
Результат:
hello
Цифр в строке нет, поэтому удалять нечего.
Это важный момент: функция не вернёт NULL только потому, что совпадений не было. Она просто оставит строку как есть.
А вот если сама исходная строка равна NULL, результат будет NULL:
SELECT REGEXP_REPLACE(NULL, '[0-9]', '', 'g') AS result;
Результат:
NULL
Если нужно считать NULL пустой строкой, используйте COALESCE:
SELECT REGEXP_REPLACE(COALESCE(phone, ''), '[^0-9]', '', 'g') AS digits_only
FROM users;
Обратные ссылки: как переставлять части строки
REGEXP_REPLACE умеет не только удалять и заменять текст. Она может захватывать части строки и использовать их в замене.
Для этого в регулярном выражении используют круглые скобки.
Например:
SELECT REGEXP_REPLACE('Ada Lovelace', '^(\w+)\s+(\w+)$', '\2, \1') AS result;
Результат:
Lovelace, Ada
Разберём шаблон '^(\w+)\s+(\w+)$':
^ — начало строки;
(\w+) — первая группа: первое слово;
\s+ — один или несколько пробелов;
(\w+) — вторая группа: второе слово;
$ — конец строки.
Для строки:
Ada Lovelace
первая группа — это Ada, вторая группа — это Lovelace.
В замене мы пишем '\2, \1'.
Это значит: сначала подставь вторую группу, потом запятую и пробел, потом первую группу.
Получается:
Lovelace, Ada
Группы нумеруются слева направо
Порядок групп зависит от порядка открывающих скобок в шаблоне.
В шаблоне '^(\w+)\s+(\w+)$':
\1 -> первое слово
\2 -> второе слово
Если бы групп было три, появились бы \3, \4 и так далее.
Пример:
SELECT REGEXP_REPLACE('2026-06-19', '^([0-9]{4})-([0-9]{2})-([0-9]{2})$', '\3.\2.\1') AS result;
Результат:
19.06.2026
Группы такие:
\1 -> 2026
\2 -> 06
\3 -> 19
В замене мы переставили их в формат «день.месяц.год».
Форматирование телефона через группы
Допустим, у нас уже есть телефон только из цифр:
5551234567
Хотим получить красивый формат:
(555) 123-4567
Запрос:
SELECT REGEXP_REPLACE(
'5551234567',
'^([0-9]{3})([0-9]{3})([0-9]{4})$',
'(\1) \2-\3'
) AS phone_pretty;
Результат:
(555) 123-4567
Разберём шаблон '^([0-9]{3})([0-9]{3})([0-9]{4})$':
- первая группа
([0-9]{3}) — первые 3 цифры;
- вторая группа
([0-9]{3}) — следующие 3 цифры;
- третья группа
([0-9]{4}) — последние 4 цифры.
А замена '(\1) \2-\3' собирает номер в нужном виде.
Важно: такой запрос сработает только для строки ровно из 10 цифр. Если цифр больше или меньше, шаблон не совпадёт, и строка останется как есть.
Поэтому для реальной чистки телефона часто делают два шага:
WITH cleaned AS (
SELECT
phone,
REGEXP_REPLACE(phone, '[^0-9]', '', 'g') AS digits
FROM users
)
SELECT
phone,
digits,
CASE
WHEN digits ~ '^[0-9]{10}$'
THEN REGEXP_REPLACE(digits, '^([0-9]{3})([0-9]{3})([0-9]{4})$', '(\1) \2-\3')
ELSE digits
END AS phone_pretty
FROM cleaned;
Сначала оставляем только цифры, потом форматируем только те строки, где получилось ровно 10 цифр.
Подстановка всего совпадения
В PostgreSQL в строке замены можно использовать не только \1, \2, \3, но и \&.
Запись \& означает всё найденное совпадение целиком.
Например, хотим обернуть все числа в квадратные скобки:
SELECT REGEXP_REPLACE('Order 42, user 105', '[0-9]+', '[\&]', 'g') AS result;
Результат:
Order [42], user [105]
Шаблон [0-9]+ нашёл числа 42 и 105.
А замена [\&] сказала: возьми всё найденное число и поставь вокруг него квадратные скобки.
Это удобно, когда не нужно разбивать совпадение на группы, а нужно просто обернуть или дополнить найденный фрагмент.
Многострочный текст и границы строк
В регулярных выражениях символы ^ и $ обычно означают начало и конец строки.
Например:
SELECT REGEXP_REPLACE('error: failed', '^error', 'warning') AS result;
Результат:
warning: failed
^error означает: слово error должно быть в начале строки.
Но если у вас в одной строке хранится многострочный текст, возникает вопрос: считать ли началом только начало всего текста или начало каждой строки внутри него?
В PostgreSQL для этого есть флаги, связанные с режимом обработки переводов строк. В практических статьях чаще всего достаточно запомнить идею: если вы работаете с многострочным текстом и используете ^ или $, обязательно проверьте поведение на примере с переводами строк.
Например, возьмите такой текст:
error: first
ok: second
error: third
И отдельно проверьте, заменяются ли error только в начале всего текста или в начале каждой строки.
Для большинства задач чистки телефонов, имён, email и пробелов многострочный режим не нужен. Он важен, когда вы обрабатываете большие текстовые поля, логи, сообщения или документы.
POSIX-классы символов
В PostgreSQL регулярные выражения используют POSIX-синтаксис. Поэтому можно писать классы символов в таком виде:
SELECT REGEXP_REPLACE('abc123', '[[:digit:]]', '', 'g');
Класс [[:digit:]] — это цифра, [[:alpha:]] — буква, [[:alnum:]] — буква или цифра.
Например, удалить всё, кроме букв и цифр:
SELECT REGEXP_REPLACE('User #42!', '[^[:alnum:]]', '', 'g') AS result;
Результат:
User42
Для новичка проще сначала использовать знакомые конструкции: [0-9], [^0-9], \s+, \w+.
Но POSIX-классы полезны, когда хочется написать более переносимый или более явный шаблон внутри PostgreSQL.
Частая ошибка: забыли флаг g
Самая частая ошибка с REGEXP_REPLACE в PostgreSQL — забыть флаг g.
Например:
SELECT REGEXP_REPLACE('a b c', '\s+', ' ') AS result;
Результат:
a b c
Схлопнулась только первая группа пробелов.
Правильно:
SELECT REGEXP_REPLACE('a b c', '\s+', ' ', 'g') AS result;
Результат:
a b c
Если вы ожидаете, что заменятся все совпадения, почти всегда добавляйте g.
Частая ошибка: слишком широкий шаблон
Регулярные выражения мощные, но из-за этого ими легко удалить больше, чем нужно.
Например, кажется, что такой шаблон удалит HTML-теги:
SELECT REGEXP_REPLACE('<b>Hello</b> <i>SQL</i>', '<.*>', '', 'g') AS result;
Но результат может неприятно удивить:
Почему? Потому что .* может захватить слишком много: от первого < до последнего >.
Осторожнее можно написать так:
SELECT REGEXP_REPLACE('<b>Hello</b> <i>SQL</i>', '<[^>]*>', '', 'g') AS result;
Результат:
Hello SQL
Шаблон <[^>]*> означает:
< — открывающая угловая скобка;
[^>]* — любые символы, кроме закрывающей угловой скобки;
> — закрывающая угловая скобка.
Но даже это не полноценный HTML-парсер. Для сложного HTML лучше использовать специальные инструменты на уровне приложения. REGEXP_REPLACE подходит для простой технической чистки, а не для разбора сложных языков разметки.
Частая ошибка: регулярка вместо простой функции
Не нужно использовать REGEXP_REPLACE там, где достаточно обычного REPLACE.
Например, если нужно заменить все дефисы на пробелы:
SELECT REPLACE('a-b-c', '-', ' ') AS result;
Это проще и понятнее, чем:
SELECT REGEXP_REPLACE('a-b-c', '-', ' ', 'g') AS result;
Обычная замена быстрее читается и обычно проще для базы.
Простое правило:
Точная подстрока -> REPLACE
Шаблон или класс символов -> REGEXP_REPLACE
REGEXP_REPLACE и производительность
Регулярные выражения удобные, но не бесплатные. Базе нужно разобрать шаблон и проверить строку.
На маленьких таблицах это не страшно. Но на большой таблице запрос вида:
SELECT *
FROM users
WHERE REGEXP_REPLACE(phone, '[^0-9]', '', 'g') = '15551234567';
может быть тяжёлым.
Почему? Потому что база должна вычислить REGEXP_REPLACE(phone, ...) для множества строк, а обычный индекс по phone здесь обычно не поможет.
Лучше хранить нормализованное значение отдельно, если оно нужно для поиска постоянно.
Например:
ALTER TABLE users
ADD COLUMN phone_digits text;
Потом заполнить:
UPDATE users
SET phone_digits = REGEXP_REPLACE(phone, '[^0-9]', '', 'g');
И уже искать так:
SELECT *
FROM users
WHERE phone_digits = '15551234567';
А на phone_digits можно построить обычный индекс.
В PostgreSQL также можно использовать функциональный индекс по выражению, если вы действительно хотите искать именно так:
CREATE INDEX idx_users_phone_digits
ON users (REGEXP_REPLACE(phone, '[^0-9]', '', 'g'));
Но создавать такие индексы стоит только под реальные частые запросы.
REGEXP_REPLACE в CHECK и generated column
Если правило очистки или нормализации повторяется в разных местах, его лучше не копировать руками в каждый запрос.
Например, если вам постоянно нужны только цифры телефона, можно сделать отдельную колонку или generated column.
Идея такая:
phone -> как ввёл пользователь
phone_digits -> только цифры
Так вы отделяете исходные данные от нормализованного вида.
Это особенно удобно, если:
- по нормализованному телефону нужно искать;
- нужно проверять уникальность;
- данные приходят из разных источников;
- отчёты и аналитика используют уже очищенное значение.
REGEXP_REPLACE отлично подходит для входа в такой пайплайн, но не всегда должен выполняться заново в каждом отчёте.
Разница между PostgreSQL, MySQL и ClickHouse
Идея везде похожая: есть строка, есть регулярное выражение, есть замена.
Но детали отличаются.
PostgreSQL
В PostgreSQL форма такая:
REGEXP_REPLACE(source, pattern, replacement [, flags]);
Пример:
SELECT REGEXP_REPLACE('+1 (555) 123-45-67', '[^0-9]', '', 'g') AS digits;
Результат:
15551234567
Главное:
- без
g заменяется только первое совпадение;
g включает замену всех совпадений;
i включает поиск без учёта регистра;
- обратные ссылки в замене пишутся как
\1, \2, \3;
\& означает всё совпадение целиком.
MySQL
В MySQL 8+ функция тоже называется REGEXP_REPLACE, но сигнатура другая:
REGEXP_REPLACE(expr, pattern, replacement [, pos [, occurrence [, match_type]]]);
Пример удаления всего, кроме цифр:
SELECT REGEXP_REPLACE(phone, '[^0-9]', '') AS digits
FROM users;
В MySQL глобальная замена обычно идёт по умолчанию, потому что occurrence = 0 означает заменить все совпадения.
Если нужно явно указать режим без учёта регистра, используют match_type:
SELECT REGEXP_REPLACE(email, 'gmail', 'gmail', 1, 0, 'i') AS normalized_email
FROM users;
Здесь:
1 — начинать поиск с первой позиции;
0 — заменить все совпадения;
'i' — искать без учёта регистра.
С обратными ссылками в MySQL будьте особенно внимательны: синтаксис и экранирование могут отличаться от PostgreSQL. В примерах и окружениях часто встречается формат $1, $2, а в строковых литералах с обратными слэшами может понадобиться дополнительное экранирование. Перед переносом запроса с группами обязательно проверьте его на маленьком примере.
ClickHouse
В ClickHouse для регулярной замены часто используют две функции: replaceRegexpOne и replaceRegexpAll.
replaceRegexpOne заменяет первое совпадение.
replaceRegexpAll заменяет все совпадения.
Пример:
SELECT replaceRegexpAll(phone, '[^0-9]', '') AS digits
FROM users;
Если нужно заменить только первое совпадение:
SELECT replaceRegexpOne('a-b-c', '-', '+') AS result;
Результат:
a+b-c
Если нужно заменить все:
SELECT replaceRegexpAll('a-b-c', '-', '+') AS result;
Результат:
a+b+c
В ClickHouse глобальность видна прямо из имени функции. Это удобно: не нужно помнить про флаг g, как в PostgreSQL.
Что тестировать перед переносом между СУБД
Если вы переносите регулярную замену между PostgreSQL, MySQL и ClickHouse, не проверяйте её только на одной красивой строке.
Соберите маленький набор тестов:
+1 (555) 123-45-67
555 123 45 67
hello world
SQL sql Sql
Ada Lovelace
NULL
И проверьте:
- что происходит с пустой строкой;
- что происходит с
NULL;
- заменяется первое совпадение или все;
- как включается поиск без учёта регистра;
- как работают группы;
- как пишутся обратные ссылки;
- нужно ли экранировать обратные слэши;
- не удаляет ли шаблон лишний текст.
Особенно осторожно переносите запросы с группами и обратными ссылками. Именно там различия между СУБД чаще всего приводят к неожиданному результату.
Практический пример: чистим телефон и проверяем длину
Допустим, телефоны должны храниться в любом виде, но для проверки нам нужны только цифры.
SELECT
id,
phone,
REGEXP_REPLACE(phone, '[^0-9]', '', 'g') AS digits
FROM users;
Результат:
id | phone | digits
---+----------------------+-------------
1 | +1 (555) 123-45-67 | 15551234567
2 | 555 123 45 67 | 5551234567
3 | abc |
Теперь можно найти подозрительные телефоны:
SELECT
id,
phone,
REGEXP_REPLACE(phone, '[^0-9]', '', 'g') AS digits
FROM users
WHERE char_length(REGEXP_REPLACE(phone, '[^0-9]', '', 'g')) NOT BETWEEN 10 AND 15;
Такой запрос полезен для аудита данных.
Но если такая проверка нужна постоянно, лучше вынести очищенный телефон в отдельную колонку, чтобы не считать регулярку каждый раз.
Практический пример: нормализуем пробелы в именах
SELECT
id,
name AS old_name,
TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g')) AS new_name
FROM users
WHERE name <> TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g'));
Так можно найти строки, где есть лишние пробелы.
После проверки:
UPDATE users
SET name = TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g'))
WHERE name <> TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g'));
Это нормальный сценарий для разовой чистки данных после импорта.
Практический пример: меняем формат даты
Есть строка:
2026-06-19
Хотим получить:
19.06.2026
Запрос:
SELECT REGEXP_REPLACE(
'2026-06-19',
'^([0-9]{4})-([0-9]{2})-([0-9]{2})$',
'\3.\2.\1'
) AS formatted_date;
Результат:
19.06.2026
Этот пример хорошо показывает силу групп: мы не просто заменили текст, а переставили части строки местами.
Но если в колонке хранится настоящий тип date, лучше форматировать дату средствами работы с датами, а не регулярками. Регулярные выражения здесь уместны только если дата пришла как текст.
Практический пример: удалить всё, кроме букв и цифр
Допустим, нужно получить простой код из строки:
User #42!
Оставим только буквы и цифры:
SELECT REGEXP_REPLACE('User #42!', '[^[:alnum:]]', '', 'g') AS code;
Результат:
User42
Такой приём используют для технических кодов, slug-подготовки или очистки импортированных значений.
Но для полноценной генерации URL-slug обычно нужна более сложная логика: транслитерация, нижний регистр, замена пробелов на дефисы, обработка дублей. Одного REGEXP_REPLACE может быть мало.
Коротко
REGEXP_REPLACE — это функция для замены текста по регулярному выражению.
В PostgreSQL базовый синтаксис такой:
REGEXP_REPLACE(source, pattern, replacement [, flags]);
Пример:
SELECT REGEXP_REPLACE('+1 (555) 123-45-67', '[^0-9]', '', 'g');
Результат:
15551234567
Главные правила:
REGEXP_REPLACE ищет не точную строку, а шаблон;
- для точной замены проще использовать обычный
REPLACE;
- в PostgreSQL без флага
g заменяется только первое совпадение;
- флаг
g заменяет все совпадения;
- флаг
i включает поиск без учёта регистра;
- пустая строка
'' в замене удаляет найденный текст;
- если совпадений нет, исходная строка возвращается без изменений;
- если исходная строка
NULL, результат тоже NULL;
- группы в круглых скобках можно использовать в замене через
\1, \2, \3;
\& в PostgreSQL означает всё совпадение целиком;
- регулярные выражения удобно использовать для чистки телефонов, пробелов, кодов и текстовых импортов;
- для больших таблиц регулярки в
WHERE могут быть дорогими;
- часто используемые нормализованные значения лучше хранить отдельно или индексировать выражение;
- при переносе между PostgreSQL, MySQL и ClickHouse проверяйте флаги, глобальность, экранирование и обратные ссылки.
Самое простое правило: если нужно заменить конкретный кусок текста — используйте REPLACE. Если нужно заменить «все цифры», «все лишние пробелы», «всё, кроме букв», «любой вариант регистра» или переставить части строки по группам — используйте REGEXP_REPLACE.
Обычная функция
REPLACEхороша, когда мы точно знаем, какую подстроку нужно заменить.Например:
SELECT REPLACE('hello world', 'world', 'SQL') AS result;Результат:
Но в реальных данных часто всё грязнее. Телефон может быть записан так:
или так:
или так:
Если нужно оставить только цифры, обычный
REPLACEуже неудобен. Придётся отдельно удалять пробелы, скобки, плюсы, дефисы и другие символы.Вот здесь и помогает
REGEXP_REPLACE.Эта функция заменяет не конкретный кусок текста, а всё, что подходит под регулярное выражение.
Например:
SELECT REGEXP_REPLACE('+1 (555) 123-45-67', '[^0-9]', '', 'g') AS digits;Результат:
Мы сказали базе: «Найди все символы, которые не являются цифрами, и замени их на пустую строку». В итоге от телефона остались только цифры.
Чем REGEXP_REPLACE отличается от REPLACE
REPLACEищет точное совпадение.SELECT REPLACE('a-b-c', '-', '+') AS result;Результат:
Здесь всё просто: каждый дефис заменили на плюс.
А
REGEXP_REPLACEищет совпадение по шаблону.Например:
SELECT REGEXP_REPLACE('a1b2c3', '[0-9]', '', 'g') AS result;Результат:
Шаблон
[0-9]означает «любая цифра от 0 до 9». Поэтому функция нашла1,2,3и удалила их.Разница такая:
Если нужно заменить точную строку — берите
REPLACE.Если нужно заменить класс символов, разные варианты написания или сложный шаблон — берите
REGEXP_REPLACE.Базовый синтаксис в PostgreSQL
В PostgreSQL базовая форма выглядит так:
REGEXP_REPLACE(source, pattern, replacement [, flags])Где:
source— исходная строка;pattern— регулярное выражение, то есть шаблон поиска;replacement— на что заменить найденный текст;flags— дополнительные флаги, напримерgилиi.Простой пример:
SELECT REGEXP_REPLACE('hello world', '\s+', ' ', 'g') AS result;Результат:
Здесь шаблон
\s+означает «один или несколько пробельных символов подряд». Мы заменили любую такую группу на один обычный пробел.То есть строка:
превратилась в:
Самый важный флаг: g
В PostgreSQL у
REGEXP_REPLACEесть важная особенность: без флагаgфункция заменяет только первое совпадение.Например:
SELECT REGEXP_REPLACE('a-b-c', '-', '+') AS result;Результат:
Заменился только первый дефис.
Чтобы заменить все дефисы, нужно добавить флаг
g:SELECT REGEXP_REPLACE('a-b-c', '-', '+', 'g') AS result;Результат:
gозначает global, то есть «по всей строке».Для новичка это, пожалуй, главная ловушка. Если вы чистите телефоны, пробелы, мусорные символы или повторяющиеся разделители, почти всегда нужен флаг
g.Плохой пример:
SELECT REGEXP_REPLACE('+1 (555) 123-45-67', '[^0-9]', '') AS digits;Результат будет не таким, как хотелось бы:
Удалился только первый символ, который не является цифрой, то есть
+.Правильный вариант:
SELECT REGEXP_REPLACE('+1 (555) 123-45-67', '[^0-9]', '', 'g') AS digits;Результат:
Удаление лишних символов из телефона
Один из самых понятных примеров — нормализация телефона.
В таблице могут быть такие значения:
Если нам нужна только последовательность цифр, можно написать:
SELECT phone, REGEXP_REPLACE(phone, '[^0-9]', '', 'g') AS digits_only FROM users;Результат:
Шаблон
'[^0-9]'означает «любой символ, кроме цифры».А пустая строка в замене
''означает «удалить найденное».Получается простое правило: чтобы оставить только цифры, удаляем всё, что не цифра.
Схлопывание пробелов
Второй частый сценарий — убрать лишние пробелы.
Например, пользователь ввёл имя так:
Внутри много лишних пробелов. Сначала можно схлопнуть несколько пробелов подряд в один:
SELECT REGEXP_REPLACE('Ivan Petrov', '\s+', ' ', 'g') AS clean_name;Результат:
Шаблон
'\s+'означает «один или несколько пробельных символов подряд».Это не только обычный пробел. В зависимости от данных туда могут попадать табы и другие пробельные символы.
Если нужно ещё убрать пробелы по краям строки, добавьте
TRIM:SELECT TRIM(REGEXP_REPLACE(' Ivan Petrov ', '\s+', ' ', 'g')) AS clean_name;Результат:
Здесь сначала
REGEXP_REPLACEпревращает все группы пробелов в один пробел, а потомTRIMубирает пробелы в начале и конце строки.Очистка имён
Допустим, в таблице есть имена, куда случайно попали лишние пробелы:
Можно привести их к более аккуратному виду:
SELECT name AS old_name, TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g')) AS clean_name FROM users;Результат:
Это хороший пример, где
REGEXP_REPLACEне меняет смысл данных, а просто убирает технический мусор.Но перед массовым
UPDATEвсё равно лучше сначала посмотреть результат черезSELECT.Перед UPDATE сначала сделайте SELECT
Если вы хотите почистить данные в таблице, не начинайте сразу с обновления.
Плохая идея:
UPDATE users SET name = TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g'));Такой запрос сразу перепишет данные. Если шаблон оказался неправильным, можно испортить много строк.
Лучше сначала посмотреть старое и новое значение рядом:
SELECT id, name AS old_name, TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g')) AS new_name FROM users WHERE name <> TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g')) ORDER BY id;Так вы увидите, какие строки реально изменятся.
И только после проверки можно делать
UPDATE:UPDATE users SET name = TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g')) WHERE name <> TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g'));Это простая, но очень важная привычка при чистке данных.
Флаг i: замена без учёта регистра
Флаг
iвключает поиск без учёта регистра.Например, нужно привести все варианты
gmailк одному виду.В данных могут быть такие email:
Запрос:
SELECT email, REGEXP_REPLACE(email, 'gmail', 'gmail', 'gi') AS normalized_email FROM users;Здесь флаги:
Результат:
Флаги можно комбинировать в одной строке:
'gi'.Порядок обычно не важен:
giиigчитаются одинаково.Флаг i на простом примере
Без флага
iрегистр важен:SELECT REGEXP_REPLACE('SQL sql Sql', 'sql', 'DB', 'g') AS result;Результат:
Заменилось только
sqlв нижнем регистре.С флагом
i:SELECT REGEXP_REPLACE('SQL sql Sql', 'sql', 'DB', 'gi') AS result;Результат:
Теперь совпали все варианты:
SQL,sqlиSql.Что происходит, если совпадений нет
Если шаблон ничего не нашёл,
REGEXP_REPLACEвозвращает исходную строку без изменений.SELECT REGEXP_REPLACE('hello', '[0-9]', '', 'g') AS result;Результат:
Цифр в строке нет, поэтому удалять нечего.
Это важный момент: функция не вернёт
NULLтолько потому, что совпадений не было. Она просто оставит строку как есть.А вот если сама исходная строка равна
NULL, результат будетNULL:SELECT REGEXP_REPLACE(NULL, '[0-9]', '', 'g') AS result;Результат:
Если нужно считать
NULLпустой строкой, используйтеCOALESCE:SELECT REGEXP_REPLACE(COALESCE(phone, ''), '[^0-9]', '', 'g') AS digits_only FROM users;Обратные ссылки: как переставлять части строки
REGEXP_REPLACEумеет не только удалять и заменять текст. Она может захватывать части строки и использовать их в замене.Для этого в регулярном выражении используют круглые скобки.
Например:
SELECT REGEXP_REPLACE('Ada Lovelace', '^(\w+)\s+(\w+)$', '\2, \1') AS result;Результат:
Разберём шаблон
'^(\w+)\s+(\w+)$':^— начало строки;(\w+)— первая группа: первое слово;\s+— один или несколько пробелов;(\w+)— вторая группа: второе слово;$— конец строки.Для строки:
первая группа — это
Ada, вторая группа — этоLovelace.В замене мы пишем
'\2, \1'.Это значит: сначала подставь вторую группу, потом запятую и пробел, потом первую группу.
Получается:
Группы нумеруются слева направо
Порядок групп зависит от порядка открывающих скобок в шаблоне.
В шаблоне
'^(\w+)\s+(\w+)$':Если бы групп было три, появились бы
\3,\4и так далее.Пример:
SELECT REGEXP_REPLACE('2026-06-19', '^([0-9]{4})-([0-9]{2})-([0-9]{2})$', '\3.\2.\1') AS result;Результат:
Группы такие:
В замене мы переставили их в формат «день.месяц.год».
Форматирование телефона через группы
Допустим, у нас уже есть телефон только из цифр:
Хотим получить красивый формат:
Запрос:
SELECT REGEXP_REPLACE( '5551234567', '^([0-9]{3})([0-9]{3})([0-9]{4})$', '(\1) \2-\3' ) AS phone_pretty;Результат:
Разберём шаблон
'^([0-9]{3})([0-9]{3})([0-9]{4})$':([0-9]{3})— первые 3 цифры;([0-9]{3})— следующие 3 цифры;([0-9]{4})— последние 4 цифры.А замена
'(\1) \2-\3'собирает номер в нужном виде.Важно: такой запрос сработает только для строки ровно из 10 цифр. Если цифр больше или меньше, шаблон не совпадёт, и строка останется как есть.
Поэтому для реальной чистки телефона часто делают два шага:
WITH cleaned AS ( SELECT phone, REGEXP_REPLACE(phone, '[^0-9]', '', 'g') AS digits FROM users ) SELECT phone, digits, CASE WHEN digits ~ '^[0-9]{10}$' THEN REGEXP_REPLACE(digits, '^([0-9]{3})([0-9]{3})([0-9]{4})$', '(\1) \2-\3') ELSE digits END AS phone_pretty FROM cleaned;Сначала оставляем только цифры, потом форматируем только те строки, где получилось ровно 10 цифр.
Подстановка всего совпадения
В PostgreSQL в строке замены можно использовать не только
\1,\2,\3, но и\&.Запись
\&означает всё найденное совпадение целиком.Например, хотим обернуть все числа в квадратные скобки:
SELECT REGEXP_REPLACE('Order 42, user 105', '[0-9]+', '[\&]', 'g') AS result;Результат:
Шаблон
[0-9]+нашёл числа42и105.А замена
[\&]сказала: возьми всё найденное число и поставь вокруг него квадратные скобки.Это удобно, когда не нужно разбивать совпадение на группы, а нужно просто обернуть или дополнить найденный фрагмент.
Многострочный текст и границы строк
В регулярных выражениях символы
^и$обычно означают начало и конец строки.Например:
SELECT REGEXP_REPLACE('error: failed', '^error', 'warning') AS result;Результат:
^errorозначает: словоerrorдолжно быть в начале строки.Но если у вас в одной строке хранится многострочный текст, возникает вопрос: считать ли началом только начало всего текста или начало каждой строки внутри него?
В PostgreSQL для этого есть флаги, связанные с режимом обработки переводов строк. В практических статьях чаще всего достаточно запомнить идею: если вы работаете с многострочным текстом и используете
^или$, обязательно проверьте поведение на примере с переводами строк.Например, возьмите такой текст:
И отдельно проверьте, заменяются ли
errorтолько в начале всего текста или в начале каждой строки.Для большинства задач чистки телефонов, имён, email и пробелов многострочный режим не нужен. Он важен, когда вы обрабатываете большие текстовые поля, логи, сообщения или документы.
POSIX-классы символов
В PostgreSQL регулярные выражения используют POSIX-синтаксис. Поэтому можно писать классы символов в таком виде:
SELECT REGEXP_REPLACE('abc123', '[[:digit:]]', '', 'g');Класс
[[:digit:]]— это цифра,[[:alpha:]]— буква,[[:alnum:]]— буква или цифра.Например, удалить всё, кроме букв и цифр:
SELECT REGEXP_REPLACE('User #42!', '[^[:alnum:]]', '', 'g') AS result;Результат:
Для новичка проще сначала использовать знакомые конструкции:
[0-9],[^0-9],\s+,\w+.Но POSIX-классы полезны, когда хочется написать более переносимый или более явный шаблон внутри PostgreSQL.
Частая ошибка: забыли флаг g
Самая частая ошибка с
REGEXP_REPLACEв PostgreSQL — забыть флагg.Например:
SELECT REGEXP_REPLACE('a b c', '\s+', ' ') AS result;Результат:
Схлопнулась только первая группа пробелов.
Правильно:
SELECT REGEXP_REPLACE('a b c', '\s+', ' ', 'g') AS result;Результат:
Если вы ожидаете, что заменятся все совпадения, почти всегда добавляйте
g.Частая ошибка: слишком широкий шаблон
Регулярные выражения мощные, но из-за этого ими легко удалить больше, чем нужно.
Например, кажется, что такой шаблон удалит HTML-теги:
SELECT REGEXP_REPLACE('<b>Hello</b> <i>SQL</i>', '<.*>', '', 'g') AS result;Но результат может неприятно удивить:
Почему? Потому что
.*может захватить слишком много: от первого<до последнего>.Осторожнее можно написать так:
SELECT REGEXP_REPLACE('<b>Hello</b> <i>SQL</i>', '<[^>]*>', '', 'g') AS result;Результат:
Шаблон
<[^>]*>означает:<— открывающая угловая скобка;[^>]*— любые символы, кроме закрывающей угловой скобки;>— закрывающая угловая скобка.Но даже это не полноценный HTML-парсер. Для сложного HTML лучше использовать специальные инструменты на уровне приложения.
REGEXP_REPLACEподходит для простой технической чистки, а не для разбора сложных языков разметки.Частая ошибка: регулярка вместо простой функции
Не нужно использовать
REGEXP_REPLACEтам, где достаточно обычногоREPLACE.Например, если нужно заменить все дефисы на пробелы:
SELECT REPLACE('a-b-c', '-', ' ') AS result;Это проще и понятнее, чем:
SELECT REGEXP_REPLACE('a-b-c', '-', ' ', 'g') AS result;Обычная замена быстрее читается и обычно проще для базы.
Простое правило:
REGEXP_REPLACE и производительность
Регулярные выражения удобные, но не бесплатные. Базе нужно разобрать шаблон и проверить строку.
На маленьких таблицах это не страшно. Но на большой таблице запрос вида:
SELECT * FROM users WHERE REGEXP_REPLACE(phone, '[^0-9]', '', 'g') = '15551234567';может быть тяжёлым.
Почему? Потому что база должна вычислить
REGEXP_REPLACE(phone, ...)для множества строк, а обычный индекс поphoneздесь обычно не поможет.Лучше хранить нормализованное значение отдельно, если оно нужно для поиска постоянно.
Например:
ALTER TABLE users ADD COLUMN phone_digits text;Потом заполнить:
UPDATE users SET phone_digits = REGEXP_REPLACE(phone, '[^0-9]', '', 'g');И уже искать так:
SELECT * FROM users WHERE phone_digits = '15551234567';А на
phone_digitsможно построить обычный индекс.В PostgreSQL также можно использовать функциональный индекс по выражению, если вы действительно хотите искать именно так:
CREATE INDEX idx_users_phone_digits ON users (REGEXP_REPLACE(phone, '[^0-9]', '', 'g'));Но создавать такие индексы стоит только под реальные частые запросы.
REGEXP_REPLACE в CHECK и generated column
Если правило очистки или нормализации повторяется в разных местах, его лучше не копировать руками в каждый запрос.
Например, если вам постоянно нужны только цифры телефона, можно сделать отдельную колонку или generated column.
Идея такая:
Так вы отделяете исходные данные от нормализованного вида.
Это особенно удобно, если:
REGEXP_REPLACEотлично подходит для входа в такой пайплайн, но не всегда должен выполняться заново в каждом отчёте.Разница между PostgreSQL, MySQL и ClickHouse
Идея везде похожая: есть строка, есть регулярное выражение, есть замена.
Но детали отличаются.
PostgreSQL
В PostgreSQL форма такая:
REGEXP_REPLACE(source, pattern, replacement [, flags]);Пример:
SELECT REGEXP_REPLACE('+1 (555) 123-45-67', '[^0-9]', '', 'g') AS digits;Результат:
Главное:
gзаменяется только первое совпадение;gвключает замену всех совпадений;iвключает поиск без учёта регистра;\1,\2,\3;\&означает всё совпадение целиком.MySQL
В MySQL 8+ функция тоже называется
REGEXP_REPLACE, но сигнатура другая:REGEXP_REPLACE(expr, pattern, replacement [, pos [, occurrence [, match_type]]]);Пример удаления всего, кроме цифр:
SELECT REGEXP_REPLACE(phone, '[^0-9]', '') AS digits FROM users;В MySQL глобальная замена обычно идёт по умолчанию, потому что
occurrence = 0означает заменить все совпадения.Если нужно явно указать режим без учёта регистра, используют
match_type:SELECT REGEXP_REPLACE(email, 'gmail', 'gmail', 1, 0, 'i') AS normalized_email FROM users;Здесь:
1— начинать поиск с первой позиции;0— заменить все совпадения;'i'— искать без учёта регистра.С обратными ссылками в MySQL будьте особенно внимательны: синтаксис и экранирование могут отличаться от PostgreSQL. В примерах и окружениях часто встречается формат
$1,$2, а в строковых литералах с обратными слэшами может понадобиться дополнительное экранирование. Перед переносом запроса с группами обязательно проверьте его на маленьком примере.ClickHouse
В ClickHouse для регулярной замены часто используют две функции:
replaceRegexpOneиreplaceRegexpAll.replaceRegexpOneзаменяет первое совпадение.replaceRegexpAllзаменяет все совпадения.Пример:
SELECT replaceRegexpAll(phone, '[^0-9]', '') AS digits FROM users;Если нужно заменить только первое совпадение:
SELECT replaceRegexpOne('a-b-c', '-', '+') AS result;Результат:
Если нужно заменить все:
SELECT replaceRegexpAll('a-b-c', '-', '+') AS result;Результат:
В ClickHouse глобальность видна прямо из имени функции. Это удобно: не нужно помнить про флаг
g, как в PostgreSQL.Что тестировать перед переносом между СУБД
Если вы переносите регулярную замену между PostgreSQL, MySQL и ClickHouse, не проверяйте её только на одной красивой строке.
Соберите маленький набор тестов:
И проверьте:
NULL;Особенно осторожно переносите запросы с группами и обратными ссылками. Именно там различия между СУБД чаще всего приводят к неожиданному результату.
Практический пример: чистим телефон и проверяем длину
Допустим, телефоны должны храниться в любом виде, но для проверки нам нужны только цифры.
SELECT id, phone, REGEXP_REPLACE(phone, '[^0-9]', '', 'g') AS digits FROM users;Результат:
Теперь можно найти подозрительные телефоны:
SELECT id, phone, REGEXP_REPLACE(phone, '[^0-9]', '', 'g') AS digits FROM users WHERE char_length(REGEXP_REPLACE(phone, '[^0-9]', '', 'g')) NOT BETWEEN 10 AND 15;Такой запрос полезен для аудита данных.
Но если такая проверка нужна постоянно, лучше вынести очищенный телефон в отдельную колонку, чтобы не считать регулярку каждый раз.
Практический пример: нормализуем пробелы в именах
SELECT id, name AS old_name, TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g')) AS new_name FROM users WHERE name <> TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g'));Так можно найти строки, где есть лишние пробелы.
После проверки:
UPDATE users SET name = TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g')) WHERE name <> TRIM(REGEXP_REPLACE(name, '\s+', ' ', 'g'));Это нормальный сценарий для разовой чистки данных после импорта.
Практический пример: меняем формат даты
Есть строка:
Хотим получить:
Запрос:
SELECT REGEXP_REPLACE( '2026-06-19', '^([0-9]{4})-([0-9]{2})-([0-9]{2})$', '\3.\2.\1' ) AS formatted_date;Результат:
Этот пример хорошо показывает силу групп: мы не просто заменили текст, а переставили части строки местами.
Но если в колонке хранится настоящий тип
date, лучше форматировать дату средствами работы с датами, а не регулярками. Регулярные выражения здесь уместны только если дата пришла как текст.Практический пример: удалить всё, кроме букв и цифр
Допустим, нужно получить простой код из строки:
Оставим только буквы и цифры:
SELECT REGEXP_REPLACE('User #42!', '[^[:alnum:]]', '', 'g') AS code;Результат:
Такой приём используют для технических кодов, slug-подготовки или очистки импортированных значений.
Но для полноценной генерации URL-slug обычно нужна более сложная логика: транслитерация, нижний регистр, замена пробелов на дефисы, обработка дублей. Одного
REGEXP_REPLACEможет быть мало.Коротко
REGEXP_REPLACE— это функция для замены текста по регулярному выражению.В PostgreSQL базовый синтаксис такой:
REGEXP_REPLACE(source, pattern, replacement [, flags]);Пример:
SELECT REGEXP_REPLACE('+1 (555) 123-45-67', '[^0-9]', '', 'g');Результат:
Главные правила:
REGEXP_REPLACEищет не точную строку, а шаблон;REPLACE;gзаменяется только первое совпадение;gзаменяет все совпадения;iвключает поиск без учёта регистра;''в замене удаляет найденный текст;NULL, результат тожеNULL;\1,\2,\3;\&в PostgreSQL означает всё совпадение целиком;WHEREмогут быть дорогими;Самое простое правило: если нужно заменить конкретный кусок текста — используйте
REPLACE. Если нужно заменить «все цифры», «все лишние пробелы», «всё, кроме букв», «любой вариант регистра» или переставить части строки по группам — используйтеREGEXP_REPLACE.