sqlpostgresqlstringslike

`STARTS_WITH` в PostgreSQL: как проверить, что строка начинается с нужного префикса

STARTS_WITH(str, prefix) в PostgreSQL 11+ проверяет начало строки буквально, без экранирования %. Разбираем регистр, индексы text_pattern_ops, NULL и эквиваленты в MySQL и ClickHouse.

8 мин чтенияСправочникsql · postgresql · strings · like · index · mysql

Когда нужно проверить начало строки, в голову обычно сразу приходит LIKE.

Например:

SELECT *
FROM orders
WHERE status LIKE 'ship%';

Такой запрос ищет строки, где status начинается с ship.

Но в PostgreSQL 11+ есть более прямой и читаемый вариант — функция starts_with.

Она отвечает ровно на один вопрос:

Начинается ли эта строка с такого-то префикса?

Пример:

SELECT starts_with('/api/orders', '/api/');

Результат:

true

Функция особенно удобна для маршрутов, кодов, артикулов, email-префиксов, статусов, технических имён и любых случаев, где префикс — это обычный текст, а не шаблон.

Главное отличие от LIKE в том, что starts_with воспринимает префикс буквально. Символы % и _ для неё не имеют особого смысла. Это не шаблон, а обычная строка.

Базовый синтаксис

У функции два аргумента:

starts_with(str, prefix)

Первый аргумент — строка, которую проверяем.

Второй аргумент — префикс, с которого она должна начинаться.

Пример:

SELECT starts_with('/api/orders', '/api/') AS is_api_path;

Результат:

is_api_path
-----------
true

Ещё пример:

SELECT starts_with('/admin/users', '/api/') AS is_api_path;

Результат:

is_api_path
-----------
false

Функция возвращает значение типа boolean: true, false или NULL, если один из аргументов равен NULL.

Поэтому её удобно использовать в WHERE:

SELECT id, status, amount
FROM orders
WHERE starts_with(status, 'ship');

Такой запрос вернёт заказы со статусами вроде shipped, shipping, ship_ready.

Почему starts_with читается лучше, чем LIKE

Сравним два варианта:

SELECT *
FROM orders
WHERE status LIKE 'ship%';

И:

SELECT *
FROM orders
WHERE starts_with(status, 'ship');

Оба запроса по смыслу ищут строки, которые начинаются с ship.

Но второй вариант читается почти как обычная фраза: status начинается с ship.

В простых случаях разница небольшая. Но чем сложнее префикс, тем заметнее польза.

Например, проверим путь API:

SELECT *
FROM access_logs
WHERE starts_with(path, '/api/v1/orders/');

Здесь не нужно вспоминать правила LIKE, думать про %, _ и экранирование. Видно сразу: путь должен начинаться с конкретного текста.

Главная разница: текст против шаблона

LIKE работает с шаблоном.

В шаблоне есть специальные символы:

  • % означает любое количество любых символов;
  • _ означает один любой символ.

Например:

SELECT 'ship_1' LIKE 'ship_';

Результат:

true

Потому что _ в LIKE — это не символ подчёркивания, а один любой символ.

А starts_with воспринимает второй аргумент как обычный текст.

SELECT starts_with('ship_1', 'ship_');

Результат:

true

Но здесь ship_ — это именно буквы s, h, i, p и символ _, а не шаблон.

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

Пример с % и _

Допустим, в статусах или промокодах есть строки, которые начинаются с 50%.

Например:

50%_off_winter
50%_off_spring

Если использовать LIKE, символ % нужно экранировать, иначе PostgreSQL воспримет его как часть шаблона.

SELECT *
FROM orders
WHERE status LIKE '50\%%';

Такой запрос уже выглядит менее приятно. Нужно помнить, что первый % — это символ процента в данных, а второй % — это подстановочный символ LIKE.

С starts_with проще:

SELECT *
FROM orders
WHERE starts_with(status, '50%');

