Почему = NULL и NOT IN молча ломают запрос, как не потерять последний день месяца и откуда берутся повторы.
В этом блоке три базы: «Библиотека» — книги и выдачи, и знакомые «Интернет-магазин» и «Компания». База каждой задачи указана над условием.
NULL — не ноль и не пустая строка, а «значение неизвестно». Любое сравнение с NULL даёт не «истину» и не «ложь»,
а тоже NULL, и WHERE такую строку не пропускает. Отсюда три ловушки:
WHERE returned_at = NULL не найдёт ничего. Пустые значения проверяют IS NULL, заполненные — IS NOT NULL.WHERE city <> 'Москва' молча выбросит клиентов без города: для них условие не истинно. Если они нужны —
city IS DISTINCT FROM 'Москва' или city <> 'Москва' OR city IS NULL.x NOT IN (подзапрос) даст пустой результат, если подзапрос вернул хотя бы один NULL: «не равно неизвестному»
не бывает истинным. Для строк без пары пишите NOT EXISTS.SELECT count(*) AS reading_room
FROM loans
WHERE reader_id IS NULL;
coalesce(a, b) возвращает первое значение, которое не NULL, — так пустому значению дают подпись:
coalesce(city, 'не указан'). nullif(a, b) делает наоборот — превращает b в NULL: nullif(promo_code, '').
date — день, timestamp — день и время. В сравнении с timestamp строка '2026-03-31' означает полночь
31 марта, поэтому created_at BETWEEN '2026-03-01' AND '2026-03-31' теряет почти весь последний день месяца.
Надёжно — полуинтервал: начало включаем, конец — нет.
SELECT id, created_at
FROM shop.orders
WHERE created_at >= '2026-03-30' AND created_at < '2026-04-01';
Здесь перед таблицей указана база: shop.orders. Так можно обратиться к таблице другой учебной базы из любой
задачи; в самих задачах это не нужно.
| Выражение | Что делает |
|---|---|
date_trunc('month', created_at) |
начало месяца: 2026-03-17 14:05 → 2026-03-01 00:00 |
created_at::date |
только дата, без времени |
extract(year FROM created_at) |
год числом |
due_at - issued_at |
разность двух date — число дней |
issued_at + 14 |
дата через 14 дней |
DATE '2026-04-01' |
дата-константа |
«Сегодня» в задачах этого блока — 1 апреля 2026 года. В рабочих запросах вместо константы пишут current_date,
но тогда ответ менялся бы каждый день.
DISTINCT убирает повторяющиеся строки результата — целиком, по всем колонкам. count(DISTINCT колонка) считает
разные значения. Если повторы появились после JOIN, сначала спросите себя, нужен ли JOIN: часто правильнее
EXISTS.
SELECT count(*) AS loans, count(DISTINCT reader_id) AS readers
FROM loans;
Для PostgreSQL '[email protected]' и '[email protected]' — разные строки: DISTINCT и GROUP BY их
не объединят, хотя человек видит одну и ту же почту. Перед сравнением такие значения приводят к одному виду:
lower() переводит строку в маленькие буквы, upper() — в большие. Сколько в магазине разных почт и сколько —
без учёта регистра:
SELECT count(DISTINCT email) AS emails, count(DISTINCT lower(email)) AS mailboxes
FROM shop.customers;