Вопросы по SQL на собеседовании — часть 3/3: Senior с разбором ответов
Это часть 3 из 3. ← Часть 1: Junior | ← Часть 2: Middle
Вопросы по SQL на собеседовании — часть 3/3: Senior с разбором ответов
TL;DR: 7 вопросов для сеньоров, где проверяют не знание синтаксиса, а архитектурное мышление — оконные функции, рекурсивные CTE, планы выполнения, блокировки, шардирование. По материалам собеседований 2024–2026.
Как пользоваться: Это финальная часть серии. Если Junior-вопросы вызывают затруднения — начни с части 1. Здесь каждый вопрос предполагает, что ты свободно ориентируешься в JOIN, индексах и транзакциях.
На Senior-уровне интервьюер не спрашивает «что такое JOIN». Вместо этого: «у нас запрос отрабатывает 40 секунд, вот EXPLAIN — найди проблему». Или: «как спроектировать схему для 100 миллионов строк с нагрузкой 10 000 запросов в секунду».
Эти вопросы — по данным собеседований в Яндексе, VK, Т-Банке и зарубежных компаниях (Glassdoor, Reddit) за 2024–2026 годы.
Содержание
- 17. Оконные функции: ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD
- 18. CTE и рекурсивные CTE
- 19. Как читать EXPLAIN и оптимизировать запросы?
- 20. Кластерный vs некластерный индекс
- 21. Что такое deadlock и как его избежать?
- 22. Шардирование и репликация
- 23. Как удалить дубликаты, оставив одну запись?
17. Оконные функции: ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD
Интервьюер проверяет: можешь ли ты решать аналитические задачи без подзапросов и self-join.
Оконные функции выполняют вычисления «через» набор строк, связанных с текущей строкой — но НЕ схлопывают результат (в отличие от GROUP BY).
Ранжирование:
SELECT
name,
department_id,
salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS dense_rnk
FROM employees;
Разница при одинаковых значениях salary = 90000:
- ROW_NUMBER: 1, 2, 3 (всегда уникальные)
- RANK: 1, 1, 3 (пропускает позиции)
- DENSE_RANK: 1, 1, 2 (не пропускает)
Смещение — LAG и LEAD:
-- Сравнить выручку с предыдущим месяцем
SELECT
month,
revenue,
LAG(revenue, 1) OVER (ORDER BY month) AS prev_month,
revenue - LAG(revenue, 1) OVER (ORDER BY month) AS growth
FROM monthly_stats;
Агрегаты в окне — нарастающий итог:
SELECT
date,
amount,
SUM(amount) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM payments;
Типичная ошибка: пытаются использовать оконную функцию в WHERE:
-- Не работает: оконные функции выполняются ПОСЛЕ WHERE
SELECT * FROM employees
WHERE ROW_NUMBER() OVER (ORDER BY salary DESC) <= 5; -- ошибка!
-- Правильно: через подзапрос или CTE
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn
FROM employees
)
SELECT * FROM ranked WHERE rn <= 5;
Follow-up: В чём разница между ROWS и RANGE в оконном фрейме? ROWS считает физические строки. RANGE группирует строки с одинаковым значением ORDER BY. При наличии дубликатов результат будет разным.
18. CTE и рекурсивные CTE
Интервьюер проверяет: умеешь ли ты работать с иерархическими данными и декомпозировать сложные запросы.
Обычный CTE — именованный временный результат для читаемости:
WITH department_stats AS (
SELECT department_id, AVG(salary) AS avg_salary, COUNT(*) AS cnt
FROM employees
GROUP BY department_id
)
SELECT d.name, ds.avg_salary, ds.cnt
FROM departments d
JOIN department_stats ds ON d.id = ds.department_id
WHERE ds.cnt > 5;
Рекурсивный CTE — для обхода иерархий (дерево сотрудников, категорий, комментариев):
-- Вся цепочка подчинения от CEO до каждого сотрудника
WITH RECURSIVE org_tree AS (
-- Базовый случай: CEO (нет руководителя)
SELECT id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Рекурсивный шаг: подчинённые
SELECT e.id, e.name, e.manager_id, t.level + 1
FROM employees e
JOIN org_tree t ON e.manager_id = t.id
)
SELECT * FROM org_tree ORDER BY level, name;
Типичная ошибка: забывают условие остановки рекурсии — получают бесконечный цикл. Всегда добавляй LIMIT или проверку глубины при разработке:
-- Защита от бесконечной рекурсии
WITH RECURSIVE org_tree AS (
SELECT id, name, manager_id, 1 AS level
FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, t.level + 1
FROM employees e
JOIN org_tree t ON e.manager_id = t.id
WHERE t.level < 20 -- ограничение глубины
)
SELECT * FROM org_tree;
Follow-up: CTE vs подзапрос — что быстрее? Зависит от СУБД. PostgreSQL 12+ оптимизирует CTE как подзапрос (если CTE используется один раз и не рекурсивный). Можно управлять через MATERIALIZED / NOT MATERIALIZED.
19. Как читать EXPLAIN и оптимизировать запросы?
Интервьюер проверяет: можешь ли ты диагностировать проблему производительности, а не просто писать запросы.
EXPLAIN показывает план выполнения запроса — как СУБД собирается получить данные:
EXPLAIN ANALYZE
SELECT e.name, d.name
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.salary > 100000;
Что искать в плане:
- Seq Scan (full table scan) — СУБД читает всю таблицу. На большой таблице — красный флаг. Решение: добавить индекс.
- Index Scan / Index Only Scan — используется индекс. Index Only Scan — ещё лучше (данные берутся прямо из индекса).
- Nested Loop / Hash Join / Merge Join — стратегии соединения. Nested Loop хорош для маленьких таблиц, Hash Join — для больших.
- Rows — оценка количества строк. Если сильно отличается от реальности — статистика устарела (нужен ANALYZE).
- Cost — условная стоимость операции. Чем меньше — тем лучше.
Чек-лист оптимизации:
- Убрать
SELECT *— выбирай только нужные столбцы - Добавить индексы на столбцы в WHERE, JOIN, ORDER BY
- Избегать функций на индексированных столбцах в WHERE:
WHERE YEAR(created_at) = 2025убивает индекс →WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01' - Заменить коррелированные подзапросы на JOIN
- Использовать LIMIT для пагинации
Типичная ошибка: оптимизируют запрос вслепую, не глядя в EXPLAIN. Добавляют индексы наугад.
Follow-up: Что такое covering index? Индекс, который содержит все столбцы, нужные запросу. СУБД не обращается к таблице — всё берёт из индекса (Index Only Scan).
Половина Senior-вопросов позади. Дальше — физическое устройство индексов, блокировки и масштабирование. Это то, что отличает сеньора от мидла.
20. Кластерный vs некластерный индекс
Интервьюер проверяет: понимаешь ли ты, как данные хранятся физически.
Кластерный индекс определяет физический порядок хранения строк в таблице. Таблица может иметь только один кластерный индекс (потому что строки можно отсортировать физически только одним способом).
В PostgreSQL аналог — команда CLUSTER, но она не поддерживает автоматическое обновление порядка. В MySQL InnoDB первичный ключ всегда является кластерным индексом.
Некластерный индекс — отдельная структура с указателями на строки. Таблица может иметь много некластерных индексов.
-- MySQL InnoDB: PRIMARY KEY = кластерный индекс
CREATE TABLE orders (
id BIGINT PRIMARY KEY, -- кластерный индекс
customer_id INT,
created_at TIMESTAMP,
INDEX idx_customer (customer_id), -- некластерный
INDEX idx_date (created_at) -- некластерный
);
Типичная ошибка: говорят, что «кластерный индекс — это PRIMARY KEY». Не всегда: в SQL Server можно сделать кластерный индекс по любому столбцу. В MySQL InnoDB — да, PK = кластерный.
Follow-up: Почему в InnoDB не рекомендуется UUID как PRIMARY KEY? UUID не последователен — каждая вставка может попасть в середину B-tree, вызывая page splits. Автоинкрементный INT/BIGINT — вставки всегда в конец, что быстрее.
21. Что такое deadlock и как его избежать?
Интервьюер проверяет: сталкивался ли ты с реальными проблемами конкурентного доступа.
Deadlock — взаимная блокировка, когда две транзакции ждут друг друга и ни одна не может продолжить:
Транзакция A: заблокировала строку 1, ждёт строку 2
Транзакция B: заблокировала строку 2, ждёт строку 1
→ deadlock
-- Транзакция A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- блокирует строку 1
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- ждёт строку 2...
-- Транзакция B (параллельно)
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- блокирует строку 2
UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- ждёт строку 1 → DEADLOCK
Как избежать:
- Единый порядок блокировки: всегда блокируй ресурсы в одном порядке (по id). Если обе транзакции сначала блокируют id=1, потом id=2 — deadlock невозможен.
- Короткие транзакции: чем быстрее транзакция — тем меньше шанс столкновения.
- SELECT ... FOR UPDATE с осторожностью: явная блокировка полезна, но увеличивает окно для deadlock.
- Retry-логика: СУБД автоматически обнаруживает deadlock и откатывает одну транзакцию. Приложение должно быть готово повторить операцию.
Типичная ошибка: «у нас deadlock — давайте увеличим таймаут». Таймаут не решает проблему — он только замедляет обнаружение. Нужно менять порядок блокировок.
Follow-up: Как СУБД обнаруживает deadlock? Строит граф ожиданий (wait-for graph). Если находит цикл — одна из транзакций откатывается (обычно та, что сделала меньше работы).
22. Шардирование и репликация
Интервьюер проверяет: думаешь ли ты о масштабировании за пределами одного сервера.
Репликация — копирование данных на несколько серверов:
- Master-Slave (Primary-Replica): запись на master, чтение с реплик. Масштабирует чтение.
- Master-Master: запись на любой узел. Сложнее в управлении, конфликты.
Шардирование — разделение данных по нескольким серверам:
- Горизонтальное: строки распределяются по шардам (пользователи 1-1M на шарде 1, 1M-2M на шарде 2)
- Вертикальное: разные таблицы на разных серверах
Шардирование по user_id:
user_id % 4 = 0 → Shard 0
user_id % 4 = 1 → Shard 1
user_id % 4 = 2 → Shard 2
user_id % 4 = 3 → Shard 3
Проблемы шардирования:
- JOIN между шардами — очень дорого или невозможно
- Перебалансировка при добавлении шардов
- Уникальность id между шардами (UUID, snowflake ID)
- Транзакции между шардами (distributed transactions)
Типичная ошибка: предлагают шардирование как первое решение. Шардирование — крайняя мера. Сначала: индексы, оптимизация запросов, кэширование, репликация для чтения. Шардирование усложняет всё.
Follow-up: Как выбрать ключ шардирования? Ключ должен обеспечивать равномерное распределение и позволять большинству запросов попадать на один шард. user_id — хорош для B2C. tenant_id — для B2B/SaaS.
23. Как удалить дубликаты, оставив одну запись?
Интервьюер проверяет: можешь ли ты решить практическую задачу с использованием оконных функций.
Классическая задача, которую дают на собеседованиях в Яндексе, VK и Т-Банке (по отзывам на Хабре и Glassdoor). Комбинирует CTE и ROW_NUMBER:
-- PostgreSQL / MySQL 8+
WITH numbered AS (
SELECT
id,
email,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM users
)
DELETE FROM users
WHERE id IN (SELECT id FROM numbered WHERE rn > 1);
Логика: для каждой группы дубликатов (по email) нумеруем строки. Оставляем rn = 1 (с наименьшим id), остальные удаляем.
Альтернатива без оконных функций:
DELETE u1 FROM users u1
JOIN users u2
ON u1.email = u2.email
AND u1.id > u2.id;
Типичная ошибка: забывают ORDER BY в ROW_NUMBER — порядок строк будет недетерминированным, и какая запись останется — непредсказуемо.
Follow-up: Как предотвратить появление дубликатов в будущем? Добавить UNIQUE constraint: ALTER TABLE users ADD CONSTRAINT uq_email UNIQUE (email);
Итого Senior: 7 вопросов. Фокус — оконные функции, рекурсия, планы выполнения, физическое хранение, конкурентность, масштабирование. Если можешь прочитать EXPLAIN и объяснить, почему UUID как PK — плохая идея в InnoDB — ты на уровне.
Чего НЕ спрашивают
Не трать время на эти темы — их практически не задают на собеседованиях по SQL в 2024–2026:
- Cursor-ы — устаревший подход, заменён на CTE и оконные функции
- Синтаксис конкретной СУБД — от тебя ждут общее понимание SQL, а не знание нюансов T-SQL vs PL/pgSQL
- Детали администрирования — бэкапы, настройка репликации, конфигурация СУБД — это задачи DBA, не разработчика
- XML/JSON-функции в SQL — спрашивают редко, только если это специфика проекта
Как готовиться
- Junior (1-2 недели): база из части 1 + решай задачи на LeetCode SQL 50 и SQLBolt
- Middle (2-3 недели): вопросы из части 2 + разберись с EXPLAIN на реальных запросах + DataLemur
- Senior (3-4 недели): вся серия + проектирование схем с нуля + оптимизация медленных запросов + книга "SQL Performance Explained" (Markus Winand)
Как попробовать Sobes AI
Sobes AI — ИИ-ассистент, который помогает на техническом собеседовании в реальном времени. Подсказывает ответы, пока ты общаешься с интервьюером.
- Зайди на sobesai.app и скачай приложение
- Запусти перед собеседованием
- Sobes AI слушает вопрос и подсказывает структурированный ответ
- Работает с SQL, базами данных и другими техническими темами
Это часть 3 из 3. ← Часть 1: Junior | ← Часть 2: Middle
Готовитесь к собеседованию?
Sobes AI слушает вопросы интервьюера и генерирует ответы в реальном времени.
Скачать Sobes AI