PostgreSQL
SELECT: фильтрация, сортировка, LIMIT
`SELECT * FROM orders WHERE status NOT IN ('cancelled', NULL)` - этот запрос вернёт 0 строк независимо от данных в таблице. NULL в IN-списке разворачивается в `status != NULL`, что даёт UNKNOWN для каждой строки. Трёхзначная булева алгебра SQL уничтожает результаты тихо, без ошибки.
- **Pagination API** - переход с OFFSET на keyset pagination ускоряет загрузку страницы 500 с 2 секунд до 5 мс
- **Analytics dashboard** - `DISTINCT ON` заменяет подзапрос с ROW_NUMBER, упрощая запрос «последнее событие на пользователя»
- **Финансовые отчёты** - NULLIF защищает от `division by zero` при агрегации нулевых периодов
WHERE: операторы и типы
WHERE фильтрует строки до агрегации и группировки. Выбор оператора влияет не только на корректность результата, но и на то, сможет ли планировщик использовать индекс.
**LIKE с leading wildcard не использует B-tree индекс.** `LIKE '%gmail.com'` сделает Seq Scan по всей таблице. Для поиска суффиксов и подстрок нужен `pg_trgm` + GIN индекс: `CREATE INDEX ON users USING GIN (email gin_trgm_ops);`
Неявное приведение типов (implicit cast) иногда ломает индекс. Колонка `id` типа `int4`, но запрос передаёт строку:
**EXISTS vs IN** при большом подзапросе: `IN` материализует весь подзапрос в памяти. `EXISTS` останавливается при первом совпадении и часто строит более эффективный план.
**ALL** - обратная сторона ANY: `amount > ALL(ARRAY[10, 20, 50])` - больше каждого элемента. Используется редко, но полезно для защитных проверок.
Колонка `user_id` имеет тип `int4` и B-tree индекс. Какой запрос гарантированно использует индекс?
ORDER BY, LIMIT, OFFSET и keyset pagination
ORDER BY определяет порядок строк в результате. Без него порядок не гарантирован - PostgreSQL вернёт строки в том порядке, который удобен планировщику (часто по heap scan, но не всегда).
**OFFSET масштабируется плохо.** Чтобы вернуть страницу 100 при OFFSET 9900 LIMIT 100, PostgreSQL читает и отбрасывает 9 900 строк - работа проделана, но результат выброшен.
| Метод | Скорость при стр. 1 | Скорость при стр. 500 | Произвольный переход | Поддержка новых строк |
|---|---|---|---|---|
| OFFSET/LIMIT | быстро | медленно (O(n)) | да | нет (дубликаты) |
| Keyset (cursor) | быстро | быстро (O(1)) | нет | да (стабильно) |
Keyset pagination требует стабильного уникального ключа сортировки (обычно `id` или `(created_at, id)`). Для API без произвольных переходов - почти всегда лучший выбор.
Запрос `SELECT * FROM events ORDER BY id LIMIT 20 OFFSET 50000` выполняется медленно. Что происходит внутри?
DISTINCT ON: PostgreSQL-специфичная выборка первой строки в группе
`DISTINCT ON` - PostgreSQL-расширение стандарта SQL. Оно возвращает ровно одну строку для каждого уникального значения указанных столбцов, выбирая «первую» согласно ORDER BY.
**Правило:** столбцы в `DISTINCT ON (...)` должны совпадать с первыми столбцами в `ORDER BY`. PostgreSQL требует этого - иначе ошибка: `SELECT DISTINCT ON expressions must match initial ORDER BY expressions`.
**Производительность:** PostgreSQL часто может использовать индекс на `(user_id, created_at DESC)` для DISTINCT ON без сортировки - план покажет `Index Scan` вместо `Sort`.
Нужно получить последний заказ каждого пользователя (все столбцы строки). Какой подход правильнее использовать в PostgreSQL?
CASE, COALESCE, NULLIF и защита от деления на ноль
`CASE` - единственная условная конструкция в стандартном SQL. Используется и в SELECT (для вычисляемых столбцов), и в WHERE (для сложных условий), и в ORDER BY (для нестандартной сортировки).
`COALESCE(a, b, c)` возвращает первый не-NULL аргумент. Применяется для подстановки значений по умолчанию и объединения столбцов.
**COALESCE ленивый:** вычисляет аргументы слева направо и останавливается при первом non-null. Это важно, если аргументы содержат подзапросы или дорогие выражения.
Запрос содержит `total_revenue / total_orders`. При `total_orders = 0` что произойдёт и как это исправить?
NULL: трёхзначная логика и ловушки
NULL в SQL - не значение, а отсутствие значения. Любое сравнение с NULL возвращает UNKNOWN (третье значение булевой логики, помимо TRUE и FALSE), а WHERE пропускает только строки с результатом TRUE.
**Ловушка NOT IN с NULL в списке.** Если подзапрос или список содержит хотя бы один NULL, `NOT IN` вернёт пустой результат для всех строк - это математически корректно, но почти никогда не то, что нужно.
**Правило большого пальца:** всегда явно обрабатывать NULL через `IS NULL` / `IS NOT NULL` / `COALESCE`. Никогда не полагаться на `!=` или `NOT IN` когда в данных возможны NULL.
WHERE col != 'x' возвращает все строки, кроме тех, где col = 'x'
WHERE col != 'x' возвращает строки где col = 'x' ложно И col не NULL. Строки с col IS NULL молча исключаются.
NULL участвует в сравнении как UNKNOWN. UNKNOWN != TRUE, поэтому WHERE не пропускает такие строки. Это поведение соответствует SQL стандарту, но часто удивляет даже опытных разработчиков.
Таблица `payments` содержит 1000 строк. Запрос `SELECT * FROM payments WHERE refunded_at != '2024-01-01'` вернул 800 строк. Почему не 1000?
SELECT: фильтрация, сортировка, LIMIT
- LIKE с leading wildcard (`%text`) не использует B-tree индекс - нужен GIN + pg_trgm
- OFFSET деградирует на больших страницах: keyset pagination (`WHERE id > last`) работает за O(1)
- DISTINCT ON - PostgreSQL-расширение для выборки первой строки в группе, удобнее ROW_NUMBER для простых случаев
- NULL в NOT IN делает весь результат пустым - использовать NOT EXISTS как безопасную альтернативу
Связанные темы
SELECT - основа для более сложных запросов и оптимизации.
- Индексы в PostgreSQL — Операторы WHERE напрямую влияют на использование индексов
- JOIN и подзапросы — EXISTS/IN часто заменяются JOIN-ами с лучшим планом
- EXPLAIN и планировщик — Проверять фактический план для каждого непрямолинейного запроса
Вопросы для размышления
- В каких ситуациях OFFSET-пагинация остаётся приемлемым выбором, а когда переход на keyset становится обязательным?
- Почему `NOT IN` с подзапросом считается антипаттерном в production-коде и чем отличается его поведение от `NOT EXISTS`?
- Как трёхзначная логика NULL влияет на агрегатные функции - COUNT(*) vs COUNT(col) - и что это означает для аналитических запросов?
Связанные уроки
- pg-05-dml — DML covers INSERT/UPDATE/DELETE; SELECT is the R in CRUD and uses the same table context
- pg-07-joins — JOINs extend SELECT to multiple tables; mastering single-table SELECT is the prerequisite
- pg-08-aggregation — GROUP BY and aggregate functions layer on top of the WHERE/ORDER BY learned in SELECT basics
- pg-12-btree — B-tree indexes accelerate the ORDER BY and range predicates introduced in this lesson
- db-06-sql-advanced