Здесь 50% — обычный текст. Ничего экранировать не нужно.

Это снижает риск ошибок. Особенно в коде, где префикс собирается динамически.

Проверка маршрутов в логах

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

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

CREATE TABLE access_logs (
    id bigserial PRIMARY KEY,
    path text NOT NULL,
    status_code integer NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now()
);

Нужно выбрать все запросы к API:

SELECT id, path, status_code, created_at
FROM access_logs
WHERE starts_with(path, '/api/');

Можно выбрать только старую версию API:

SELECT id, path, status_code, created_at
FROM access_logs
WHERE starts_with(path, '/api/v1/');

Или все запросы к админке:

SELECT id, path, status_code, created_at
FROM access_logs
WHERE starts_with(path, '/admin/');

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

Проверка кодов и артикулов

Префикс часто используется в кодах.

Например:

RU-001
RU-002
KZ-115
VN-220

Если в таблице товаров есть артикул:

CREATE TABLE products (
    id bigserial PRIMARY KEY,
    sku text NOT NULL,
    name text NOT NULL,
    price numeric(12, 2) NOT NULL
);

Можно найти все товары с префиксом RU-:

SELECT id, sku, name, price
FROM products
WHERE starts_with(sku, 'RU-');

Или товары из другой линейки:

SELECT id, sku, name, price
FROM products
WHERE starts_with(sku, 'PREMIUM-');

В таких задачах starts_with хорошо передаёт намерение: нас интересует именно начало строки.

Чувствительность к регистру

starts_with чувствителен к регистру.

Это значит, что API и api — разные строки.

SELECT starts_with('API/orders', 'api');

Результат:

false

А так будет true:

SELECT starts_with('API/orders', 'API');

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

Например, через lower:

SELECT *
FROM users
WHERE starts_with(lower(email), 'admin@');

Такой запрос найдёт и admin@example.com, и Admin@example.com, и ADMIN@example.com.

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

CREATE INDEX idx_users_email_lower
    ON users (lower(email));

Тогда запрос с lower(email) сможет получить шанс на нормальный план.

Что происходит с пустым префиксом

Любая строка начинается с пустой строки.

Поэтому такой запрос вернёт true:

SELECT starts_with('abc', '');

Результат:

true

Это логично, но в реальном коде иногда становится неожиданностью.

Если префикс приходит из формы поиска, пользователь может отправить пустое значение. Тогда условие:

WHERE starts_with(name, '')

подойдёт почти ко всем строкам, где name не равен NULL.

Если такое поведение нежелательно, проверяйте префикс отдельно на уровне приложения или в SQL.

Например:

SELECT *
FROM products
WHERE length('RU-') > 0
  AND starts_with(sku, 'RU-');

В реальном запросе вместо 'RU-' обычно будет параметр приложения.

Что происходит с NULL

Если один из аргументов равен SQL-NULL, результат тоже будет NULL.

SELECT starts_with(NULL, 'api');

Результат:

null

И так тоже:

SELECT starts_with('/api/orders', NULL);

Результат:

null

Это нормальное поведение SQL: неизвестное значение даёт неизвестный результат.

В WHERE это обычно не видно, потому что строки, где условие вернуло NULL, отфильтровываются так же, как строки с false.

Например:

SELECT *
FROM access_logs
WHERE starts_with(path, '/api/');

Если path равен NULL, строка не попадёт в результат.

Но в SELECT, CASE или бизнес-логике разница между false и NULL может быть важной.

Если вам нужен строгий результат true или false, используйте COALESCE:

SELECT
    path,
    COALESCE(starts_with(path, '/api/'), false) AS is_api_path
FROM access_logs;

Теперь NULL превратится в false.

starts_with в CASE

Так как функция возвращает boolean, её удобно использовать в CASE.

Например, разметим запросы по разделам сайта:

