sqlpostgresqlstringsmysql

SPLIT_PART in PostgreSQL: Split a String and Take the N-th Field

Pull the domain from an email, a segment from a path, or a code from a SKU with a single SPLIT_PART call, plus MySQL and ClickHouse equivalents.

9 min branjaReferencesql · postgresql · strings · mysql · clickhouse
Ta članek je trenutno v ruščini — angleški prevod je v pripravi.

В SQL часто приходится работать со строками, внутри которых уже есть понятная структура.

Например:

anna@gmail.com

В email есть две части:

anna
gmail.com

И они разделены символом @.

Или есть путь:

eng/backend/payments

В нём несколько частей:

eng
backend
payments

И они разделены слешем /.

Или есть составной статус заказа:

paid:card:eur

Он может означать:

статус оплаты: способ оплаты: валюта

В таких случаях не всегда нужны сложные регулярные выражения. Если строка устроена просто и разделитель известен заранее, в PostgreSQL удобно использовать функцию SPLIT_PART.

Она делает ровно то, что нужно: разбивает строку по разделителю и возвращает нужную часть.

Что делает SPLIT_PART простыми словами

SPLIT_PART берёт строку, разрезает её по указанному разделителю и возвращает одну конкретную часть.

Синтаксис:

SPLIT_PART(string, delimiter, field_number)

Где:

  • string — строка, которую нужно разделить;
  • delimiter — разделитель;
  • field_number — номер части, которую нужно вернуть.

Простой пример:

SELECT SPLIT_PART('anna@gmail.com', '@', 1) AS local_part;

Результат:

local_part
----------
anna

Мы сказали базе:

Раздели строку anna@gmail.com по символу @ и верни первую часть.

Если нужна вторая часть:

SELECT SPLIT_PART('anna@gmail.com', '@', 2) AS domain;

Результат:

domain
---------
gmail.com

То есть SPLIT_PART идеально подходит для ситуаций, где строка похожа на набор частей, склеенных через один и тот же разделитель.

Нумерация начинается с 1

Очень важный момент: части строки в SPLIT_PART считаются с единицы.

Не с нуля, как во многих языках программирования, а именно с 1.

Например:

SELECT
  SPLIT_PART('a.b.c', '.', 1) AS part_1,
  SPLIT_PART('a.b.c', '.', 2) AS part_2,
  SPLIT_PART('a.b.c', '.', 3) AS part_3;

Результат:

part_1 | part_2 | part_3
-------+--------+-------
a      | b      | c

Можно представить это так:

'a.b.c'

1-я часть: a
2-я часть: b
3-я часть: c

Поэтому, если вы хотите взять первую часть строки, пишите 1, а не 0.

Пример с email: получаем домен

Один из самых понятных примеров — получение домена из email.

Допустим, есть таблица users:

id | email
---+------------------
1  | anna@gmail.com
2  | bob@mail.ru
3  | kate@yahoo.com
4  | tom@gmail.com

Чтобы получить домен, нужно взять вторую часть email после символа @.

SELECT
  id,
  email,
  SPLIT_PART(email, '@', 2) AS domain
FROM users;

Результат:

id | email           | domain
---+-----------------+----------
1  | anna@gmail.com  | gmail.com
2  | bob@mail.ru     | mail.ru
3  | kate@yahoo.com  | yahoo.com
4  | tom@gmail.com   | gmail.com

Теперь можно, например, посчитать пользователей по почтовым доменам.

SELECT
  SPLIT_PART(email, '@', 2) AS domain,
  COUNT(*) AS users_count
FROM users
GROUP BY SPLIT_PART(email, '@', 2)
ORDER BY users_count DESC;

Результат может быть таким:

domain    | users_count
----------+------------
gmail.com | 2
mail.ru   | 1
yahoo.com | 1

Такой запрос помогает быстро понять, какими почтовыми сервисами пользуются ваши пользователи.

Получаем часть email до @

Если вторая часть email — это домен, то первая часть — это всё, что идёт до @.

