sqlpostgresqlstringsindexing

REVERSE в SQL: как развернуть строку и зачем это бывает нужно

Разбираем REVERSE как инструмент для работы с суффиксами, палиндромами и сегментами строк, включая Unicode и отличия PostgreSQL, MySQL и ClickHouse.

11 мин чтенияСправочникsql · postgresql · strings · indexing · mysql

REVERSE — это строковая функция, которая переворачивает строку задом наперёд.

Звучит как игрушка:

SELECT REVERSE('postgres') AS result;

Результат:

sergtsop

Но на практике REVERSE бывает полезен не только для забавных примеров. С его помощью можно:

  • проверить, является ли слово палиндромом;
  • достать последний кусок строки после разделителя;
  • развернуть текст для отчёта или отладки;
  • превратить поиск «заканчивается на» в поиск «начинается с»;
  • построить индексируемый поиск по суффиксу, например по домену email.

Главная идея такая: REVERSE меняет местами начало и конец строки. А это важно, потому что в SQL многие операции гораздо удобнее делать с начала строки, чем с конца.

Что делает REVERSE

Функция REVERSE принимает строку и возвращает эту же строку в обратном порядке.

SELECT REVERSE('hello') AS result;

Результат:

olleh

Ещё несколько простых примеров:

SELECT REVERSE('SQL') AS result;

Результат:

LQS
SELECT REVERSE('12345') AS result;

Результат:

54321
SELECT REVERSE('abc-def') AS result;

Результат:

fed-cba

Важно: REVERSE переворачивает именно символы строки. Он не понимает смысл текста, не знает, где слова, где домены, где фамилии, а где коды. Для него строка — это просто последовательность символов.

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

Синтаксис очень простой:

REVERSE(string)

Например:

SELECT REVERSE('database') AS reversed_text;

Результат:

esabatad

Можно применять функцию к колонке таблицы:

SELECT
    id,
    name,
    REVERSE(name) AS reversed_name
FROM users;

Если в таблице есть данные:

id | name
---+-------
1  | Alex
2  | Maria
3  | Ivan

результат будет таким:

id | name  | reversed_name
---+-------+--------------
1  | Alex  | xelA
2  | Maria | airaM
3  | Ivan  | navI

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

Что будет с NULL

Если передать в REVERSE значение NULL, результат тоже будет NULL.

SELECT REVERSE(NULL) AS result;

Результат:

NULL

Это обычное поведение SQL: если значения нет, то и переворачивать нечего.

Если вместо NULL нужна пустая строка, используйте COALESCE:

SELECT REVERSE(COALESCE(name, '')) AS reversed_name
FROM users;

COALESCE(name, '') означает: если name равен NULL, возьми пустую строку.

Простая проверка: палиндромы

Палиндром — это строка, которая читается одинаково слева направо и справа налево.

Например:

level
radar
anna

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

SELECT
    word,
    word = REVERSE(word) AS is_palindrome
FROM words;

Если данные такие:

word
------
level
hello
radar
sql

результат будет таким:

word  | is_palindrome
------+--------------
level | true
hello | false
radar | true
sql   | false

Для простых слов это работает отлично.

Палиндромы без учёта регистра

Есть нюанс: SQL сравнивает строки буквально. Поэтому Anna и annA — это разные строки.

SELECT 'Anna' = REVERSE('Anna') AS result;

REVERSE('Anna') вернёт:

annA

А значит сравнение даст false.

Чтобы не учитывать регистр, можно привести обе стороны к нижнему регистру через LOWER:

SELECT
    name,
    LOWER(name) = REVERSE(LOWER(name)) AS is_palindrome
FROM users;

Теперь Anna, ANNA и anna будут проверяться одинаково.

Пример:

SELECT LOWER('Anna') = REVERSE(LOWER('Anna')) AS result;

Результат:

true

Сначала строка превращается в anna, потом переворачивается в anna, и сравнение проходит успешно.

Палиндромы с пробелами и знаками препинания

Иногда палиндромом считается не одно слово, а целая фраза. Например:

А роза упала на лапу Азора

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

В PostgreSQL можно сначала нормализовать строку: привести к нижнему регистру и убрать всё лишнее.

Например для латиницы и цифр:

SELECT
    phrase,
    regexp_replace(LOWER(phrase), '[^a-z0-9]', '', 'g') AS normalized_phrase
FROM phrases;

Если была строка:

A man, a plan, a canal: Panama!

