SQL примеры запросов: 30 готовых шаблонов для аналитика
Зачем аналитику шаблоны SQL-запросов
На работе 80% SQL-запросов — вариации одних и тех же паттернов. Ниже — 30 готовых шаблонов, которые покрывают типичные задачи аналитика данных. Каждый запрос можно адаптировать под свои таблицы за пару минут.
Все запросы написаны для PostgreSQL — самой популярной СУБД на собеседованиях и в продуктовых компаниях.
Базовые запросы
1. Количество уникальных пользователей за период
SELECT COUNT(DISTINCT user_id) AS unique_users
FROM events
WHERE event_date BETWEEN '2026-03-01' AND '2026-03-31';2. Топ-10 по метрике
SELECT user_id, SUM(amount) AS total_spent
FROM orders
GROUP BY user_id
ORDER BY total_spent DESC
LIMIT 10;3. Распределение по категориям
SELECT category, COUNT(*) AS cnt,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct
FROM products
GROUP BY category
ORDER BY cnt DESC;4. Фильтрация с несколькими условиями
SELECT * FROM orders
WHERE status = 'completed'
AND amount > 1000
AND created_at >= CURRENT_DATE - INTERVAL '30 days';5. Дедупликация
SELECT DISTINCT ON (user_id) *
FROM user_sessions
ORDER BY user_id, session_start DESC;DISTINCT ON — PostgreSQL-расширение. Оставляет одну строку на user_id — последнюю сессию.
JOIN-паттерны
6. Подтянуть данные из справочника
SELECT o.order_id, o.amount, u.name, u.city
FROM orders o
JOIN users u ON o.user_id = u.user_id;7. Пользователи без заказов
SELECT u.user_id, u.name
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;Подробнее: JOIN в SQL — шпаргалка.
8. Выручка по категориям (три таблицы)
SELECT p.category, SUM(oi.quantity * oi.price) AS revenue
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.created_at >= '2026-01-01'
GROUP BY p.category
ORDER BY revenue DESC;Агрегации и группировки
9. Средний чек по месяцам
SELECT DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS orders,
ROUND(AVG(amount), 2) AS avg_check
FROM orders
GROUP BY 1 ORDER BY 1;10. Пользователи с 5+ заказами
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 5;11. Конверсия по этапам воронки
SELECT
COUNT(DISTINCT user_id) FILTER (WHERE event = 'view') AS views,
COUNT(DISTINCT user_id) FILTER (WHERE event = 'cart') AS carts,
COUNT(DISTINCT user_id) FILTER (WHERE event = 'purchase') AS purchases,
ROUND(100.0 *
COUNT(DISTINCT user_id) FILTER (WHERE event = 'purchase') /
NULLIF(COUNT(DISTINCT user_id) FILTER (WHERE event = 'view'), 0)
, 1) AS conversion_pct
FROM events;Подробнее: воронка конверсии.
12. Сегментация CASE WHEN
SELECT
CASE
WHEN total >= 100000 THEN 'VIP'
WHEN total >= 10000 THEN 'Regular'
ELSE 'New'
END AS segment,
COUNT(*) AS users
FROM (SELECT user_id, SUM(amount) AS total FROM orders GROUP BY 1) t
GROUP BY 1;Оконные функции
13. Нумерация строк внутри группы
SELECT user_id, order_id, created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS order_num
FROM orders;14. Нарастающий итог
SELECT created_at::DATE AS day,
SUM(amount) AS daily,
SUM(SUM(amount)) OVER (ORDER BY created_at::DATE) AS cumulative
FROM orders GROUP BY 1;15. Предыдущее значение (LAG)
SELECT month, revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_month,
revenue - LAG(revenue) OVER (ORDER BY month) AS diff
FROM monthly_revenue;16. Скользящее среднее 7 дней
SELECT day, value,
AVG(value) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma_7
FROM daily_metrics;17. Топ-N в каждой группе
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rn
FROM products
)
SELECT * FROM ranked WHERE rn <= 3;18. Доля от итога группы
SELECT department, employee, salary,
ROUND(100.0 * salary / SUM(salary) OVER (PARTITION BY department), 1) AS pct
FROM employees;Подробнее: оконные функции SQL — шпаргалка.
Работа с датами
19. DAU за последние 30 дней
SELECT activity_date, COUNT(DISTINCT user_id) AS dau
FROM user_activity
WHERE activity_date >= CURRENT_DATE - 30
GROUP BY 1 ORDER BY 1;20. Группировка по неделям
SELECT DATE_TRUNC('week', created_at) AS week,
COUNT(*) AS orders
FROM orders
GROUP BY 1 ORDER BY 1;21. Разница в днях между событиями
SELECT user_id,
MAX(activity_date) - MIN(activity_date) AS active_days_span,
COUNT(DISTINCT activity_date) AS active_days
FROM user_activity
GROUP BY 1;22. Генерация ряда дат (заполнение пропусков)
SELECT d::DATE AS day,
COALESCE(o.cnt, 0) AS orders
FROM generate_series('2026-01-01', '2026-03-31', '1 day'::INTERVAL) d
LEFT JOIN (
SELECT created_at::DATE AS day, COUNT(*) AS cnt FROM orders GROUP BY 1
) o ON d::DATE = o.day;Продуктовая аналитика
23. Retention D1
WITH first_visit AS (
SELECT user_id, MIN(activity_date) AS cohort_date
FROM user_activity GROUP BY 1
)
SELECT cohort_date,
COUNT(DISTINCT f.user_id) AS cohort_size,
COUNT(DISTINCT a.user_id) AS returned_d1,
ROUND(100.0 * COUNT(DISTINCT a.user_id) / COUNT(DISTINCT f.user_id), 1) AS d1_pct
FROM first_visit f
LEFT JOIN user_activity a ON f.user_id = a.user_id
AND a.activity_date = f.cohort_date + 1
GROUP BY 1 ORDER BY 1;Подробнее: как считать retention.
24. Когортный анализ по месяцам
WITH cohorts AS (
SELECT user_id, DATE_TRUNC('month', MIN(created_at)) AS cohort
FROM orders GROUP BY 1
)
SELECT c.cohort,
DATE_TRUNC('month', o.created_at) AS activity_month,
COUNT(DISTINCT o.user_id) AS users
FROM cohorts c
JOIN orders o ON c.user_id = o.user_id
GROUP BY 1, 2 ORDER BY 1, 2;Подробнее: когортный анализ.
25. LTV по когортам
WITH cohorts AS (
SELECT user_id, DATE_TRUNC('month', MIN(created_at)) AS cohort
FROM orders GROUP BY 1
)
SELECT c.cohort,
COUNT(DISTINCT c.user_id) AS cohort_size,
ROUND(SUM(o.amount) / COUNT(DISTINCT c.user_id), 0) AS ltv_per_user
FROM cohorts c
JOIN orders o ON c.user_id = o.user_id
GROUP BY 1 ORDER BY 1;26. RFM-сегментация
WITH rfm AS (
SELECT user_id,
CURRENT_DATE - MAX(created_at::DATE) AS recency,
COUNT(*) AS frequency,
SUM(amount) AS monetary
FROM orders GROUP BY 1
)
SELECT user_id, recency, frequency, monetary,
NTILE(5) OVER (ORDER BY recency ASC) AS r,
NTILE(5) OVER (ORDER BY frequency DESC) AS f,
NTILE(5) OVER (ORDER BY monetary DESC) AS m
FROM rfm;Продвинутые паттерны
27. Pivoting без расширений
SELECT user_id,
SUM(CASE WHEN category = 'SQL' THEN score END) AS sql_score,
SUM(CASE WHEN category = 'Python' THEN score END) AS python_score,
SUM(CASE WHEN category = 'Stats' THEN score END) AS stats_score
FROM quiz_results
GROUP BY 1;28. Sessionization (группировка событий в сессии)
WITH gaps AS (
SELECT *, event_time - LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time) AS gap
FROM events
),
sessions AS (
SELECT *, SUM(CASE WHEN gap > INTERVAL '30 minutes' OR gap IS NULL THEN 1 ELSE 0 END)
OVER (PARTITION BY user_id ORDER BY event_time) AS session_id
FROM gaps
)
SELECT user_id, session_id,
MIN(event_time) AS session_start,
MAX(event_time) AS session_end,
COUNT(*) AS events
FROM sessions
GROUP BY 1, 2;29. Поиск аномалий (z-score)
WITH stats AS (
SELECT AVG(daily_revenue) AS avg_rev, STDDEV(daily_revenue) AS std_rev
FROM daily_metrics
)
SELECT day, daily_revenue,
ROUND((daily_revenue - avg_rev) / NULLIF(std_rev, 0), 2) AS z_score
FROM daily_metrics, stats
WHERE ABS(daily_revenue - avg_rev) > 2 * std_rev;Подробнее: выбросы в данных.
30. Recursive CTE — иерархия категорий
WITH RECURSIVE tree AS (
SELECT id, name, parent_id, 1 AS depth
FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, t.depth + 1
FROM categories c JOIN tree t ON c.parent_id = t.id
)
SELECT * FROM tree ORDER BY depth, name;Как использовать эти шаблоны
- Сохраните — добавьте в свою библиотеку сниппетов
- Адаптируйте — замените имена таблиц и столбцов на свои
- Комбинируйте — реальные задачи обычно требуют 2–3 паттерна в одном запросе
- Тренируйтесь — решайте SQL-задачи каждый день
Читайте также
- SQL-тренажёр: 200+ задач
- 50 SQL-задач для собеседования
- Оконные функции SQL: шпаргалка
- JOIN в SQL: шпаргалка
- SQL на собеседовании аналитика
FAQ
Какие SQL-запросы аналитик пишет чаще всего?
GROUP BY с агрегациями (80% рабочих задач), LEFT JOIN для подтягивания справочников, оконные функции для рангов и нарастающих итогов, DATE_TRUNC для группировки по периодам. Эти четыре паттерна покрывают большинство ежедневных задач.
Чем отличается рабочий SQL от учебного?
Рабочие запросы длиннее (30–100 строк), используют CTE для читаемости, обрабатывают NULL и edge cases, и почти всегда включают JOIN нескольких таблиц. На собеседованиях задачи короче, но покрывают те же паттерны.
Нужно ли знать все 30 паттернов для собеседования?
Первые 18 (базовые + JOIN + агрегации + оконные функции) — обязательно. Паттерны 19–26 (даты + продуктовая аналитика) — для middle. Паттерны 27–30 (продвинутые) — для senior.
Какую СУБД учить?
PostgreSQL. Его используют в большинстве продуктовых компаний, и его синтаксис принимают на собеседованиях. Если в компании ClickHouse или BigQuery — основы те же, отличия минимальны.
Потренируйтесь писать SQL-запросы — откройте тренажёр с 200+ задачами и разборами.