Вопросы по SQL на собеседовании
SQL спрашивают почти на любом собесе, где есть данные. Проверяют не синтаксис, а понимание: что произойдёт со строками после LEFT JOIN с условием в WHERE, чем оконная функция отличается от группировки, почему запрос внезапно читает всю таблицу.
Что спрашивают
- +JOIN и его ловушки: куда деваются строки, когда фильтр по правой таблице уезжает в WHERE, чем FULL отличается от UNION
- +Оконные функции: ROW_NUMBER против RANK, рамка окна, зачем нужен PARTITION BY, когда окно дешевле self-join
- +Агрегация: GROUP BY и HAVING, поведение COUNT с NULL, агрегаты по нескольким уровням
- +Производительность: индексы и селективность, план запроса, почему функция над колонкой убивает индекс
Из чего состоит тема
Так тема разложена в тренажёре: движок ведёт прогресс по каждой подтеме отдельно и возвращает те, где вы ошибаетесь.
- Агрегации и GROUP BY42
- Оконные функции34
- Подзапросы и CTE34
- JOIN'ы31
- Производительность30
Разборы подтем
Конспект по каждой: что это, как отвечать вслух, на чём валятся, плюс вопросы для самопроверки.
- JOIN в SQL: виды, ON против WHERE и потерянные строки31 вопросов
- GROUP BY и HAVING в SQL42 вопросов
- Подзапросы и CTE в SQL34 вопросов
- Оптимизация SQL-запросов: индексы и план30 вопросов
- SQL: оконные функции34 вопросов
Примеры вопросов с разбором
- Чем WHERE отличается от HAVING?A)HAVING якобы работает заметно быстрее эквивалентного ему WHERE-условияB)WHERE фильтрует строки до GROUP BY, HAVING — группы после агрегацииC)WHERE умеет работать с числовыми колонками таблицыD)Разницы нет: HAVING — устаревший синоним WHERE
показать ответ и разбор
+B)WHERE фильтрует строки до GROUP BY, HAVING — группы после агрегации// разбор: Порядок логического выполнения: FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Условие на «сырые» колонки — в WHERE (и это эффективнее: меньше строк идёт в группировку); условие на COUNT/SUM — только в HAVING. «Города, где больше 100 юзеров» — типовой мини-тест на это.
- Таблица users (100 строк) LEFT JOIN orders по user_id. У части юзеров нет заказов, у части — несколько. Сколько строк будет в результате?A)Ровно 100 строк — по одной на каждого пользователяB)Ровно столько строк, сколько всего записей в таблице ordersC)Не меньше 100: без заказов — строка с NULL, с N заказами — N строкD)Не больше 100 строк при распределении заказов
показать ответ и разбор
+C)Не меньше 100: без заказов — строка с NULL, с N заказами — N строк// разбор: LEFT JOIN сохраняет все строки левой таблицы (без пары — с NULL справа) и размножает строку на каждое совпадение справа. Фан-аут join'а — источник классических багов с задвоением сумм: агрегируй до join'а или считай по distinct-ключу.
- Запрос WHERE DATE(created_at) = '2026-01-15' работает медленно, хотя на created_at есть B-tree индекс. Почему и как починить?A)Индекс повреждён, и его достаточно просто перестроить командой REINDEXB)B-tree индекс не умеет работать с типом датыC)Нужно просто добавить в запрос ограничение LIMITD)Функция над колонкой несаргируема — нужен диапазон >= '15' AND < '16'
показать ответ и разбор
+D)Функция над колонкой несаргируема — нужен диапазон >= '15' AND < '16'// разбор: B-tree ищет по значениям колонки; DATE(created_at) — уже другое выражение, планировщик уходит в seq scan. Полуинтервал сохраняет индексный range scan и корректен с таймстемпами любой точности. Общее правило: не заворачивай индексированную колонку в функции в WHERE/JOIN (то же с CAST и LOWER).
- Что вернёт запрос WHERE col NOT IN (SELECT x FROM t), если среди x есть хотя бы один NULL?A)Все строки, где значение col отсутствует в списке из подзапроса tB)Пустой результат: NULL в списке делает NOT IN всегда не-TRUEC)Только строки, где сам col содержит значение NULLD)Ошибку выполнения из-за NULL в списке значений
показать ответ и разбор
+B)Пустой результат: NULL в списке делает NOT IN всегда не-TRUE// разбор: col NOT IN (1, 2, NULL) раскрывается в col≠1 AND col≠2 AND col≠NULL; последнее — UNKNOWN, и вся конъюнкция не бывает TRUE. Классический прод-баг «запрос внезапно вернул ноль строк». Безопасные альтернативы: NOT EXISTS (NULL-нечувствителен) или фильтр WHERE x IS NOT NULL в подзапросе.
- Чем ROW_NUMBER(), RANK() и DENSE_RANK() отличаются на равных значениях сортировки?A)Между этими тремя функциями нет никакой разницы, кроме скорости их вычисленияB)RANK умеет работать с числовыми колонками сортировкиC)ROW_NUMBER — уникальны; RANK с пропусками (1,2,2,4); DENSE_RANK без (1,2,2,3)D)DENSE_RANK нумерует строки в обратном порядке сортировки
показать ответ и разбор
+C)ROW_NUMBER — уникальны; RANK с пропусками (1,2,2,4); DENSE_RANK без (1,2,2,3)// разбор: «Топ-3 зарплаты»: ROW_NUMBER отдаст ровно троих (кого из равных — решает тай-брейк в ORDER BY), RANK при дубле третьего места — четверых и больше (1,2,3,3), DENSE_RANK берёт все строки трёх лучших УРОВНЕЙ зарплаты (1,2,2,3). Выбор функции — это выбор бизнес-правила обращения с ничьими, проговори его вслух на собесе.
- COUNT(*), COUNT(col) и COUNT(DISTINCT col) — в чём разница?A)COUNT(*) — строки; COUNT(col) — не-NULL; DISTINCT — уникальные не-NULLB)Все три формы COUNT — полные синонимы с одинаковым результатомC)COUNT(*) заметно медленнее остальных форм, поэтому его стараются избегать в продеD)COUNT(col) учитывает каждый NULL как отдельное посчитанное значение
показать ответ и разбор
+A)COUNT(*) — строки; COUNT(col) — не-NULL; DISTINCT — уникальные не-NULL// разбор: NULL-семантика — любимая почва для подвохов: COUNT(phone) на таблице юзеров — это «сколько юзеров с телефоном», не «сколько юзеров». Тот же принцип в AVG/SUM: NULL игнорируются, и AVG(col) ≠ SUM(col)/COUNT(*) при наличии NULL. COUNT(DISTINCT) на больших данных дорог — в аналитике часто берут approx_count_distinct.
- Почему условие на правую таблицу в WHERE превращает LEFT JOIN в INNER JOIN, и как написать правильно?A)Не превращает — это распространённый миф про поведение LEFT JOIN и WHEREB)WHERE выполняется до самого JOIN, поэтому фильтр по нему не срабатываетC)LEFT JOIN не получится сочетать с условием WHERED)У левых строк orders = NULL; фильтр по правой в WHERE их режет — условие в ON
показать ответ и разбор
+D)У левых строк orders = NULL; фильтр по правой в WHERE их режет — условие в ON// разбор: Семантика: JOIN формирует строки, затем WHERE фильтрует результат; NULL ≠ 'paid', и «левые» строки исчезают. LEFT JOIN orders o ON o.user_id = u.id AND o.status = 'paid' сохраняет всех юзеров, присоединяя только оплаченные заказы. Один из самых частых вопросов на живом SQL-кодинге.
- EXPLAIN ANALYZE показывает Nested Loop с оценкой rows=1, а фактически — 500 000 строк, запрос на часы. В чём типичная причина и что делать?A)Планировщик промахнулся в кардинальности; лечат ANALYZE и CREATE STATISTICSB)Nested Loop безнадёжно медленный, его нужно отключить в конфигеC)Диск слишком медленный, поможет переход на быстрый SSDD)EXPLAIN показывает неправду, реальный план выполнения совсем другой
показать ответ и разбор
+A)Планировщик промахнулся в кардинальности; лечат ANALYZE и CREATE STATISTICS// разбор: Оценки кардинальности — топливо оптимизатора: при «rows=1» nested loop с индексным поиском выглядит идеально, а на полумиллионе итераций умирает. Частые виновники: зависимые колонки (город+регион), выражения и LIKE-паттерны, свежезалитые данные без ANALYZE. Сеньорский навык — читать разрыв estimated vs actual и чинить статистику, а не хинтовать вслепую.
- Зачем нужны CTE (WITH ...) и что важно знать про их материализацию в Postgres?A)CTE безусловно ускоряют выполнение запросаB)CTE — это полноценные временные таблицы, живущие до самого конца сессии подключенияC)CTE именуют шаги (+рекурсия); Postgres <12 материализует, 12+ инлайнитD)Один и тот же CTE не получится сослать дважды в рамках запроса
показать ответ и разбор
+C)CTE именуют шаги (+рекурсия); Postgres <12 материализует, 12+ инлайнит// разбор: Читаемость многошаговой логики — главное назначение; рекурсивные CTE закрывают орг-структуры и графы. Нюанс производительности: материализованный CTE не получает pushdown предикатов — на старых Postgres «красивый» запрос внезапно сканирует всю таблицу. С версии 12 однократно используемые CTE инлайнятся автоматически.
это 9 из 171
Ещё 162 вопросов по теме — в тренажёре, с движком повторения
Прочитать разбор и ответить самому — разные навыки. В Сеньорчике вопросы идут сессиями, а движок возвращает подтемы, где вы ошибаетесь, пока они не начнут отскакивать. Бесплатно, лимит по энергии.
Частые вопросы
Какой уровень SQL спрашивают на собеседовании аналитика?
До оконных функций и подзапросов включительно. Голый SELECT с WHERE проверяют разве что на стажировке, дальше идут JOIN нескольких таблиц, группировки по нескольким полям, окна и умение объяснить план запроса.
Дают ли писать SQL прямо на собеседовании?
Часто дают: задача в редакторе или на доске, реже тестовое с датасетом. Обычно важнее ход мысли, чем идеальный синтаксис, но ошибка в JOIN или потеря NULL считается серьёзной.
Что чаще всего заваливают в SQL?
Три вещи: фильтр по правой таблице в LEFT JOIN, разница между WHERE и HAVING, поведение NULL в сравнениях и агрегатах. Всё это выглядит просто на бумаге и рассыпается, когда просят объяснить результат.