Например:

anna@gmail.com

Первая часть:

anna

Запрос:

SELECT
  id,
  email,
  SPLIT_PART(email, '@', 1) AS local_part
FROM users;

Результат:

id | email           | local_part
---+-----------------+-----------
1  | anna@gmail.com  | anna
2  | bob@mail.ru     | bob
3  | kate@yahoo.com  | kate

Это может пригодиться, например, чтобы быстро найти тестовые аккаунты, внутренние маски или пользователей с определённым шаблоном в email.

SELECT
  id,
  email
FROM users
WHERE SPLIT_PART(email, '@', 1) LIKE 'test%';

Такой запрос найдёт email, где часть до @ начинается с test.

Что будет, если разделителя нет

Это важная особенность SPLIT_PART.

Если разделитель не найден, вся строка считается первой частью.

Например:

SELECT
  SPLIT_PART('hello', '@', 1) AS part_1,
  SPLIT_PART('hello', '@', 2) AS part_2;

Результат:

part_1 | part_2
-------+-------
hello  |

Первая часть — это вся строка hello.

А второй части нет, поэтому PostgreSQL вернул пустую строку.

Важно:

Если нужной части нет, SPLIT_PART возвращает пустую строку '', а не NULL.

Это может влиять на фильтры и отчёты.

Например, если в таблице есть невалидные email без @, такой запрос:

SELECT
  SPLIT_PART(email, '@', 2) AS domain,
  COUNT(*) AS users_count
FROM users
GROUP BY SPLIT_PART(email, '@', 2);

может дать отдельную группу с пустым доменом.

domain    | users_count
----------+------------
gmail.com | 10
mail.ru   | 5
          | 2

Пустое значение здесь может означать, что у части пользователей email записан некорректно.

Как отфильтровать строки без нужной части

Если вы хотите работать только с email, где есть символ @, можно добавить фильтр:

SELECT
  SPLIT_PART(email, '@', 2) AS domain,
  COUNT(*) AS users_count
FROM users
WHERE email LIKE '%@%'
GROUP BY SPLIT_PART(email, '@', 2)
ORDER BY users_count DESC;

Так мы заранее убираем строки, где нет @.

Можно также отфильтровать пустой результат:

SELECT
  SPLIT_PART(email, '@', 2) AS domain,
  COUNT(*) AS users_count
FROM users
WHERE SPLIT_PART(email, '@', 2) <> ''
GROUP BY SPLIT_PART(email, '@', 2)
ORDER BY users_count DESC;

Оба варианта рабочие, но смысл немного разный.

Первый проверяет, что в email есть разделитель @.

Второй проверяет, что после разделения вторая часть не пустая.

Для простых учебных запросов обычно достаточно email LIKE '%@%'. Для настоящей валидации email этого мало, но для демонстрации SPLIT_PART подходит хорошо.

Разделитель — это обычный текст, а не регулярное выражение

SPLIT_PART делит строку по конкретному разделителю.

Например:

SELECT SPLIT_PART('a.b.c', '.', 2) AS result;

Результат:

result
------
b

Здесь точка . означает именно точку.

Это не регулярное выражение.

То есть SPLIT_PART не думает, что точка — это «любой символ». Она ищет в строке обычный символ ..

То же самое с другими символами:

SELECT SPLIT_PART('paid:card:eur', ':', 2);

Результат:

card

Разделителем может быть не только один символ, но и строка из нескольких символов.

Например:

SELECT SPLIT_PART('2026--06--17', '--', 2) AS month;

Результат:

month
-----
06

Здесь разделитель — это строка --.

Пример: разбираем составной статус заказа

Допустим, в таблице orders статус хранится в таком виде:

id | status
---+---------------
1  | paid:card:eur
2  | paid:cash:usd
3  | failed:card:eur
4  | pending:bank:usd

Это составная строка. В ней зашито сразу несколько смыслов:

payment_state : method : currency

