Практические примеры SQL

d{ "title": "Практические примеры SQL: отбор данных, оптимизация и типовые ошибки", "keywords": "SQL примеры, SELECT запросы, JOIN, GROUP BY, оптимизация SQL, ошибки SQL, подзапросы, CTE, оконные функции", "description": "Подборка практических примеров SQL с реальными сценариями: от простой фильтрации до оконных функций. Разбор типовых ошибок, способы ускорения запросов, конкретные цифры производительности.", "html_content": "

1. Базовый отбор данных: фильтры и сортировка для повседневных задач

Самый частый сценарий — выборка заказов за определённый период. Вместо WHERE date > '2025-01-01' используйте диапазон BETWEEN, чтобы исключить граничные ошибки. Например: WHERE order_date BETWEEN '2026-01-01' AND '2026-01-31' — такой вариант включает обе даты и работает быстрее на индексах.

Фильтр по статусу и сумме чека: WHERE status = 'completed' AND total_amount > 5000. Важно: проверяйте типы данных. Если столбец total_amount хранит копейки как целое число (500000 = 5000 руб), сравнение должно быть > 500000. Иначе запрос вернёт пустой результат.

Сортировка по нескольким полям: ORDER BY status ASC, total_amount DESC. Статусы идут по алфавиту, а внутри каждого статуса — от дорогих заказов к дешёвым. Это удобно для отчётов: сначала «отменённые» (C), потом «выполненные» (E). Если нужно обратное — используйте CASE для кастомного порядка.

2. JOIN: как соединять таблицы без потери строк и дубликатов

Частая задача: показать заказы вместе с именем клиента. INNER JOIN clients ON orders.client_id = clients.id. Если у заказа нет клиента (например, удалённая запись), такой заказ пропадёт из выборки. Для отчётов с суммой лучше использовать LEFT JOIN.

Пример: получить все товары и их последнюю поставку. LEFT JOIN (SELECT product_id, MAX(supply_date) AS last_date FROM supplies GROUP BY product_id) AS last_supply ON products.id = last_supply.product_id. Это даст NULL для товаров без поставок — явно видно, что нужно заказать.

Когда нужно соединить 4–5 таблиц, запрос становится громоздким. Разбивайте на WITH (CTE) — это повышает читаемость и часто ускоряет выполнение за счёт материализации промежуточных данных. Например: выделите отдельно список активных клиентов, затем соедините его с заказами.

3. Агрегация и группировка: точные подсчёты с фильтрацией групп

Посчитать сумму продаж по каждому менеджеру: SELECT manager_id, SUM(amount) AS total FROM orders GROUP BY manager_id. Если нужно оставить только тех, у кого сумма больше 100 000 — используйте HAVING SUM(amount) > 100000. HAVING работает после группировки, WHERE — до.

Пример: найти топ-5 категорий товаров по количеству продаж. SELECT category_id, COUNT(*) AS cnt FROM order_items GROUP BY category_id ORDER BY cnt DESC LIMIT 5. Запрос выполняется за 50 мс на таблице в 500 тыс. строк при наличии индекса по category_id.

Частая ошибка: включать в SELECT поля, не участвующие в GROUP BY и не агрегированные. В MySQL это может сработать, но выдаст случайное значение. В PostgreSQL — ошибка. Всегда либо добавляйте поле в GROUP BY, либо применяйте агрегатную функцию.

4. Подзапросы и CTE: когда вложенный запрос работает быстрее

Подзапрос в WHERE: найти товары дороже среднего по категории. WHERE price > (SELECT AVG(price) FROM products WHERE category_id = p.category_id). Такой коррелированный подзапрос может выполняться для каждой строки — на 10 000 строк это 0.5–1 секунда. Альтернатива — оконная функция.

CTE (Common Table Expression) — временная таблица для одного запроса. Пример: WITH top_clients AS (SELECT id FROM clients WHERE total_spent > 50000). Затем используем её в основном запросе. CTE читается проще подзапроса и может материализоваться, если используется многократно.

Когда CTE невыгоден: если он вызывается один раз и фильтр уникален — подзапрос часто быстрее из-за отсутствия материализации. В PostgreSQL 16+ оптимизатор может сам решить, материализовать CTE или нет.

5. Оконные функции: ранжирование и скользящие суммы без группировки

Оконные функции позволяют делать расчёты по группам, не сжимая строки. Пример: для каждого заказа показать его долю от суммы по клиенту. SELECT total, SUM(total) OVER (PARTITION BY client_id) AS client_total. Так вы видите и каждую покупку, и общий итог по клиенту в той же строке.

Ранжирование товаров по цене в категории: RANK() OVER (PARTITION BY category_id ORDER BY price DESC) AS rnk. RANK оставляет пропуски при совпадении цен, DENSE_RANK — без пропусков. Выбирайте по смыслу: ранги с пропусками нужны для строгой нумерации, без пропусков — для распределения по группам.

Скользящее среднее за 7 дней: AVG(total) OVER (ORDER BY order_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW). Это даёт сглаженный тренд продаж без потери дат. Для 1000 записей такой запрос выполняется за 0.1 сек.

6. Типовые ошибки и способы их избежать

Ошибка №1: неиспользование индексов. Если в запросе есть WHERE city = 'Москва' без индекса — база будет сканировать всю таблицу. На 500 тыс. строк время запроса — 2–5 секунд. С индексом — 0.01 секунды. Проверяйте планы выполнения через EXPLAIN.

Ошибка №2: SELECT * в продакшн-коде. Это тащит все столбцы, даже если нужно только 2. Лишняя нагрузка на диск и сеть. Всегда перечисляйте конкретные поля.

Ошибка №3: неправильная обработка NULL. Любое сравнение с NULL даёт NULL, а не TRUE/FALSE. В WHERE это работает как FALSE. Если нужно проверить на NULL — только IS NULL или IS NOT NULL.

7. Практические рекомендации: как ускорить запросы в 10 раз

Первое правило: создавайте индексы под самые частые фильтры и соединения. Один составной индекс (client_id, status) покрывает запросы с фильтром по клиенту и статусу, и соединение по клиенту. Экономия — от 50 мс до 2 секунд на миллион строк.

Второе правило: используйте EXPLAIN ANALYZE для каждого медленного запроса. Ищите последовательное сканирование таблиц (Seq Scan) — это сигнал к добавлению индекса. Если видите сортировку на 100 тыс. строк — возможно, нужен индекс с сортировкой.

Третье правило: для частых отчётов создавайте материализованные представления. Они обновляются раз в час/день, но запрос по ним выполняется за 0.1 сек вместо 5 секунд на сырых данных.