Ловушки NULL, дат и повторов

Почему = NULL и NOT IN молча ломают запрос, как не потерять последний день месяца и откуда берутся повторы.

В этом блоке три базы: «Библиотека» — книги и выдачи, и знакомые «Интернет-магазин» и «Компания». База каждой задачи указана над условием.

NULL — «неизвестно»

NULL — не ноль и не пустая строка, а «значение неизвестно». Любое сравнение с NULL даёт не «истину» и не «ложь», а тоже NULL, и WHERE такую строку не пропускает. Отсюда три ловушки:

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

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;

Задачи

  1. Задача 36. Книги на руках
  2. Задача 37. Должники
  3. Задача 38. Читатели без выдач
  4. Задача 39. Все, кроме москвичей
  5. Задача 40. Заказы за март
  6. Задача 41. Заказы по месяцам
  7. Задача 42. Города покупателей
  8. Задача 43. Промокоды
  9. Задача 44. Дубли учёток