SELECT
    id,
    path,
    CASE
        WHEN starts_with(path, '/api/') THEN 'api'
        WHEN starts_with(path, '/admin/') THEN 'admin'
        WHEN starts_with(path, '/static/') THEN 'static'
        ELSE 'other'
    END AS path_group
FROM access_logs;

Так можно быстро собрать понятную группировку для отчёта.

Например, посчитать количество запросов по разделам:

SELECT
    CASE
        WHEN starts_with(path, '/api/') THEN 'api'
        WHEN starts_with(path, '/admin/') THEN 'admin'
        WHEN starts_with(path, '/static/') THEN 'static'
        ELSE 'other'
    END AS path_group,
    count(*) AS request_count
FROM access_logs
GROUP BY path_group
ORDER BY request_count DESC;

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

starts_with в CHECK-ограничении

Функцию можно использовать не только в выборках, но и в ограничениях.

Например, хотим, чтобы все артикулы начинались с SKU-:

CREATE TABLE items (
    id bigserial PRIMARY KEY,
    sku text NOT NULL,
    name text NOT NULL,
    CONSTRAINT items_sku_prefix_check
        CHECK (starts_with(sku, 'SKU-'))
);

Теперь PostgreSQL не даст вставить строку с неправильным артикулом:

INSERT INTO items (sku, name)
VALUES ('ABC-001', 'Keyboard');

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

Производительность и индексы

На маленьких таблицах разницы обычно не видно: starts_with, LIKE и другие варианты работают быстро.

На больших таблицах нужно думать об индексе.

Для префиксного поиска часто используют LIKE 'prefix%' с подходящим индексом.

Например:

CREATE INDEX idx_orders_status_pattern
    ON orders (status text_pattern_ops);

И запрос:

SELECT *
FROM orders
WHERE status LIKE 'ship%';

Такой индекс помогает PostgreSQL искать строки по началу текста.

Почему не всегда достаточно обычного индекса?

Обычный B-tree индекс по текстовой колонке зависит от правил сортировки. В некоторых локалях PostgreSQL не сможет эффективно использовать его для LIKE 'x%'. Класс операторов text_pattern_ops специально предназначен для шаблонов с префиксом.

Проверять нужно не на глаз, а через EXPLAIN.

EXPLAIN
SELECT *
FROM orders
WHERE status LIKE 'ship%';

Если видите Index Scan или Bitmap Index Scan, индекс используется. Если видите Seq Scan, PostgreSQL читает таблицу последовательно.

А что с индексом для starts_with

Важный практический момент: starts_with не всегда оптимизируется так же очевидно, как LIKE 'prefix%'.

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

EXPLAIN
SELECT *
FROM orders
WHERE starts_with(status, 'ship');

Если план показывает последовательное чтение всей таблицы, а вам нужен быстрый поиск по префиксу, можно использовать вариант с LIKE и индексом text_pattern_ops.

SELECT *
FROM orders
WHERE status LIKE 'ship%';

Получается такое правило:

  • для читаемости и безопасной проверки буквального префикса удобен starts_with;
  • для горячих запросов на больших таблицах проверяйте план;
  • если индекс лучше подхватывается через LIKE 'x%', используйте LIKE в производительном участке.

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

Как совместить читаемость и скорость

Иногда можно оставить starts_with в менее критичных местах: в CHECK, отчётах, небольших справочниках, админских запросах.

А для нагруженного поиска использовать LIKE с правильно подготовленным префиксом.

Например, если префикс задан разработчиком и не содержит пользовательских спецсимволов:

SELECT *
FROM access_logs
WHERE path LIKE '/api/%';

Если же префикс приходит от пользователя и может содержать % или _, нужно аккуратно экранировать его для LIKE.

В таких случаях starts_with проще и безопаснее:

SELECT *
FROM access_logs
WHERE starts_with(path, '50%');

Главное — не принимать решение вслепую. Для больших таблиц смотрите EXPLAIN.