То есть:

  • первая часть — состояние платежа;
  • вторая часть — способ оплаты;
  • третья часть — валюта.

Можно разложить это по отдельным колонкам прямо в запросе:

SELECT
  id,
  SPLIT_PART(status, ':', 1) AS payment_state,
  SPLIT_PART(status, ':', 2) AS payment_method,
  SPLIT_PART(status, ':', 3) AS currency
FROM orders;

Результат:

id | payment_state | payment_method | currency
---+---------------+----------------+---------
1  | paid          | card           | eur
2  | paid          | cash           | usd
3  | failed        | card           | eur
4  | pending       | bank           | usd

Теперь с этими частями можно работать отдельно.

Например, посчитать заказы по способу оплаты:

SELECT
  SPLIT_PART(status, ':', 2) AS payment_method,
  COUNT(*) AS orders_count
FROM orders
GROUP BY SPLIT_PART(status, ':', 2)
ORDER BY orders_count DESC;

Или выбрать только платежи в евро:

SELECT
  id,
  status
FROM orders
WHERE SPLIT_PART(status, ':', 3) = 'eur';

Пример: верхний уровень отдела

Допустим, в таблице employees отдел хранится как путь:

id | name       | dept
---+------------+----------------------
1  | Anna       | eng/backend/payments
2  | Bob        | eng/frontend/web
3  | Kate       | marketing/content
4  | Tom        | sales/b2b

Первая часть до / — это верхний уровень отдела:

eng
marketing
sales

Запрос:

SELECT
  id,
  name,
  SPLIT_PART(dept, '/', 1) AS top_level_dept
FROM employees;

Результат:

id | name | top_level_dept
---+------+---------------
1  | Anna | eng
2  | Bob  | eng
3  | Kate | marketing
4  | Tom  | sales

Теперь можно посчитать сотрудников по верхнему уровню отдела:

SELECT
  SPLIT_PART(dept, '/', 1) AS top_level_dept,
  COUNT(*) AS employees_count
FROM employees
GROUP BY SPLIT_PART(dept, '/', 1)
ORDER BY employees_count DESC;

Результат:

top_level_dept | employees_count
---------------+----------------
eng            | 2
marketing      | 1
sales          | 1

Такой подход удобен, когда в одном поле хранится иерархия.

Отрицательный номер части в PostgreSQL 14+

В PostgreSQL 14 и новее SPLIT_PART умеет брать части с конца строки.

Для этого третий аргумент можно сделать отрицательным.

Например:

SELECT SPLIT_PART('eng/backend/payments', '/', -1) AS last_part;

Результат:

last_part
---------
payments

-1 означает «первая часть с конца», то есть последняя часть строки.

Можно взять предпоследнюю часть:

SELECT SPLIT_PART('eng/backend/payments', '/', -2) AS second_from_end;

Результат:

second_from_end
---------------
backend

Это удобно, когда длина строки заранее неизвестна.

Например, путь может быть таким:

eng/payments

А может быть таким:

eng/backend/payments

А может быть таким:

company/eng/backend/payments

Если вам всегда нужна последняя часть, отрицательный индекс сильно упрощает запрос.

SELECT SPLIT_PART(dept, '/', -1) AS final_dept
FROM employees;

Но есть важный нюанс: если ваш проект работает на PostgreSQL 13 или старше, отрицательный индекс в SPLIT_PART не поддерживается. Такой запрос может завершиться ошибкой.

Поэтому перед использованием отрицательных индексов лучше проверить версию PostgreSQL.

SELECT version();

Если версия старая, последний сегмент можно получить другими способами, например через регулярные выражения.

SELECT regexp_replace('eng/backend/payments', '^.*/', '') AS last_part;

Результат:

last_part
---------
payments

Но для новичка главное запомнить простую мысль:

В новых версиях PostgreSQL можно брать части с конца через отрицательные номера. В старых версиях так делать нельзя.

SPLIT_PART и пустые части

Иногда в строке могут быть два разделителя подряд.

Например:

