Šis raksts pašlaik ir krievu valodā — angļu tulkojums tiek gatavots.
Когда нужно проверить, что строка начинается с известного префикса — путь запроса, код страны, артикул — в PostgreSQL 11+ есть аккуратная функция STARTS_WITH(str, prefix). Она читается лучше, чем LIKE 'x%', не требует экранирования спецсимволов и при правильном индексе работает так же быстро. Ниже разберём её поведение с регистром, индексами и аналоги в других СУБД.
У STARTS_WITH ровно два аргумента: строка и префикс, и оба — обычные текстовые значения, а не шаблоны. Это отличает её от LIKE, где префикс одновременно и данные, и язык подстановок. Поэтому STARTS_WITH(path, '/api/') совпадёт ровно с теми строками, что начинаются с /api/, и ни с чем больше, тогда как path LIKE '/api/%' придётся читать с оглядкой на % и _ внутри /api/. Функция доступна с PostgreSQL 11 и возвращает boolean, так что её удобно ставить прямо в WHERE, в CASE или в CHECK-ограничение. Дальше разберём, чем именно она отличается от LIKE по поведению и по скорости.
STARTS_WITH против LIKE 'x%'
STARTS_WITH(str, prefix) возвращает true, если str начинается с prefix. Это эквивалент str LIKE prefix || '%', но без подводных камней с шаблонами.
SELECT starts_with('/api/orders', '/api/');
SELECT starts_with('/admin', '/api/');
Типичный сценарий — фильтрация по маршруту в логах или по сегменту пути:
SELECT id, status, amount
FROM orders
WHERE starts_with(status, 'ship');
Главное преимущество перед LIKE — префикс берётся буквально. В LIKE символы % и _ имеют особое значение, поэтому проверка пути вроде '50%_off' требует экранирования. С STARTS_WITH ничего экранировать не нужно:
SELECT * FROM orders WHERE status LIKE '50\%%';
SELECT * FROM orders WHERE starts_with(status, '50%');
То же удобно, когда префикс приходит из переменной приложения: с STARTS_WITH его не нужно очищать от % и _, а значит, нет целого класса ошибок и неожиданных совпадений. Намерение читается с первого взгляда: «строка начинается с этого префикса», без мысленного разбора шаблона.
Регистр и индексы
STARTS_WITH чувствителен к регистру: 'API' и 'api' — разные префиксы. Если нужно сравнение без учёта регистра, приводите обе стороны к одному регистру через lower.
SELECT * FROM users WHERE starts_with(email, 'Admin@');
SELECT * FROM users WHERE starts_with(lower(email), 'admin@');
Теперь про производительность. STARTS_WITH сам по себе планировщик не превращает в поиск по индексу так же охотно, как col LIKE 'x%'. Поэтому на больших таблицах для префиксных запросов надёжнее опираться на LIKE 'x%' с подходящим индексом, а STARTS_WITH использовать там, где читаемость важнее, либо в паре с дополнительным условием.
Ключевая деталь: обычный B-tree индекс по text использует правила сортировки локали, и LIKE 'x%' по нему ускоряется только в локали C. В остальных локалях нужен индекс с классом операторов text_pattern_ops:
CREATE INDEX idx_orders_status_pat
ON orders (status text_pattern_ops);
SELECT * FROM orders WHERE status LIKE 'ship%';
Проверяйте план через EXPLAIN: если видите Index Scan или Bitmap Index Scan вместо Seq Scan, индекс подхватился.
Эмуляция в других СУБД
Функции с именем STARTS_WITH нет в MySQL и в стандартном SQL, поэтому переносимый код обычно пишут на LIKE или LEFT.
В MySQL чаще всего используют LIKE (помните про экранирование % и _) или LEFT:
SELECT id, status FROM orders
WHERE LEFT(status, 4) = 'ship';
SELECT id, status FROM orders
WHERE status LIKE 'ship%';
Тонкость MySQL — регистр и сравнение зависят от collation колонки. При utf8mb4_general_ci сравнение нечувствительно к регистру по умолчанию, и LIKE 'ship%' совпадёт с 'Shipped'. Для строгого посимвольного сравнения используйте бинарную collation или BINARY.
В ClickHouse есть удобная функция startsWith, очень близкая к постгресовой:
SELECT id, status FROM orders
WHERE startsWith(status, 'ship');
Три реализации расходятся в трёх местах, и все они касаются именно проверки префикса. Регистр: STARTS_WITH в PostgreSQL и startsWith в ClickHouse сравнивают побайтово и регистрозависимы, а LIKE в MySQL зависит от collation колонки и по умолчанию регистр игнорирует. Пустой префикс: STARTS_WITH('abc', '') возвращает true, потому что любая строка начинается с пустого префикса; LEFT(col, 0) = '' тоже даёт true, так что эмуляция здесь совпадает. NULL: о нём ниже отдельно, потому что это самое частое расхождение между движками.
Если проверка префикса попадает в CHECK или в условие миграции, прогоните её на строках с NULL, пустым значением и многобайтовыми Unicode-символами в начале: то, как STARTS_WITH сравнивает их побайтово, должно совпасть с тем, что делает целевой движок. И помните, что обёртка вроде starts_with(lower(status), 'ship') или LEFT(status, 4) = 'ship' — это функция над колонкой, и без отдельного индекса по выражению она закрывает путь к обычному B-tree, как мы видели выше с text_pattern_ops.
И главная ловушка для переносимости. В Postgres STARTS_WITH(NULL, 'x') и STARTS_WITH('abc', NULL) возвращают NULL, а не false. В WHERE строка с NULL отсеется, как и при LIKE, но в SELECT-выражении или CASE NULL поведёт себя иначе, чем ожидаемый false. Поэтому для надёжности оборачивайте результат в COALESCE(starts_with(col, 'x'), false), когда от него зависит ветвление логики. Эмуляция через LEFT(col, n) = 'x' ведёт себя так же: NULL на входе даёт NULL, а не false.
Когда нужно проверить, что строка начинается с известного префикса — путь запроса, код страны, артикул — в PostgreSQL 11+ есть аккуратная функция
STARTS_WITH(str, prefix). Она читается лучше, чемLIKE 'x%', не требует экранирования спецсимволов и при правильном индексе работает так же быстро. Ниже разберём её поведение с регистром, индексами и аналоги в других СУБД.У
STARTS_WITHровно два аргумента: строка и префикс, и оба — обычные текстовые значения, а не шаблоны. Это отличает её отLIKE, где префикс одновременно и данные, и язык подстановок. ПоэтомуSTARTS_WITH(path, '/api/')совпадёт ровно с теми строками, что начинаются с/api/, и ни с чем больше, тогда какpath LIKE '/api/%'придётся читать с оглядкой на%и_внутри/api/. Функция доступна с PostgreSQL 11 и возвращаетboolean, так что её удобно ставить прямо вWHERE, вCASEили вCHECK-ограничение. Дальше разберём, чем именно она отличается отLIKEпо поведению и по скорости.STARTS_WITH против LIKE 'x%'
STARTS_WITH(str, prefix)возвращаетtrue, еслиstrначинается сprefix. Это эквивалентstr LIKE prefix || '%', но без подводных камней с шаблонами.SELECT starts_with('/api/orders', '/api/'); -- true SELECT starts_with('/admin', '/api/'); -- falseТипичный сценарий — фильтрация по маршруту в логах или по сегменту пути:
SELECT id, status, amount FROM orders WHERE starts_with(status, 'ship');Главное преимущество перед
LIKE— префикс берётся буквально. ВLIKEсимволы%и_имеют особое значение, поэтому проверка пути вроде'50%_off'требует экранирования. СSTARTS_WITHничего экранировать не нужно:-- LIKE treats % and _ as wildcards and needs escaping SELECT * FROM orders WHERE status LIKE '50\%%'; -- starts_with takes the prefix literally SELECT * FROM orders WHERE starts_with(status, '50%');То же удобно, когда префикс приходит из переменной приложения: с
STARTS_WITHего не нужно очищать от%и_, а значит, нет целого класса ошибок и неожиданных совпадений. Намерение читается с первого взгляда: «строка начинается с этого префикса», без мысленного разбора шаблона.Регистр и индексы
STARTS_WITHчувствителен к регистру:'API'и'api'— разные префиксы. Если нужно сравнение без учёта регистра, приводите обе стороны к одному регистру черезlower.-- case-sensitive: matches only the exact prefix case SELECT * FROM users WHERE starts_with(email, 'Admin@'); -- case-insensitive: normalize both sides SELECT * FROM users WHERE starts_with(lower(email), 'admin@');Теперь про производительность.
STARTS_WITHсам по себе планировщик не превращает в поиск по индексу так же охотно, какcol LIKE 'x%'. Поэтому на больших таблицах для префиксных запросов надёжнее опираться наLIKE 'x%'с подходящим индексом, аSTARTS_WITHиспользовать там, где читаемость важнее, либо в паре с дополнительным условием.Ключевая деталь: обычный B-tree индекс по
textиспользует правила сортировки локали, иLIKE 'x%'по нему ускоряется только в локалиC. В остальных локалях нужен индекс с классом операторовtext_pattern_ops:-- index that powers prefix search regardless of locale CREATE INDEX idx_orders_status_pat ON orders (status text_pattern_ops); -- this uses the index above SELECT * FROM orders WHERE status LIKE 'ship%';Проверяйте план через
EXPLAIN: если видитеIndex ScanилиBitmap Index ScanвместоSeq Scan, индекс подхватился.Эмуляция в других СУБД
Функции с именем
STARTS_WITHнет в MySQL и в стандартном SQL, поэтому переносимый код обычно пишут наLIKEилиLEFT.В MySQL чаще всего используют
LIKE(помните про экранирование%и_) илиLEFT:-- MySQL: prefix test via LEFT, no wildcards to escape SELECT id, status FROM orders WHERE LEFT(status, 4) = 'ship'; -- MySQL: same idea with LIKE SELECT id, status FROM orders WHERE status LIKE 'ship%';Тонкость MySQL — регистр и сравнение зависят от collation колонки. При
utf8mb4_general_ciсравнение нечувствительно к регистру по умолчанию, иLIKE 'ship%'совпадёт с'Shipped'. Для строгого посимвольного сравнения используйте бинарную collation илиBINARY.В ClickHouse есть удобная функция
startsWith, очень близкая к постгресовой:-- ClickHouse: native startsWith SELECT id, status FROM orders WHERE startsWith(status, 'ship');Три реализации расходятся в трёх местах, и все они касаются именно проверки префикса. Регистр:
STARTS_WITHв PostgreSQL иstartsWithв ClickHouse сравнивают побайтово и регистрозависимы, аLIKEв MySQL зависит от collation колонки и по умолчанию регистр игнорирует. Пустой префикс:STARTS_WITH('abc', '')возвращаетtrue, потому что любая строка начинается с пустого префикса;LEFT(col, 0) = ''тоже даётtrue, так что эмуляция здесь совпадает. NULL: о нём ниже отдельно, потому что это самое частое расхождение между движками.Если проверка префикса попадает в
CHECKили в условие миграции, прогоните её на строках с NULL, пустым значением и многобайтовыми Unicode-символами в начале: то, какSTARTS_WITHсравнивает их побайтово, должно совпасть с тем, что делает целевой движок. И помните, что обёртка вродеstarts_with(lower(status), 'ship')илиLEFT(status, 4) = 'ship'— это функция над колонкой, и без отдельного индекса по выражению она закрывает путь к обычному B-tree, как мы видели выше сtext_pattern_ops.И главная ловушка для переносимости. В Postgres
STARTS_WITH(NULL, 'x')иSTARTS_WITH('abc', NULL)возвращаютNULL, а неfalse. ВWHEREстрока сNULLотсеется, как и приLIKE, но вSELECT-выражении илиCASENULLповедёт себя иначе, чем ожидаемыйfalse. Поэтому для надёжности оборачивайте результат вCOALESCE(starts_with(col, 'x'), false), когда от него зависит ветвление логики. Эмуляция черезLEFT(col, n) = 'x'ведёт себя так же:NULLна входе даётNULL, а неfalse.