Шпаргалка по SQL для аналитика
created_at имеет тип timestamp. Какой тип данных вернёт DATE_TRUNC('day', created_at)?Содержание:
Зачем нужна шпаргалка по SQL
SQL — рабочий язык аналитика, и на собеседовании вас почти наверняка попросят написать запрос вживую. Проблема в том, что синтаксис разбросан по десяткам гайдов: JOIN в одном месте, оконные функции в другом, обработка NULL в третьем. Эта шпаргалка по SQL собирает всё, что нужно аналитику данных, на одной странице — от простого SELECT до оконных функций и CTE. Открыли, нашли нужный паттерн, скопировали, поправили под свои таблицы.
Шпаргалка рассчитана на тех, кто уже понимает, что такое таблица и строка, но путается в порядке выполнения, забывает синтаксис HAVING или не помнит, чем RANK отличается от ROW_NUMBER. Если вы готовитесь к собесу, держите её рядом и параллельно решайте задачи — теория без практики выветривается за неделю. Тренажёр Карьерник как раз даёт живые SQL-задачи с проверкой ответа, чтобы синтаксис из этой шпаргалки закрепился руками, а не остался в закладках.
Все примеры написаны под PostgreSQL — самый частый диалект в аналитике. В MySQL и других базах синтаксис местами отличается, но логика одна и та же.
Выборка: SELECT, WHERE, ORDER BY, LIMIT
SELECT определяет, какие колонки вернуть, FROM — откуда. Звёздочка тянет все колонки, но в рабочих запросах лучше перечислять нужные явно — так читается понятнее и не ломается при изменении схемы.
SELECT user_id, country, created_at FROM users;
SELECT DISTINCT country FROM users;WHERE фильтрует строки до агрегации. Условия комбинируются через AND, OR и NOT, а для диапазонов и списков есть удобные операторы BETWEEN, IN и LIKE.
SELECT * FROM users
WHERE age > 30
AND country IN ('RU', 'KZ')
AND email LIKE '%@gmail.com'
AND created_at BETWEEN '2026-01-01' AND '2026-04-01'
AND deleted_at IS NULL;ORDER BY сортирует результат: ASC по возрастанию (по умолчанию), DESC по убыванию. Можно сортировать по нескольким колонкам сразу. LIMIT ограничивает число строк, OFFSET пропускает первые N — вместе они дают постраничную выборку.
SELECT * FROM users
ORDER BY country ASC, created_at DESC
LIMIT 20 OFFSET 40;JOIN: типы соединений
JOIN склеивает строки двух таблиц по условию в ON. Тип соединения определяет, что делать со строками, которым не нашлась пара.
INNER JOIN оставляет только те строки, где совпадение нашлось в обеих таблицах. Если у пользователя нет заказов, он в результат не попадёт.
SELECT u.user_id, o.order_id, o.amount
FROM users u
INNER JOIN orders o ON o.user_id = u.id;LEFT JOIN берёт все строки из левой таблицы и подтягивает совпадения из правой; где пары нет — NULL. Это рабочая лошадка аналитика: «все пользователи и их заказы, даже если заказов не было».
SELECT u.user_id, COUNT(o.order_id) AS orders_cnt
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.user_id;RIGHT JOIN — зеркало LEFT, на практике почти не используется: проще поменять таблицы местами. FULL OUTER JOIN возвращает все строки обеих таблиц, заполняя NULL там, где пары нет. CROSS JOIN даёт декартово произведение — каждая строка с каждой; нужен редко и легко получается случайно, если забыть условие в ON.
GROUP BY, HAVING и агрегаты
Агрегатные функции сворачивают группу строк в одно число. COUNT(*) считает все строки, COUNT(col) — только не-NULL значения, COUNT(DISTINCT col) — уникальные. Остальные: SUM, AVG, MIN, MAX.
GROUP BY разбивает таблицу на группы по значениям колонки, и агрегаты считаются внутри каждой группы. HAVING фильтрует уже сами группы — в отличие от WHERE, который работает по строкам до группировки.
SELECT country, COUNT(*) AS users_cnt, AVG(age) AS avg_age
FROM users
WHERE deleted_at IS NULL
GROUP BY country
HAVING COUNT(*) > 100
ORDER BY users_cnt DESC;Частая задача — посчитать долю в процентах. Тут важна ловушка: деление целого на целое в PostgreSQL даёт целое (усечение), поэтому умножайте на 100.0, чтобы результат стал дробным. Условную агрегацию удобно делать через FILTER.
SELECT
country,
COUNT(*) AS total,
COUNT(*) FILTER (WHERE is_paid) AS paid,
ROUND(100.0 * COUNT(*) FILTER (WHERE is_paid) / COUNT(*), 2) AS paid_pct
FROM users
GROUP BY country;Полезно помнить логический порядок выполнения запроса: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. Именно поэтому в WHERE нельзя сослаться на алиас из SELECT — на момент WHERE его ещё не существует.
Оконные функции
Оконные функции считают значение по «окну» строк, но, в отличие от GROUP BY, не схлопывают строки — каждая строка остаётся, рядом добавляется результат. Окно задаётся через OVER (PARTITION BY ... ORDER BY ...).
ROW_NUMBER нумерует строки внутри группы, RANK и DENSE_RANK ранжируют с учётом одинаковых значений: RANK пропускает следующий номер после ничьей (1, 2, 2, 4), DENSE_RANK не пропускает (1, 2, 2, 3).
SELECT
user_id, product, created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn,
DENSE_RANK() OVER (PARTITION BY user_id ORDER BY amount DESC) AS rnk
FROM orders;LAG и LEAD достают значение из предыдущей или следующей строки — удобно для расчёта прироста день к дню.
SELECT
day, revenue,
LAG(revenue) OVER (ORDER BY day) AS prev_revenue,
revenue - LAG(revenue) OVER (ORDER BY day) AS delta
FROM daily_revenue;Накопительный итог и скользящее среднее задаются рамкой окна. По умолчанию ORDER BY в окне суммирует от начала до текущей строки, а явная рамка ROWS BETWEEN N PRECEDING AND CURRENT ROW ограничивает диапазон.
SELECT
day, revenue,
SUM(revenue) OVER (ORDER BY day) AS running_total,
AVG(revenue) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma7
FROM daily_revenue;Важно: оконную функцию нельзя использовать в WHERE или HAVING — на момент их выполнения окна ещё не посчитаны. Если нужно отфильтровать по результату оконки, заверните её в CTE и фильтруйте снаружи (пример ниже, в задаче top-N).
Подзапросы и CTE
Подзапрос — это запрос внутри запроса. В WHERE он часто стоит после IN или EXISTS, в FROM — как временная таблица.
-- подзапрос в WHERE
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);
-- коррелированный подзапрос: ссылается на внешнюю таблицу
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id AND o.amount > 100
);CTE (Common Table Expression) — это именованный подзапрос через WITH. Читается сверху вниз, как шаги, поэтому длинные запросы из вложенных подзапросов почти всегда понятнее переписать на CTE.
WITH active_users AS (
SELECT id FROM users
WHERE last_login > CURRENT_DATE - INTERVAL '30 days'
),
their_orders AS (
SELECT o.*
FROM orders o
JOIN active_users a ON a.id = o.user_id
)
SELECT user_id, SUM(amount) AS total
FROM their_orders
GROUP BY user_id;Классическая задача «топ-N в каждой группе» решается оконной функцией внутри CTE с фильтром снаружи — потому что в WHERE оконку не вызвать.
WITH ranked AS (
SELECT *,
DENSE_RANK() OVER (PARTITION BY category ORDER BY price DESC) AS rnk
FROM products
)
SELECT * FROM ranked WHERE rnk <= 3;Рекурсивный CTE (WITH RECURSIVE) нужен для иерархий и генерации последовательностей — например, чтобы получить непрерывный календарь дат и заполнить пропуски нулями.
WITH RECURSIVE calendar AS (
SELECT DATE '2026-01-01' AS d
UNION ALL
SELECT d + 1 FROM calendar WHERE d < DATE '2026-03-31'
)
SELECT * FROM calendar;Работа с NULL
NULL — это не ноль и не пустая строка, а «неизвестно». Любое сравнение с NULL даёт не TRUE и не FALSE, а UNKNOWN, поэтому проверять надо через IS NULL и IS NOT NULL, а не через = NULL.
SELECT * FROM users WHERE deleted_at IS NULL;
SELECT COALESCE(phone, 'не указан') AS phone FROM users; -- первый не-NULL
SELECT NULLIF(status, 'unknown') AS status FROM users; -- NULL, если значение равно 'unknown'COALESCE возвращает первый не-NULL аргумент — удобно подставлять значение по умолчанию. NULLIF(a, b) возвращает NULL, если a = b. Главное применение NULLIF — безопасное деление: оно превращает делитель-ноль в NULL и спасает от ошибки «деление на ноль».
SELECT revenue / NULLIF(orders_cnt, 0) AS avg_check FROM daily;Две частые ловушки. Первая: NOT IN с подзапросом, где встречается NULL, возвращает пустой результат целиком — потому что сравнение с NULL даёт UNKNOWN. Безопаснее использовать NOT EXISTS. Вторая: COUNT(col) не считает строки с NULL в этой колонке, тогда как COUNT(*) считает все, — это легко принять за расхождение в данных.
-- ловушка: вернёт пусто, если у кого-то user_id = NULL
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM orders);
-- безопасно
SELECT * FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);Даты, строки и CASE
С датами в аналитике работают постоянно. DATE_TRUNC округляет дату до начала периода (день, неделя, месяц) — основа любой группировки по времени. EXTRACT достаёт часть даты числом. Арифметика идёт через INTERVAL.
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS signups
FROM users
WHERE created_at >= CURRENT_DATE - INTERVAL '6 months'
GROUP BY 1
ORDER BY 1;Строковые функции пригодятся для очистки и парсинга. Конкатенация в PostgreSQL — через ||, обрезка пробелов — TRIM, поиск подстроки — POSITION, а вытащить часть по разделителю проще всего через SPLIT_PART.
SELECT
TRIM(LOWER(name)) AS name_clean,
SPLIT_PART(email, '@', 2) AS domain,
LEFT(phone, 4) || '****' AS phone_masked
FROM users;CASE WHEN — это ветвление прямо в SELECT: проверяет условия сверху вниз и возвращает значение первого сработавшего. Незаменим для сегментации и условной агрегации.
SELECT
user_id,
CASE
WHEN age < 18 THEN 'до 18'
WHEN age < 35 THEN '18-34'
WHEN age < 55 THEN '35-54'
ELSE '55+'
END AS age_group
FROM users;Частые ошибки
Самая частая ошибка новичков — SELECT * в продакшен-запросах и дашбордах. Тянуть все колонки медленнее, ломает читаемость и приводит к сюрпризам, когда в таблицу добавляют поле. Перечисляйте колонки явно.
Вторая — забытое условие в JOIN. Без ON (или с неверным ключом) база делает декартово произведение, и число строк взрывается. Если после JOIN строк стало подозрительно много, первым делом проверяйте условие соединения и уникальность ключа справа.
Третья — путаница WHERE и HAVING. WHERE фильтрует строки до группировки, HAVING — группы после. Условие на агрегат (COUNT(*) > 100) должно идти в HAVING, а на обычную колонку — в WHERE, иначе либо синтаксическая ошибка, либо лишняя нагрузка.
Четвёртая — деление целых чисел. 5 / 20 в PostgreSQL вернёт 0, а не 0.25. Приводите к дробному типу: 5::NUMERIC / 20 или умножайте на 100.0. И не забывайте про NULLIF(делитель, 0), чтобы не упасть на нуле.
Пятая — сравнение с NULL через = и NOT IN с подзапросом, в котором есть NULL. Проверяйте через IS NULL, а для исключения используйте NOT EXISTS.
Связанные темы
- 50 вопросов по SQL на собеседовании
- JOIN в SQL: шпаргалка
- Оконные функции SQL: шпаргалка
- GROUP BY: шпаргалка
- CTE в SQL: шпаргалка
- NULL в SQL: шпаргалка
FAQ
С чего начать учить SQL аналитику?
С SELECT, WHERE и GROUP BY — этого хватает для 80% базовых задач. Дальше подключайте JOIN, потом подзапросы и CTE, и в конце оконные функции. Параллельно решайте задачи: SQL запоминается только через практику, а не чтением.
Чем GROUP BY отличается от оконных функций?
GROUP BY схлопывает группу строк в одну агрегированную, теряя детализацию. Оконная функция считает агрегат по окну, но оставляет все исходные строки и добавляет результат рядом. Если нужно сохранить строки и при этом видеть, например, накопительный итог, — это окно.
В чём разница между WHERE и HAVING?
WHERE фильтрует отдельные строки до группировки и не умеет работать с агрегатами. HAVING фильтрует уже готовые группы и применяется к агрегатам вроде COUNT(*) или SUM(amount). По логике выполнения WHERE идёт раньше GROUP BY, а HAVING — после.
Почему запрос возвращает пустой результат?
Частая причина — NOT IN с подзапросом, где есть NULL: тогда всё условие становится UNKNOWN и не проходит ни одна строка. Вторая — слишком жёсткие условия в WHERE или неверный JOIN, отрезающий все совпадения. Замените NOT IN на NOT EXISTS и проверяйте фильтры по очереди.
Какой диалект SQL учить аналитику?
PostgreSQL — самый частый в аналитике и хорошая база: синтаксис близок к стандарту. MySQL и SQL Server отличаются в мелочах (функции дат, оконки, лимиты), но 90% знаний переносятся напрямую. Начните с одного диалекта и не распыляйтесь.