Все главы учебника
Содержание учебника
Глава 03 / Базы данных

SELECT: фильтры, NULL и устойчивый порядок

14 мин чтенияКонтент v0.10.0

Вера спрашивает, какие книги пора напомнить вернуть. Общий список выдач содержит и завершённую историю Бориса, и две активные выдачи Анны. Вернуть все строки и отфильтровать их случайно в браузере неудобно: нужен точный вопрос к базе. Мы сначала определим нужные факты, затем запишем условия и проверим каждый результат на исходном seed.

Запрос описывает результат

SELECT выбирает поля ответа, FROM — источник строк, WHERE — условие отбора. В этом уроке читаем одну таблицу за раз. Код выглядит английским текстом, но ключевые слова имеют точную грамматику. SQL-литерал текста записывают в одинарных кавычках. Двойные кавычки обозначают имя объекта; значение «Анна» не следует превращать в имя колонки.

SELECT id, member_id, copy_id, due_on
FROM club.loans
WHERE returned_on IS NULL
ORDER BY due_on, id;

Результат состоит из двух строк: 1000,1,100,2026-09-15 и 1002,1,101,2026-09-17. Условия читаются как договор: нужна активная выдача; дата задаёт основной порядок; id разрешает совпадение дат. Формальный логический смысл запроса не означает, что движок физически выполнит действия в этом же порядке. Позже EXPLAIN покажет выбранный путь.

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

Неизвестное значение не равно пустому

У Бориса email=NULL. Сравнение email = NULL не превращается в проверку отсутствия. Обычно оно даёт неизвестный результат; WHERE сохраняет строки с true, а неизвестное отбрасывает. IS NULL и IS NOT NULL задают нужную проверку явно. Пустая строка, пробел и NULL различаются: каждое значение требует своей предметной политики.

SELECT id, name FROM club.members
WHERE email IS NULL ORDER BY id;
SELECT id, name FROM club.members
WHERE email = NULL ORDER BY id;

Первая команда возвращает 2,Борис. Вторая не возвращает строк. Это не отсутствие Бориса в таблице: ошибочно сформулировано условие. При NOT, AND и OR неизвестность также влияет на результат. Например, «email не равен адресу Анны» не обязательно включает строку с неизвестным email; если нужны оба случая, запишите email <> 'anna@example.test' OR email IS NULL.

COALESCE выбирает первое ненулевое значение и полезен для отображения. Он не меняет исходное отсутствие факта и не должен скрывать его в проверке правил. Заменить неизвестную дату нулём значит придумать новую дату, которая потом попадёт в отчёт. Для показа подписи можно выбрать отдельный текст, сохранив NULL в модели.

SELECT id, COALESCE(email, 'не указан') AS contact
FROM club.members ORDER BY id;

Борис получает подпись «не указан», остальные — свои адреса. Alias contact — имя колонки результата, не новая сохранённая колонка. Если запрос возвращает вычисление, выражение также имеет тип; смешивать дату и произвольный текст в одной ветке без преобразования не получится.

Порядок и граница страницы

SQL не обещает порядок строк без ORDER BY. Первичный ключ помогает идентифицировать строку, но не является командой сортировки любого SELECT. Для первых двух участников используем определённую последовательность:

SELECT id, name FROM club.members ORDER BY id LIMIT 2;
SELECT id, name FROM club.members
WHERE id > 2 ORDER BY id LIMIT 2;

Первая страница содержит Анну и Бориса, вторая — Веру. Это keyset pagination по простому ключу. При сортировке по дате добавляют tie-breaker ID и передают соответствующую пару. OFFSET тоже существует, но глубокая страница может заставлять просматривать и отбрасывать много строк. Между страницами конкурентные изменения создают отдельный вопрос о снимке и допустимых пропусках; один LIMIT не решает его.

Для NULL в сортируемом поле задайте нужный договор NULLS FIRST или NULLS LAST, когда отсутствие должно иметь определённое место. Не используйте наблюдаемый порядок маленькой таблицы как тест скрытых правил. Результат должен быть воспроизводим благодаря самому запросу.

Даты и проверяемое «сейчас»

Для напоминаний возьмём фиксированную дату 2026-09-18. Активная выдача считается просроченной, если due_on строго меньше этой даты. Выдача со сроком ровно в этот день пока не просрочена по выбранному договору. В production дату передаёт приложение либо запрос использует согласованное время; timezone и календарная политика должны быть известны.

SELECT id, due_on FROM club.loans
WHERE returned_on IS NULL
  AND due_on < DATE '2026-09-18'
ORDER BY due_on, id;

Ожидаются 1000 и 1002. Строка 1001 не включается, хотя её срок тоже раньше контрольной даты: она возвращена. Если убрать первое условие, отчёт меняет смысл. Полезно проверять условия отдельно, а затем их пересечение: так легче найти лишнюю строку.

Самостоятельная лаборатория

Отведите 20–30 минут. Получите участников без email, выдачи со сроком между 15 и 17 сентября включительно и следующую страницу участников после ID=1. Перед выполнением выпишите ожидаемые ID. Затем объясните, попадёт ли NULL email в NOT(email='anna@example.test') и почему пустая строка дала бы другой факт.

Критерии: все ответы имеют явную сортировку; диапазон дней сформулирован без скрытой части времени; отсутствие проверяется IS NULL; первая/следующая страницы используют один порядок. Не добавляйте DISTINCT, чтобы скрыть неизвестную причину: в запросе одной таблицы этого урока удвоение ещё не возникает.

Разбор

Без email найден Борис. Диапазон due_on содержит выдачи 1000,1001,1002; только добавление условия активности убирает 1001. После id=1 следующая страница возвращает Бориса и Веру. Неизвестность сравнения не становится true после обычного NOT. Когда нужно ответить с названием книги или именем читателя, одной таблицы уже недостаточно; следующим шагом научимся соединять факты.

Первичные источники

PostgreSQL: Querying a Table, Comparison Functions, Sorting Rows.