после нормализации получится:

amanaplanacanalpanama

Теперь можно сравнить нормализованную строку с её разворотом:

SELECT
    phrase,
    regexp_replace(LOWER(phrase), '[^a-z0-9]', '', 'g')
    =
    REVERSE(regexp_replace(LOWER(phrase), '[^a-z0-9]', '', 'g')) AS is_palindrome
FROM phrases;

Запрос длинный, но логика простая:

  1. Приводим фразу к нижнему регистру.
  2. Убираем пробелы и знаки препинания.
  3. Переворачиваем результат.
  4. Сравниваем исходную нормализованную строку с перевёрнутой.

Для новичка главное понять сам принцип: перед проверкой палиндрома строку часто нужно привести к единому виду.

Почему REVERSE полезен для поиска с конца

Самое интересное применение REVERSE — поиск по окончанию строки.

Например, нужно найти всех пользователей с email на @gmail.com.

Обычно хочется написать так:

SELECT id, email
FROM users
WHERE email LIKE '%@gmail.com';

Этот запрос понятный: найти email, которые заканчиваются на @gmail.com.

Но для базы данных это не самый удобный поиск. Шаблон начинается с %, а значит перед @gmail.com может быть что угодно:

alex@gmail.com
maria@gmail.com
some.long.name@gmail.com

Обычный B-tree индекс хорошо работает, когда поиск начинается с начала строки:

SELECT * FROM users WHERE email LIKE 'alex%';

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

И вот здесь появляется трюк с REVERSE.

Как суффикс превращается в префикс

Суффикс — это окончание строки.

Например, у email:

alex@gmail.com

суффикс:

@gmail.com

Если перевернуть email, получится:

moc.liamg@xela

Теперь бывший конец строки оказался в начале.

То есть поиск:

заканчивается на @gmail.com

после разворота превращается в:

начинается с moc.liamg@

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

Пример:

SELECT
    email,
    REVERSE(email) AS reversed_email
FROM users;

Результат:

email            | reversed_email
-----------------+-------------------
alex@gmail.com   | moc.liamg@xela
maria@gmail.com  | moc.liamg@airam
ivan@yahoo.com   | moc.oohay@navi

Теперь все Gmail-адреса начинаются с одинакового префикса:

moc.liamg@

Поиск email по домену через REVERSE

В PostgreSQL можно написать так:

SELECT id, email
FROM users
WHERE REVERSE(email) LIKE REVERSE('%@gmail.com');

Разберём, что происходит.

Обычный шаблон:

%@gmail.com

означает: любая строка, которая заканчивается на @gmail.com.

Если его перевернуть:

SELECT REVERSE('%@gmail.com');

получится:

moc.liamg@%

А это уже шаблон вида:

начинается с moc.liamg@

То есть:

SELECT * FROM users WHERE REVERSE(email) LIKE 'moc.liamg@%';

Так мы превратили поиск по окончанию в поиск по началу.

Индекс по развёрнутой строке

Сам по себе REVERSE в WHERE ещё не гарантирует ускорение. Если просто написать WHERE REVERSE(email) LIKE 'moc.liamg@%', база всё равно должна где-то взять результат REVERSE(email).

Чтобы такой поиск стал быстрым на большой таблице, в PostgreSQL можно создать функциональный индекс:

CREATE INDEX idx_users_email_reversed
ON users (REVERSE(email) text_pattern_ops);

После этого запрос:

SELECT id, email
FROM users
WHERE REVERSE(email) LIKE REVERSE('%@gmail.com');

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

Идея такая:

  • в таблице хранится обычный email;
  • в индексе база хранит результат REVERSE(email);
  • когда мы ищем по REVERSE(email), база может идти по индексу, а не пересчитывать всё с нуля для каждой строки.

Почему выражение должно совпадать

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

Если индекс создан так:

CREATE INDEX idx_users_email_reversed
ON users (REVERSE(email) text_pattern_ops);

то в запросе тоже должно быть REVERSE(email) LIKE ....

Если написать немного иначе, например REVERSE(LOWER(email)) LIKE ..., это уже другое выражение.

Для него нужен отдельный индекс:

CREATE INDEX idx_users_email_lower_reversed
ON users (REVERSE(LOWER(email)) text_pattern_ops);

И тогда запрос должен использовать такое же выражение:

SELECT id, email
FROM users
WHERE REVERSE(LOWER(email)) LIKE REVERSE(LOWER('%@gmail.com'));

