Классические SQL задачи собесов

Проверь себя · 1/3разбор после ответа
На ревью вы видите выражение CAST(user_id AS text) в коде Postgres. Какой вариант с оператором :: записывает то же самое приведение типа?

Зачем это знать

Некоторые SQL-задачи появляются на каждом собесе. Знать шаблон решения — экономит 5 минут в стрессе: не тратишь время на изобретение велосипеда, а сразу применяешь известную конструкцию. Ниже — 25 типовых задач с готовыми решениями.

1. Вторая по размеру зарплата

-- Способ 1: DENSE_RANK (корректно работает с одинаковыми значениями)
SELECT salary FROM (
    SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
    FROM employees
) t WHERE rnk = 2;

-- Способ 2: LIMIT OFFSET
SELECT DISTINCT salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 1;

-- Способ 3: подзапрос
SELECT MAX(salary) FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);

DENSE_RANK предпочтительнее, если в данных могут быть повторяющиеся значения — LIMIT OFFSET на дубликатах даст некорректный результат.

2. N-ая по размеру зарплата

SELECT salary FROM (
    SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
    FROM employees
) t WHERE rnk = N;

3. Топ-3 зарплат в каждом отделе

WITH ranked AS (
    SELECT department, name, salary,
           DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk
    FROM employees
)
SELECT department, name, salary
FROM ranked
WHERE rnk <= 3;

4. Дубликаты email

SELECT email FROM users
GROUP BY email HAVING COUNT(*) > 1;

5. Пользователи без заказов

-- LEFT JOIN
SELECT u.*
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

-- NOT EXISTS
SELECT * FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders WHERE user_id = u.id);

6. Нарастающий итог

SELECT day, revenue,
       SUM(revenue) OVER (ORDER BY day) AS cumulative
FROM daily;

7. Изменение месяц к месяцу (MoM)

SELECT month, revenue,
       LAG(revenue) OVER (ORDER BY month) AS prev,
       revenue - LAG(revenue) OVER (ORDER BY month) AS diff,
       (revenue - LAG(revenue) OVER (ORDER BY month)) * 100.0 /
           NULLIF(LAG(revenue) OVER (ORDER BY month), 0) AS pct_change
FROM monthly;

8. Первый заказ каждого пользователя

-- Вариант 1: ROW_NUMBER
SELECT * FROM (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS rn
    FROM orders
) t WHERE rn = 1;

-- Вариант 2: MIN
SELECT o.* FROM orders o
WHERE o.created_at = (
    SELECT MIN(created_at) FROM orders WHERE user_id = o.user_id
);

9. Непрерывные дни (стрики)

WITH numbered AS (
    SELECT user_id, day,
           day - (ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY day))::int AS grp
    FROM logins
)
SELECT user_id, MIN(day), MAX(day), COUNT(*) AS streak
FROM numbered
GROUP BY user_id, grp;

Паттерн gaps and islands — группируем подряд идущие даты в «острова», считая разность позиции и даты.

10. Медиана

-- Postgres / MSSQL
SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) AS median
FROM employees;

-- Универсальный способ (ROW_NUMBER)
WITH ordered AS (
    SELECT salary,
           ROW_NUMBER() OVER (ORDER BY salary) AS rn,
           COUNT(*) OVER () AS n
    FROM employees
)
SELECT AVG(salary) AS median
FROM ordered
WHERE rn IN ((n+1)/2, (n+2)/2);

11. Пропуски (недостающие даты)

-- Сгенерировать ожидаемый календарь
WITH expected AS (
    SELECT generate_series('2026-01-01'::DATE, '2026-04-23'::DATE, '1 day') AS d
)
SELECT e.d FROM expected e
LEFT JOIN orders o ON DATE(o.created_at) = e.d
WHERE o.id IS NULL;

12. Скользящая сумма за 7 дней

SELECT day, revenue,
       SUM(revenue) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS rolling_7
FROM daily;

13. Расчёт retention

WITH cohorts AS (
    SELECT user_id, DATE_TRUNC('month', MIN(created_at)) AS cohort
    FROM users GROUP BY user_id
),
activity AS (
    SELECT DISTINCT user_id, DATE_TRUNC('month', created_at) AS active_m
    FROM events
)
SELECT cohort,
       (EXTRACT(YEAR  FROM AGE(active_m, cohort)) * 12
        + EXTRACT(MONTH FROM AGE(active_m, cohort)))::int AS months_since,
       COUNT(DISTINCT user_id) AS active
