SQL собеседование Senior 2026 — оконные функции, CTE
S.
Sobes AI

Вопросы по SQL на собеседовании — часть 3/3: Senior с разбором ответов

16.03.2026 | 4 мин чтения | 1 просмотров

Это часть 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

Интервьюер проверяет: можешь ли ты решать аналитические задачи без подзапросов и 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 — условная стоимость операции. Чем меньше — тем лучше.

Чек-лист оптимизации:

  1. Убрать SELECT * — выбирай только нужные столбцы
  2. Добавить индексы на столбцы в WHERE, JOIN, ORDER BY
  3. Избегать функций на индексированных столбцах в WHERE: WHERE YEAR(created_at) = 2025 убивает индекс → WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01'
  4. Заменить коррелированные подзапросы на JOIN
  5. Использовать 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

Как избежать:

  1. Единый порядок блокировки: всегда блокируй ресурсы в одном порядке (по id). Если обе транзакции сначала блокируют id=1, потом id=2 — deadlock невозможен.
  2. Короткие транзакции: чем быстрее транзакция — тем меньше шанс столкновения.
  3. SELECT ... FOR UPDATE с осторожностью: явная блокировка полезна, но увеличивает окно для deadlock.
  4. 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 — ИИ-ассистент, который помогает на техническом собеседовании в реальном времени. Подсказывает ответы, пока ты общаешься с интервьюером.

  1. Зайди на sobesai.app и скачай приложение
  2. Запусти перед собеседованием
  3. Sobes AI слушает вопрос и подсказывает структурированный ответ
  4. Работает с SQL, базами данных и другими техническими темами

Это часть 3 из 3. ← Часть 1: Junior | ← Часть 2: Middle

Готовитесь к собеседованию?

Sobes AI слушает вопросы интервьюера и генерирует ответы в реальном времени.

Скачать Sobes AI