Аналог в MySQL

В MySQL нет функции starts_with с таким же именем.

Чаще всего используют LIKE:

SELECT id, status
FROM orders
WHERE status LIKE 'ship%';

Или функцию LEFT:

SELECT id, status
FROM orders
WHERE LEFT(status, 4) = 'ship';

Вариант с LEFT похож по смыслу на starts_with: мы берём первые 4 символа строки и сравниваем с ship.

Но есть важная особенность MySQL: чувствительность к регистру зависит от collation колонки.

Например, при collation с суффиксом _ci сравнение обычно нечувствительно к регистру. Тогда ship% может совпасть и с Shipped.

Если нужно строгое сравнение с учётом регистра, используют бинарное сравнение или подходящую collation.

SELECT id, status
FROM orders
WHERE BINARY status LIKE 'ship%';

При переносе запроса из PostgreSQL в MySQL это важно проверить. В PostgreSQL starts_with регистрозависим, а в MySQL результат может зависеть от настроек колонки.

Аналог в ClickHouse

В ClickHouse есть функция startsWith.

Она очень близка по смыслу к PostgreSQL starts_with.

SELECT id, status
FROM orders
WHERE startsWith(status, 'ship');

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

При переносе запросов всё равно стоит отдельно проверить поведение на:

NULL
''
Ship
ship
50%_off

Именно на таких значениях обычно всплывают различия между СУБД.

starts_with или LIKE: что выбрать

Если нужно просто проверить начало строки и префикс должен восприниматься буквально, берите starts_with.

Хорошие случаи:

WHERE starts_with(path, '/api/')
WHERE starts_with(sku, 'RU-')
WHERE starts_with(email, 'admin@')

Если нужен именно шаблон, где % и _ должны работать как специальные символы, используйте LIKE.

Например:

WHERE status LIKE 'ship%'

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

Частые ошибки

Первая ошибка — забыть, что starts_with чувствителен к регистру.

SELECT starts_with('Shipped', 'ship');

Результат:

false

Если нужен поиск без учёта регистра, нормализуйте обе стороны:

SELECT starts_with(lower('Shipped'), 'ship');

Вторая ошибка — ждать, что NULL превратится в false.

SELECT starts_with(NULL, 'ship');

Результат:

null

Если нужен именно false, используйте COALESCE:

SELECT COALESCE(starts_with(NULL, 'ship'), false);

Третья ошибка — думать, что LIKE и starts_with полностью одинаковы.

Так не всегда. В LIKE символы % и _ особенные, а в starts_with это обычные символы.

Четвёртая ошибка — не проверять индекс на большой таблице.

Красивый запрос может быть медленным, если PostgreSQL читает всю таблицу. Используйте EXPLAIN.

Пятая ошибка — забыть про пустой префикс.

SELECT starts_with('abc', '');

Результат:

true

Если пустой префикс не должен проходить, проверяйте его отдельно.

Главное

starts_with(str, prefix) проверяет, начинается ли строка str с префикса prefix.

В PostgreSQL эта функция доступна с версии 11 и возвращает boolean.

В отличие от LIKE, префикс в starts_with — обычный текст, а не шаблон. Символы % и _ не нужно экранировать.

starts_with чувствителен к регистру. Для поиска без учёта регистра используйте lower или другой подход к нормализации.

Если один из аргументов равен NULL, результат тоже будет NULL. Для строгого true или false оборачивайте результат в COALESCE.

Для больших таблиц проверяйте план через EXPLAIN. Иногда для быстрого префиксного поиска лучше использовать LIKE 'x%' вместе с индексом text_pattern_ops.

starts_with хорош тем, что делает намерение запроса очевидным: мы не пишем шаблон, не играем с подстановками, не экранируем лишние символы, а просто проверяем начало строки. Для новичка и для команды, которая потом будет читать этот SQL, это часто уже большая победа.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр