# Каталог анти-паттернов (cheat sheet) _Эксплуатация и наблюдаемость · PostgreSQL Knowledge Base_ **TL;DR:** Набор ошибок, которые компилируются и проходят тесты, но врут на проде. NOT IN с NULL глотает строки. BETWEEN по времени ловит лишнюю границу. COUNT по nullable-колонке считает не то. Целочисленное деление режет дробь. char(n), money и timestamp без TZ создают проблемы на пустом месте. ## Ошибки, которые не падают, а врут Худший сорт ошибок - не те, что роняют запрос, а те, что молча возвращают неверный результат. Ниже - типовые. Каждая «работает» на демо-данных и проявляется на реальных. ## NOT IN + NULL Если в подзапросе появится хоть один NULL, `NOT IN` вернёт пусто или выкинет строки - из-за трёхзначной логики SQL. ```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` включает обе границы. Для дат это ловушка: ```sql -- НЕ ТАК: поймает и полночь следующего дня 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. Это разные числа, и второе часто пишут, имея в виду первое. ```sql SELECT COUNT(*) FROM tickets; -- все строки SELECT COUNT(passenger) FROM tickets; -- только где passenger IS NOT NULL ``` ## Целочисленное деление `int / int` даёт `int`: дробная часть отбрасывается до всякого округления. ```sql 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](/courses/postgres/kb/sargability.md)) и планировщик. Как находить такие запросы в проде - [pg-stat-map](/courses/postgres/kb/pg-stat-map.md). ## Команды ```sql SELECT 5 / 2, 5 / 2.0; ``` Целочисленное против дробного деления: 2 и 2.5 ```sql SELECT COUNT(*), COUNT(passenger) FROM tickets; ``` COUNT(*) считает строки, COUNT(col) - только не-NULL значения ```sql SELECT 'a'::char(3) = 'a '; ``` char(n) дополняет пробелами - сравнение игнорирует хвостовые пробелы ## См. также - [Карта pg_stat_* и pg_stat_statements](/courses/postgres/kb/pg-stat-map.md) - [Конфигурация памяти в контексте механизма](/courses/postgres/kb/memory-config.md) - [Безопасность движка: pg_hba, роли, SECURITY DEFINER](/courses/postgres/kb/engine-security.md) - [Sargability: почему индекс не используется](/courses/postgres/kb/sargability.md)