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;

Как использовать эти шаблоны

  1. Сохраните — добавьте в свою библиотеку сниппетов
  2. Адаптируйте — замените имена таблиц и столбцов на свои
  3. Комбинируйте — реальные задачи обычно требуют 2–3 паттерна в одном запросе
  4. Тренируйтесь — решайте 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+ задачами и разборами.