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

Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.

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

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

1. Second highest salary

-- Способ 1: DENSE_RANK (handles ties)
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: Subquery
SELECT MAX(salary) FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);

2. Nth highest

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

3. Top 3 salaries per department

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. Duplicate emails

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

5. Users with no orders

-- 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. Running total

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

7. Month-over-month change

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. First order of each user

-- Approach 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;

-- Approach 2: MIN
SELECT o.* FROM orders o
WHERE o.created_at = (
    SELECT MIN(created_at) FROM orders WHERE user_id = o.user_id
);

9. Consecutive days (streaks)

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;

Gap and islands pattern.

10. Median

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

-- Universal (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. Gaps (missing dates)

-- Generate expected
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. Rolling 7-day sum

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

13. Retention calculation

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(MONTH FROM AGE(active_m, cohort)) AS months_since,
       COUNT(DISTINCT user_id) AS active
FROM cohorts JOIN activity USING(user_id)
GROUP BY 1, 2;

14. Funnel conversion

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 (manual)

-- Wide format
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 (hierarchy)

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

17. All pairs (products together)

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

18. Users active today and yesterday

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. Difference между two tables

-- In A, not in B
SELECT * FROM a
EXCEPT
SELECT * FROM b;

20. Random sample

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

-- Portable
SELECT * FROM users ORDER BY RANDOM() LIMIT 1000;

21. Pagination

SELECT * FROM users
ORDER BY id
LIMIT 20 OFFSET 40;  -- Page 3, 20 per page

-- Faster: cursor-based
SELECT * FROM users WHERE id > last_seen_id ORDER BY id LIMIT 20;

22. Unique combinations

SELECT DISTINCT col1, col2 FROM TABLE;

23. Delete duplicates keep one

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. Running count distinct

Harder.

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. Year-over-year

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;

Patterns summary

  • Top-N per group → window function + rank
  • Running totals → SUM OVER
  • MoM / YoY → LAG
  • Gaps → generate_series + LEFT JOIN
  • Streaks → gaps and islands
  • Funnels → conditional aggregation
  • Retention → cohort + offset + aggregate
  • Hierarchies → recursive CTE

На собесе

Use these patterns. Explain which fits.

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

FAQ

Запомнить all 25?

Patterns — да. Syntax — reference. Build over time.

Speed от practice?

Yes. Много раз solved — reflex.

LeetCode Database?

Good practice. Medium-hard.


Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.