Простое правило: если вы хотите, чтобы функциональный индекс помог, функция в WHERE должна быть такой же, как в индексе.

Когда индекс по REVERSE действительно нужен

Не нужно создавать такой индекс на всякий случай.

Он полезен, если у вас часто бывают запросы вида:

SELECT * FROM users WHERE email LIKE '%@gmail.com';

или:

SELECT * FROM files WHERE filename LIKE '%.pdf';

или:

SELECT * FROM links WHERE url LIKE '%.ru';

То есть когда вы регулярно ищете строки по окончанию.

Примеры задач:

  • найти пользователей по домену email;
  • найти файлы по расширению;
  • найти URL по доменной зоне;
  • найти коды, которые заканчиваются определённым суффиксом;
  • ускорить частый поиск «заканчивается на».

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

REVERSE не ускоряет обычный поиск по началу

Индекс по REVERSE(email) помогает искать по окончанию строки.

Но он не заменяет обычный индекс по email.

Например, такой запрос:

SELECT id, email
FROM users
WHERE email LIKE 'alex%';

ищет по началу email. Для него нужен обычный индекс по email, а не индекс по REVERSE(email).

Запомните:

  • email LIKE 'alex%' — поиск по началу, нужен обычный индекс;
  • email LIKE '%@gmail.com' — поиск по окончанию, может помочь индекс по REVERSE(email);
  • email LIKE '%gmail%' — поиск внутри строки, лучше смотреть в сторону других инструментов, например полнотекстового поиска или trigram-индексов в PostgreSQL.

REVERSE особенно хорош именно для строгого сценария «строка заканчивается на что-то».

Достать последний сегмент строки

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

Например, есть домен:

mail.corp.example.com

Нужно получить последнюю часть:

com

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

SELECT REVERSE(SPLIT_PART(REVERSE('mail.corp.example.com'), '.', 1)) AS last_part;

Результат:

com

Разберём по шагам.

Исходная строка:

mail.corp.example.com

После REVERSE:

moc.elpmaxe.proc.liam

Теперь первый сегмент до точки:

moc

Разворачиваем его обратно:

com

И получаем последний сегмент исходной строки.

Пример с расширением файла

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

filename
----------------
report.pdf
photo.jpg
archive.tar.gz
README

Нужно достать расширение после последней точки.

SELECT
    filename,
    REVERSE(SPLIT_PART(REVERSE(filename), '.', 1)) AS extension
FROM files;

Результат:

filename       | extension
---------------+----------
report.pdf     | pdf
photo.jpg      | jpg
archive.tar.gz | gz
README         | README

Последний пример показывает ограничение: если в строке нет точки, SPLIT_PART вернёт всю строку. Поэтому для реального отчёта лучше добавить проверку.

Например:

SELECT
    filename,
    CASE
        WHEN filename LIKE '%.%' THEN REVERSE(SPLIT_PART(REVERSE(filename), '.', 1))
        ELSE NULL
    END AS extension
FROM files;

Теперь для README будет NULL, потому что расширения нет.

filename       | extension
---------------+----------
report.pdf     | pdf
photo.jpg      | jpg
archive.tar.gz | gz
README         | NULL

Так результат становится честнее.

Последний сегмент URL или пути

Та же идея работает с URL и путями.

Например, есть путь:

/catalog/products/42

Нужно получить последнюю часть:

42

Запрос:

SELECT REVERSE(SPLIT_PART(REVERSE('/catalog/products/42'), '/', 1)) AS last_segment;

Результат:

42

Или для таблицы:

SELECT
    path,
    REVERSE(SPLIT_PART(REVERSE(path), '/', 1)) AS last_segment
FROM pages;

Если данные такие:

path
---------------------
/catalog/products/42
/users/15
/articles/sql/reverse

результат будет таким:

path                  | last_segment
----------------------+-------------
/catalog/products/42  | 42
/users/15             | 15
/articles/sql/reverse | reverse

Это простой способ «читать строку с конца», когда обычные функции удобнее работают с началом строки.

Важно: REVERSE не переворачивает элементы списка

Допустим, есть строка:

mon,tue,wed

Если применить к ней REVERSE, получится:

SELECT REVERSE('mon,tue,wed') AS result;

Результат:

dew,eut,nom

Это не список в обратном порядке. Это просто вся строка, прочитанная справа налево.

Если вам нужно получить:

