Структура запроса, NULL, поиск, сортировка и выдача данных страницами
SQL — язык, на котором клиент описывает, какие данные хочет получить или
изменить. В этом уроке работаем с SELECT: командой чтения данных.
SQL называют декларативным языком. Вы описываете нужный результат, а PostgreSQL сам выбирает способ его получить. Например, запрос говорит «покажи товары дороже 1000 рублей», но не диктует, в каком порядке читать файлы таблицы.
Следующий запрос читает таблицу marketplace.products:
SELECT
p.id,
p.sku,
p.name,
p.price,
p.price * 1.20 AS price_with_tax
FROM marketplace.products AS p;FROM указывает источник строк;SELECT перечисляет столбцы результата;AS p задаёт таблице короткое имя;AS price_with_tax даёт имя вычисленному столбцу.Короткое имя называют псевдонимом (alias). Оно не переименовывает объект
в базе и действует только внутри запроса.
SELECT * выбирает все столбцы. Это удобно для знакомства с таблицей в
psql, но в коде приложения лучше перечислять столбцы: добавление нового поля
не изменит ответ незаметно.
Запрос пишут начиная с SELECT, но логически PostgreSQL рассматривает его
части в другом порядке:
FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMITСначала определяются исходные строки, затем фильтр, группировка, выбранные
столбцы и порядок. Поэтому псевдоним, созданный в SELECT, обычно ещё нельзя
использовать в WHERE: на логическом этапе фильтрации его не существует.
Внутри сервер может выполнить работу иначе ради скорости, но результат обязан соответствовать этому смыслу.
WHERE содержит условие отбора. Такое условие называют предикатом.
SELECT id, name, price
FROM marketplace.products
WHERE price >= 1000;В результат попадут только строки, для которых условие равно TRUE.
NULL не равен пустой строке, нулю или слову «нет». Он означает, что значения
нет либо оно неизвестно. Проверять его нужно через IS NULL:
SELECT id
FROM app_users
WHERE email IS NULL;Выражение email = NULL даёт не TRUE и не FALSE, а третье логическое
значение UNKNOWN — «неизвестно». WHERE оставляет только TRUE, поэтому
такая запись не найдёт строки.
Когда два значения нужно сравнить так, чтобы NULL считался обычным отдельным
состоянием, используется IS DISTINCT FROM:
SELECT old_value IS DISTINCT FROM new_value AS changed;У NOT IN есть связанная ловушка: один NULL в проверяемом наборе может
сделать итог неизвестным. Для поиска строк без соответствия обычно надёжнее
NOT EXISTS; он подробно разобран в уроке о подзапросах.
BETWEEN включает обе границы. Для выборки событий за март удобнее явно
включить начало и исключить начало следующего месяца:
SELECT id, occurred_at
FROM app_events
WHERE occurred_at >= TIMESTAMPTZ '2026-03-01 00:00+00'
AND occurred_at < TIMESTAMPTZ '2026-04-01 00:00+00';Такой полуоткрытый интервал не зависит от точности времени и не посчитает полночь 1 апреля одновременно в двух месяцах.
LIKE сравнивает текст с шаблоном. Символ % означает любое количество
символов, а _ — ровно один. ILIKE делает то же без учёта регистра.
SELECT id, name
FROM products
WHERE name ILIKE 'postgres%';Запрос найдёт строки, которые начинаются с postgres. Если % и _ введены
пользователем и должны означать сами символы, их нужно экранировать.
Параметризованный запрос защищает от внедрения SQL-кода, но не отменяет особый
смысл символов шаблона.
Поиск вида '%term%' обычный B-tree-индекс чаще всего не ускоряет. Для поиска
по фрагментам позже разберём расширение pg_trgm, а для поиска по словам —
полнотекстовый поиск.
DISTINCT оставляет только разные сочетания выбранных значений:
SELECT DISTINCT org_id, status
FROM orders;Если две строки имеют одинаковые org_id и status, в результате останется
одна. DISTINCT не исправляет ошибочную связь таблиц и не должен использоваться
как универсальное средство «убрать лишние строки».
Конструкция PostgreSQL DISTINCT ON выбирает первую строку каждой группы.
Нужно явно задать, что значит «первая»:
SELECT DISTINCT ON (order_id)
order_id, status, created_at, id
FROM payments
ORDER BY order_id, created_at DESC, id DESC;Так запрос выбирает последний платёж заказа. id разрешает ничью, если время
двух платежей совпало.
Без ORDER BY PostgreSQL не обещает никакого порядка. Сегодня строки могут
выглядеть отсортированными, а после обновления, нового плана или создания
индекса порядок изменится.
SELECT id, created_at, status
FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 50;DESC означает порядок от большего к меньшему. Второй столбец id делает
порядок полным: даже строки с одинаковым created_at располагаются однозначно.
Если столбец допускает NULL, выберите NULLS FIRST или NULLS LAST.
LIMIT 50 OFFSET 1000 означает «пропусти 1000 строк и верни следующие 50».
Чем дальше страница, тем больше строк сервер должен найти и отбросить. Кроме
того, новые и удалённые строки сдвигают границы страниц.
Для последовательной выдачи используют продолжение от последней увиденной
строки. Этот способ называют пагинацией по ключу (keyset pagination):
SELECT id, created_at, status
FROM orders
WHERE org_id = $1
AND (created_at, id) < ($2, $3)
ORDER BY created_at DESC, id DESC
LIMIT 50;$1, $2 и $3 — параметры, которые безопасно передаст приложение. Вторая
страница получает время и id последней строки первой страницы. Для такого
запроса подходит B-tree-индекс (org_id, created_at DESC, id DESC).
OFFSET остаётся нормальным выбором для небольших служебных списков и прямого
перехода к условному номеру страницы, если объём невелик.
В лаборатории 05 вы построите страницу каталога с однозначным порядком и вычисляемой ценовой категорией. Проверка сравнит не только набор, но и точную последовательность идентификаторов.
Перед следующим уроком убедитесь, что можете объяснить:
email = NULL не работает как проверка отсутствия значения?ORDER BY нельзя полагаться на порядок строк?created_at в сортировке добавляют id?OFFSET?Подробнее: SELECT, сравнение значений, поиск по шаблону.
Далее: Как безопасно менять структуру базы