Классические SQL задачи собесов
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;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, воронки).