a..c

Если разделитель — точка, то между двумя точками находится пустая часть.

Проверим:

SELECT
  SPLIT_PART('a..c', '.', 1) AS part_1,
  SPLIT_PART('a..c', '.', 2) AS part_2,
  SPLIT_PART('a..c', '.', 3) AS part_3;

Результат:

part_1 | part_2 | part_3
-------+--------+-------
a      |        | c

Вторая часть пустая.

Это не ошибка функции. Это значит, что в исходной строке действительно есть пустой сегмент между двумя разделителями.

Такое часто встречается в грязных данных:

paid::eur
eng//payments
anna@@gmail.com

Поэтому, если формат строки важен, иногда стоит дополнительно проверять, что нужные части не пустые.

Например:

SELECT
  id,
  status
FROM orders
WHERE SPLIT_PART(status, ':', 1) <> ''
  AND SPLIT_PART(status, ':', 2) <> ''
  AND SPLIT_PART(status, ':', 3) <> '';

Такой запрос оставит только строки, где все три части заполнены.

SPLIT_PART не заменяет нормальную структуру данных

SPLIT_PART очень удобен, но им не стоит злоупотреблять.

Если в поле status хранится строка:

paid:card:eur

это может быть нормально для логов, временного импорта или технических событий.

Но если это важные бизнес-данные, часто лучше хранить их в отдельных колонках:

payment_state | payment_method | currency
--------------+----------------+---------
paid          | card           | eur

Тогда запросы будут проще:

SELECT *
FROM orders
WHERE currency = 'eur';

Вместо:

SELECT *
FROM orders
WHERE SPLIT_PART(status, ':', 3) = 'eur';

Почему это важно?

Потому что отдельные колонки:

  • проще читать;
  • проще валидировать;
  • проще индексировать;
  • проще использовать в отчётах;
  • меньше зависят от формата строки.

SPLIT_PART отлично подходит для разбора строк, но если вы постоянно достаёте из одной строки одни и те же части, возможно, данные стоит хранить иначе.

SPLIT_PART в WHERE

SPLIT_PART можно использовать не только в SELECT, но и в WHERE.

Например, выбрать пользователей с доменом gmail.com:

SELECT
  id,
  email
FROM users
WHERE SPLIT_PART(email, '@', 2) = 'gmail.com';

Это удобно и читаемо.

Но на больших таблицах стоит помнить о производительности. Если вы применяете функцию к колонке в условии, обычный индекс по email может не помочь так эффективно, как простой поиск по готовому значению.

Для маленьких таблиц или разовой аналитики это обычно не проблема.

Но если такой фильтр используется часто, можно подумать о более правильном решении:

  • хранить домен в отдельной колонке;
  • создавать вычисляемую колонку;
  • использовать функциональный индекс;
  • нормализовать данные при загрузке.

Для обучения достаточно понимать идею:

SPLIT_PART в WHERE работает, но для больших рабочих таблиц лучше заранее подумать о структуре данных и индексах.

SPLIT_PART в GROUP BY

Если вы выводите результат SPLIT_PART и хотите по нему группировать, нужно использовать то же выражение в GROUP BY.

Например:

SELECT
  SPLIT_PART(email, '@', 2) AS domain,
  COUNT(*) AS users_count
FROM users
GROUP BY SPLIT_PART(email, '@', 2)
ORDER BY users_count DESC;

В PostgreSQL также можно написать короче через номер колонки:

SELECT
  SPLIT_PART(email, '@', 2) AS domain,
  COUNT(*) AS users_count
FROM users
GROUP BY 1
ORDER BY users_count DESC;

GROUP BY 1 означает:

группировать по первой колонке из SELECT.

В нашем случае первая колонка — это:

SPLIT_PART(email, '@', 2)

Для учебных статей лучше сначала писать полное выражение. Так понятнее, что именно происходит.

А когда вы уже уверенно читаете SQL, можно использовать короткую запись.

Когда SPLIT_PART подходит хорошо