wed,tue,mon

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

В PostgreSQL это можно сделать так:

SELECT string_agg(part, ',' ORDER BY ord DESC) AS reversed_list
FROM unnest(string_to_array('mon,tue,wed', ',')) WITH ORDINALITY AS t(part, ord);

Результат:

wed,tue,mon

Здесь происходит несколько шагов:

  1. string_to_array('mon,tue,wed', ',') превращает строку в массив.
  2. unnest(...) WITH ORDINALITY разворачивает массив в строки и добавляет номер элемента.
  3. ORDER BY ord DESC сортирует элементы в обратном порядке.
  4. string_agg(part, ',') склеивает их обратно в строку.

Это важная разница:

SELECT REVERSE('mon,tue,wed');

даёт:

dew,eut,nom

А разворот элементов списка даёт:

wed,tue,mon

REVERSE переворачивает символы, а не смысловые части строки.

REVERSE и Unicode

С обычной латиницей и кириллицей REVERSE обычно ведёт себя ожидаемо.

Например:

SELECT REVERSE('Привет') AS result;

Результат:

тевирП

Но с Unicode есть тонкость.

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

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

REVERSE обычно переставляет именно технические символы, а не «видимые человеческие буквы» как единые графемы. Поэтому в сложных Unicode-строках результат может выглядеть странно.

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

REVERSE в PostgreSQL

В PostgreSQL функция называется REVERSE.

Пример:

SELECT REVERSE('postgres') AS result;

Результат:

sergtsop

Функцию можно использовать в SELECT:

SELECT
    id,
    email,
    REVERSE(email) AS email_reversed
FROM users;

В WHERE:

SELECT id, email
FROM users
WHERE REVERSE(email) LIKE REVERSE('%@gmail.com');

И в функциональном индексе:

CREATE INDEX idx_users_email_reversed
ON users (REVERSE(email) text_pattern_ops);

Для поиска по шаблону LIKE в PostgreSQL иногда важен операторный класс text_pattern_ops, особенно если база работает не в локали C. Если вы только учитесь, можно запомнить проще: для индексируемого префиксного LIKE по тексту в PostgreSQL часто используют специальный индекс с text_pattern_ops.

REVERSE в MySQL

В MySQL функция тоже называется REVERSE.

SELECT REVERSE('mysql') AS result;

Результат:

lqsym

Её можно применять к колонкам:

SELECT
    id,
    email,
    REVERSE(email) AS email_reversed
FROM users;

Для поиска по окончанию идея такая же:

SELECT id, email
FROM users
WHERE REVERSE(email) LIKE REVERSE('%@gmail.com');

Но с индексами нужно быть внимательнее. В MySQL часто используют generated column: отдельную вычисляемую колонку, где хранится развёрнутый email, и индекс уже по ней.

Например идея может выглядеть так:

ALTER TABLE users
ADD COLUMN email_reversed VARCHAR(255)
GENERATED ALWAYS AS (REVERSE(email)) STORED;

CREATE INDEX idx_users_email_reversed
ON users (email_reversed);

После этого можно искать так:

SELECT id, email
FROM users
WHERE email_reversed LIKE REVERSE('%@gmail.com');

Такой подход делает поиск по окончанию более удобным для индексации.

REVERSE в ClickHouse

В ClickHouse есть разные варианты разворота строки.

Для простых байтовых строк может использоваться reverse, а для UTF-8 строк лучше использовать reverseUTF8.

Пример для обычной ASCII-строки:

SELECT reverse('clickhouse') AS result;

Результат:

esuohkcilc

Для русских строк лучше использовать UTF-8 вариант:

SELECT reverseUTF8('Привет') AS result;

Результат:

тевирП

Если использовать байтовый разворот на не-ASCII строках, можно получить испорченную последовательность байтов и некорректный текст. Поэтому простое правило такое:

  • для ASCII-строк можно использовать reverse;
  • для кириллицы и другого UTF-8 текста лучше использовать reverseUTF8.

Что тестировать при переносе между СУБД

На простых строках всё выглядит одинаково:

hello
postgres
12345

Но реальные проблемы обычно появляются на краях.

Перед переносом логики между PostgreSQL, MySQL и ClickHouse стоит проверить такие значения:

hello
Привет
alex@gmail.com
mail.corp.example.com
a.b.c
''
NULL
🙂
é

