Ошибки, которые не падают, а врут
Худший сорт ошибок - не те, что роняют запрос, а те, что молча возвращают неверный результат. Ниже - типовые. Каждая «работает» на демо-данных и проявляется на реальных.
NOT IN + NULL
Если в подзапросе появится хоть один NULL, NOT IN вернёт пусто или
выкинет строки - из-за трёхзначной логики SQL.
-- НЕ ТАК: один NULL в списке - и результат пустой
SELECT * FROM flights
WHERE flight_id NOT IN (SELECT flight_id FROM tickets);
-- ТАК: устойчиво к NULL и обычно ещё и быстрее
SELECT * FROM flights f
WHERE NOT EXISTS (SELECT 1 FROM tickets t WHERE t.flight_id = f.flight_id);
Правило: для «нет среди» бери NOT EXISTS, а не NOT IN.
BETWEEN по времени
BETWEEN включает обе границы. Для дат это ловушка:
-- НЕ ТАК: поймает и полночь следующего дня
WHERE booked_at BETWEEN '2026-01-01' AND '2026-01-31'
-- ТАК: полуинтервал [начало, начало следующего)
WHERE booked_at >= '2026-01-01' AND booked_at < '2026-02-01'
Полуоткрытый интервал >= ... < ... не зависит от того, есть ли в дне
доли секунды, и не считает одну запись дважды на стыке периодов.
COUNT по nullable-колонке
COUNT(*) считает строки. COUNT(col) считает строки, где col
НЕ NULL. Это разные числа, и второе часто пишут, имея в виду первое.
SELECT COUNT(*) FROM tickets; -- все строки
SELECT COUNT(passenger) FROM tickets; -- только где passenger IS NOT NULL
Целочисленное деление
int / int даёт int: дробная часть отбрасывается до всякого
округления.
SELECT 5 / 2; -- 2, не 2.5
SELECT 5 / 2.0; -- 2.5
SELECT 5::numeric / 2; -- 2.5000000000000000
Считаешь доли, проценты, средние - приводи к numeric/float хотя бы
один операнд заранее.
Типы, которые создают проблемы на ровном месте
- char(n) дополняет значение пробелами до длины n и сравнивается с
усечением хвостовых пробелов. Сюрпризы при сравнении и конкатенации.
Бери
textилиvarchar- в PostgreSQL они не медленнее. - money зависит от локали
lc_monetaryи плохо переносится. Для денег -numeric(p, s)с явной точностью. - timestamp без time zone хранит «голое» время без привязки к зоне.
На сервере и клиенте в разных зонах получишь разные толкования одного
значения. По умолчанию бери
timestamptz. - serial - это старый синтаксический сахар над sequence; у него
есть нюансы с правами и зависимостями. В новом коде - стандартный
GENERATED ALWAYS AS IDENTITY.
Почему WHERE col::date = ... мешает индексу - это про sargability (см.
sargability) и планировщик. Как находить такие запросы в проде -
pg-stat-map.