SPLIT_PART хорошо подходит, когда:

  • разделитель известен заранее;
  • формат строки простой;
  • нужно взять конкретную часть;
  • не требуется сложная проверка;
  • строка не содержит вложенной логики.

Хорошие примеры:

anna@gmail.com
paid:card:eur
eng/backend/payments
RU-MOW-001
2026-06-17

Например, из кода:

RU-MOW-001

можно достать страну, город и номер:

SELECT
  SPLIT_PART('RU-MOW-001', '-', 1) AS country_code,
  SPLIT_PART('RU-MOW-001', '-', 2) AS city_code,
  SPLIT_PART('RU-MOW-001', '-', 3) AS item_number;

Результат:

country_code | city_code | item_number
-------------+-----------+------------
RU           | MOW       | 001

Это простой, понятный и читаемый сценарий для SPLIT_PART.

Когда SPLIT_PART уже недостаточно

SPLIT_PART становится неудобным, когда формат строки сложный.

Например:

  • разделителей может быть разное количество;
  • часть строки может отсутствовать;
  • внутри значения могут быть вложенные разделители;
  • нужно проверять шаблон;
  • нужно извлечь значение по сложному правилу;
  • строка похожа на JSON, URL или произвольный лог.

Например:

payment[state=paid;method=card;currency=eur]

Здесь простой разрез по одному символу уже не такой удобный.

В таких случаях лучше использовать другие инструменты:

  • regexp_match;
  • regexp_replace;
  • regexp_split_to_array;
  • функции для JSON, если строка на самом деле JSON;
  • нормализацию данных в отдельные колонки.

Главная идея:

Если строка простая — берите SPLIT_PART. Если формат сложный — не прячьте бизнес-логику в цепочке разрезаний.

Аналог в MySQL: SUBSTRING_INDEX

В MySQL функции SPLIT_PART нет.

Похожую задачу часто решают через SUBSTRING_INDEX.

Синтаксис:

SUBSTRING_INDEX(string, delimiter, count)

Но работает она немного иначе.

Если count положительный, MySQL берёт всё до указанного количества разделителей слева.

Пример:

SELECT SUBSTRING_INDEX('a.b.c.d', '.', 2) AS result;

Результат:

result
------
a.b

То есть это не ровно вторая часть, а первые две части вместе.

Если count отрицательный, MySQL берёт части справа.

SELECT SUBSTRING_INDEX('a.b.c.d', '.', -1) AS result;

Результат:

result
------
d

Для email это удобно. Чтобы получить домен:

SELECT SUBSTRING_INDEX(email, '@', -1) AS domain
FROM users;

Из строки:

anna@gmail.com

получится:

gmail.com

А вот чтобы получить ровно вторую часть строки a.b.c.d, нужен вложенный вызов:

SELECT SUBSTRING_INDEX(
         SUBSTRING_INDEX('a.b.c.d', '.', 2),
         '.',
         -1
       ) AS second_part;

Результат:

second_part
-----------
b

Логика такая:

  1. SUBSTRING_INDEX('a.b.c.d', '.', 2) берёт a.b;
  2. внешний SUBSTRING_INDEX(..., '.', -1) берёт последнюю часть из a.b;
  3. получается b.

То есть в MySQL можно сделать то же самое, но синтаксис менее прямой, чем в PostgreSQL.

Аналог в ClickHouse: splitByChar и splitByString

В ClickHouse задача обычно решается через массивы.

Функция splitByChar разбивает строку по символу и возвращает массив частей.

Например:

SELECT splitByChar('@', 'anna@gmail.com');

Результат будет похож на массив:

['anna', 'gmail.com']

Чтобы взять нужный элемент массива, используют индекс.

В ClickHouse индексация массивов начинается с 1.

Домен email:

SELECT splitByChar('@', email)[2] AS domain
FROM users;

Если разделитель состоит из нескольких символов, используют splitByString.

Например:

SELECT splitByString('--', '2026--06--17')[2] AS month;