Особенно важно проверить:

  • пустую строку;
  • NULL;
  • кириллицу;
  • эмодзи;
  • символы с акцентами;
  • строки с несколькими разделителями;
  • строки без разделителя;
  • email и домены, по которым будет поиск.

Так вы заранее увидите, где функция работает ожидаемо, а где нужно добавить отдельную обработку.

Когда REVERSE лучше не использовать

REVERSE полезен, но не стоит применять его везде подряд.

Он не нужен, если задача проще решается обычными функциями.

Например, если вам нужно просто проверить, что email начинается с alex, пишите:

SELECT * FROM users WHERE email LIKE 'alex%';

Не нужно разворачивать строку.

Если нужно найти подстроку в любом месте:

SELECT * FROM users WHERE email LIKE '%gmail%';

REVERSE тоже не решает задачу идеально. Для такого поиска лучше подходят другие инструменты, например trigram-индексы в PostgreSQL.

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

REVERSE особенно хорош там, где вы действительно хотите посмотреть на строку с конца.

Частая ошибка: думать, что REVERSE понимает структуру строки

Допустим, есть строка:

a,b,c

REVERSE не знает, что это список из трёх элементов. Он просто перевернёт символы:

SELECT REVERSE('a,b,c') AS result;

Результат:

c,b,a

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

Если строка такая:

mon,tue,wed

то результат будет уже не тем, что нужно:

dew,eut,nom

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

Практический пример: найти пользователей с Gmail

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

id | email
---+-------------------
1  | alex@gmail.com
2  | maria@yahoo.com
3  | ivan@gmail.com
4  | test@company.com

Обычный запрос:

SELECT id, email
FROM users
WHERE email LIKE '%@gmail.com';

Результат:

id | email
---+----------------
1  | alex@gmail.com
3  | ivan@gmail.com

Для маленькой таблицы этого достаточно.

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

SELECT id, email
FROM users
WHERE REVERSE(email) LIKE REVERSE('%@gmail.com');

Логически результат будет тем же:

id | email
---+----------------
1  | alex@gmail.com
3  | ivan@gmail.com

Но теперь окончание email превратилось в начало перевёрнутой строки. А это уже можно индексировать через функциональный индекс в PostgreSQL или через вычисляемую колонку в других СУБД.

Практический пример: получить доменную зону

Допустим, есть домены:

host
----------------------
google.com
mail.example.org
school.quality-academy.ru

Нужно получить последнюю часть после точки:

com
org
ru

Запрос:

SELECT
    host,
    REVERSE(SPLIT_PART(REVERSE(host), '.', 1)) AS tld
FROM domains;

Результат:

host                      | tld
--------------------------+-----
google.com                | com
mail.example.org          | org
school.quality-academy.ru | ru

Здесь tld — это top-level domain, доменная зона.

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

Практический пример: проверить коды, которые заканчиваются на суффикс

Допустим, у заказов есть код:

order_code
------------
QA-2026-001-RU
QA-2026-002-KZ
QA-2026-003-RU
QA-2026-004-GE

Нужно найти коды, которые заканчиваются на -RU.

Обычный запрос:

SELECT order_code
FROM orders
WHERE order_code LIKE '%-RU';

Через REVERSE:

SELECT order_code
FROM orders
WHERE REVERSE(order_code) LIKE REVERSE('%-RU');

REVERSE('%-RU') превратится в:

UR-%

То есть база будет искать перевёрнутые строки, которые начинаются с UR-.

На маленьких таблицах разницы почти не будет. На больших таблицах и при правильном индексе такой подход может быть полезен.

Коротко

REVERSE — это функция, которая возвращает строку в обратном порядке.

SELECT REVERSE('postgres');

Результат:

sergtsop

Она полезна, когда нужно:

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

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

  • REVERSE переворачивает символы, а не слова и не элементы списка;
  • NULL на входе даёт NULL на выходе;
  • для простых строк функция ведёт себя предсказуемо;
  • для сложного Unicode лучше отдельно тестировать результат;
  • поиск LIKE '%suffix' можно переписать через REVERSE, чтобы суффикс стал префиксом;
  • для ускорения такого поиска нужен подходящий индекс;
  • индекс по REVERSE(column) помогает искать по окончанию, но не заменяет обычный индекс по самой колонке.

Самая полезная мысль: REVERSE нужен не только для того, чтобы «перевернуть слово». Он помогает посмотреть на строку с конца, а многие задачи после этого становятся гораздо проще.

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

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

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