FROM cohorts JOIN activity USING(user_id)
GROUP BY 1, 2;

14. Конверсия воронки

WITH user_steps AS (
    SELECT user_id,
           MAX(CASE WHEN event = 'signup' THEN 1 ELSE 0 END) AS signed,
           MAX(CASE WHEN event = 'active' THEN 1 ELSE 0 END) AS active,
           MAX(CASE WHEN event = 'paid' THEN 1 ELSE 0 END) AS paid
    FROM events GROUP BY user_id
)
SELECT
    SUM(signed) AS signups,
    SUM(active) AS activations,
    SUM(paid) AS purchases,
    SUM(active) * 100.0 / SUM(signed) AS s_to_a_pct
FROM user_steps;
Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

15. Pivot (вручную)

-- Широкий формат
SELECT
    month,
    SUM(CASE WHEN product = 'A' THEN revenue END) AS A,
    SUM(CASE WHEN product = 'B' THEN revenue END) AS B,
    SUM(CASE WHEN product = 'C' THEN revenue END) AS C
FROM sales GROUP BY month;

16. Self-join (иерархия)

SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

17. Все пары товаров в одном заказе

SELECT p1.product, p2.product
FROM sales p1
JOIN sales p2 ON p1.order_id = p2.order_id
WHERE p1.product < p2.product;

18. Пользователи активные сегодня и вчера

SELECT DISTINCT user_id FROM events
WHERE DATE(created_at) = CURRENT_DATE - 1
INTERSECT
SELECT DISTINCT user_id FROM events
WHERE DATE(created_at) = CURRENT_DATE;

19. Разница между двумя таблицами

-- В A, но не в B
SELECT * FROM a
EXCEPT
SELECT * FROM b;

20. Случайная выборка

-- Postgres
SELECT * FROM users TABLESAMPLE BERNOULLI(10);

-- Портируемый вариант
SELECT * FROM users ORDER BY RANDOM() LIMIT 1000;

21. Пагинация

SELECT * FROM users
ORDER BY id
LIMIT 20 OFFSET 40;  -- страница 3, по 20 записей

-- Быстрее: курсор-пагинация
SELECT * FROM users WHERE id > last_seen_id ORDER BY id LIMIT 20;

22. Уникальные комбинации

SELECT DISTINCT col1, col2 FROM TABLE;

23. Удалить дубликаты, оставить один

WITH ranked AS (
    SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
    FROM users
)
DELETE FROM users WHERE id IN (SELECT id FROM ranked WHERE rn > 1);

24. Накопительный COUNT DISTINCT

Сложнее — COUNT(DISTINCT) OVER в Postgres не работает, поэтому идём через подзапрос.

WITH first_occurrence AS (
    SELECT user_id, MIN(day) AS first_day
    FROM events GROUP BY user_id
)
SELECT day,
       (SELECT COUNT(*) FROM first_occurrence WHERE first_day <= day) AS users_ever
FROM calendar;

25. Год к году (YoY)

SELECT
    EXTRACT(MONTH FROM DATE) AS month,
    EXTRACT(YEAR FROM DATE) AS year,
    SUM(revenue) AS rev,
    LAG(SUM(revenue), 12) OVER (ORDER BY DATE) AS prev_year
FROM daily
GROUP BY 1, 2;

Сводка паттернов

  • Топ-N в группе → оконная функция + rank
  • Нарастающий итог → SUM OVER
  • MoM / YoY → LAG
  • Пропуски дат → generate_series + LEFT JOIN
  • Стрики → gaps and islands
  • Воронки → условная агрегация
  • Retention → когорта + смещение + агрегация
  • Иерархии → рекурсивный CTE

На собесе

Используйте готовые паттерны, вместо того чтобы писать с нуля. На собесе важно не просто написать SQL, а объяснить, почему выбран именно этот приём (например, DENSE_RANK вместо LIMIT OFFSET из-за дубликатов).

Связанные темы

FAQ

Нужно ли запоминать все 25 задач?

Паттерны — да, синтаксис — нет. Шаблон «Top-N per group через оконку» должен узнаваться за секунду, а конкретный SQL достаточно восстановить из шпаргалки.

Как тренировать скорость?

Практикой. Решённая много раз задача превращается в рефлекс — на собесе пишешь не думая о синтаксисе.

Подходит ли LeetCode Database для тренировки?

Да, хорошо заходят задачи уровня medium и hard. Но LeetCode уклонён в абстракции — реальные задачи продуктового аналитика ближе к задачам из этой шпаргалки (когорты, retention, воронки).