Результат:

06

Чтобы взять последний элемент массива, можно использовать arrayElement с отрицательным индексом:

SELECT arrayElement(splitByChar('/', 'eng/backend/payments'), -1) AS last_part;

Результат:

payments

В ClickHouse подход немного другой:

сначала разбиваем строку в массив, потом берём элемент массива.

Краткое сравнение PostgreSQL, MySQL и ClickHouse

В PostgreSQL:

SELECT SPLIT_PART('a.b.c', '.', 2);

Результат:

b

В MySQL:

SELECT SUBSTRING_INDEX(SUBSTRING_INDEX('a.b.c', '.', 2), '.', -1);

Результат:

b

В ClickHouse:

SELECT splitByChar('.', 'a.b.c')[2];

Результат:

b

Идея везде одна:

разбить строку по разделителю и взять нужную часть.

Но синтаксис отличается.

PostgreSQL в этом случае выглядит особенно удобно, потому что SPLIT_PART сразу возвращает нужное поле.

Практические шаблоны

Домен из email

SELECT SPLIT_PART(email, '@', 2) AS domain
FROM users;

Локальная часть email до @

SELECT SPLIT_PART(email, '@', 1) AS local_part
FROM users;

Первая часть статуса

SELECT SPLIT_PART(status, ':', 1) AS payment_state
FROM orders;

Вторая часть статуса

SELECT SPLIT_PART(status, ':', 2) AS payment_method
FROM orders;

Третья часть статуса

SELECT SPLIT_PART(status, ':', 3) AS currency
FROM orders;

Верхний уровень отдела

SELECT SPLIT_PART(dept, '/', 1) AS top_level_dept
FROM employees;

Последняя часть пути в PostgreSQL 14+

SELECT SPLIT_PART(dept, '/', -1) AS last_dept
FROM employees;

Группировка пользователей по доменам

SELECT
  SPLIT_PART(email, '@', 2) AS domain,
  COUNT(*) AS users_count
FROM users
GROUP BY SPLIT_PART(email, '@', 2)
ORDER BY users_count DESC;

Фильтр по домену

SELECT
  id,
  email
FROM users
WHERE SPLIT_PART(email, '@', 2) = 'gmail.com';

Что важно запомнить

SPLIT_PART — это функция PostgreSQL для простого разделения строки.

Она используется так:

SPLIT_PART(string, delimiter, field_number)

Например:

SELECT SPLIT_PART('anna@gmail.com', '@', 2);

Результат:

gmail.com

Главные правила:

  • части считаются с 1, а не с 0;
  • если нужной части нет, возвращается пустая строка '';
  • если разделитель не найден, вся строка считается первой частью;
  • разделитель — это обычный текст, а не регулярное выражение;
  • в PostgreSQL 14+ можно использовать отрицательные номера частей для отсчёта с конца.

SPLIT_PART хорошо подходит для простых форматов:

email@domain.com
paid:card:eur
eng/backend/payments
RU-MOW-001

Но если формат строки сложный, лучше использовать регулярные выражения, массивы, JSON-функции или хранить данные в отдельных колонках.

Короткий вывод

SPLIT_PART помогает быстро достать нужную часть строки по разделителю.

Например, домен из email:

SELECT SPLIT_PART(email, '@', 2) AS domain
FROM users;

Или валюту из составного статуса:

SELECT SPLIT_PART(status, ':', 3) AS currency
FROM orders;

Это простая и читаемая функция, которая отлично подходит для учебных задач, аналитических запросов и быстрой обработки строк.

Главное — помнить, что SPLIT_PART не проверяет качество данных за вас. Если строка пришла в неожиданном формате, функция не упадёт, а может вернуть пустую строку. Поэтому в важных отчётах лучше дополнительно проверять, что нужная часть действительно существует и не пустая.

Vadite na resničnih nalogah

Rešujte naloge v vadnici SQL s takojšnjim ocenjevanjem in namigi.

Odpri vadnico