сеньорчикОткрыть в Telegram
← все вопросывопросы для собеседований · SQL

Вопросы по SQL на собеседовании

SQL спрашивают почти на любом собесе, где есть данные. Проверяют не синтаксис, а понимание: что произойдёт со строками после LEFT JOIN с условием в WHERE, чем оконная функция отличается от группировки, почему запрос внезапно читает всю таблицу.

171 вопросов в банке·5 подтем·ниже разбор 9

Что спрашивают

Из чего состоит тема

Так тема разложена в тренажёре: движок ведёт прогресс по каждой подтеме отдельно и возвращает те, где вы ошибаетесь.

Разборы подтем

Конспект по каждой: что это, как отвечать вслух, на чём валятся, плюс вопросы для самопроверки.

Примеры вопросов с разбором

  1. #aggregation_groupby1 / 9
    Чем 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 юзеров» — типовой мини-тест на это.

  2. #joins2 / 9
    Таблица users (100 строк) LEFT JOIN orders по user_id. У части юзеров нет заказов, у части — несколько. Сколько строк будет в результате?
    A)Ровно 100 строк — по одной на каждого пользователя
    B)Ровно столько строк, сколько всего записей в таблице orders
    C)Не меньше 100: без заказов — строка с NULL, с N заказами — N строк
    D)Не больше 100 строк при распределении заказов
    показать ответ и разбор
    +C)Не меньше 100: без заказов — строка с NULL, с N заказами — N строк

    // разбор: LEFT JOIN сохраняет все строки левой таблицы (без пары — с NULL справа) и размножает строку на каждое совпадение справа. Фан-аут join'а — источник классических багов с задвоением сумм: агрегируй до join'а или считай по distinct-ключу.

  3. #performance3 / 9
    Запрос WHERE DATE(created_at) = '2026-01-15' работает медленно, хотя на created_at есть B-tree индекс. Почему и как починить?
    A)Индекс повреждён, и его достаточно просто перестроить командой REINDEX
    B)B-tree индекс не умеет работать с типом даты
    C)Нужно просто добавить в запрос ограничение LIMIT
    D)Функция над колонкой несаргируема — нужен диапазон >= '15' AND < '16'
    показать ответ и разбор
    +D)Функция над колонкой несаргируема — нужен диапазон >= '15' AND < '16'

    // разбор: B-tree ищет по значениям колонки; DATE(created_at) — уже другое выражение, планировщик уходит в seq scan. Полуинтервал сохраняет индексный range scan и корректен с таймстемпами любой точности. Общее правило: не заворачивай индексированную колонку в функции в WHERE/JOIN (то же с CAST и LOWER).

  4. #subqueries_cte4 / 9
    Что вернёт запрос WHERE col NOT IN (SELECT x FROM t), если среди x есть хотя бы один NULL?
    A)Все строки, где значение col отсутствует в списке из подзапроса t
    B)Пустой результат: NULL в списке делает NOT IN всегда не-TRUE
    C)Только строки, где сам col содержит значение NULL
    D)Ошибку выполнения из-за 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 в подзапросе.

  5. #window_functions5 / 9
    Чем 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). Выбор функции — это выбор бизнес-правила обращения с ничьими, проговори его вслух на собесе.

  6. #aggregation_groupby6 / 9
    COUNT(*), COUNT(col) и COUNT(DISTINCT col) — в чём разница?
    A)COUNT(*) — строки; COUNT(col) — не-NULL; DISTINCT — уникальные не-NULL
    B)Все три формы 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.

  7. #joins7 / 9
    Почему условие на правую таблицу в WHERE превращает LEFT JOIN в INNER JOIN, и как написать правильно?
    A)Не превращает — это распространённый миф про поведение LEFT JOIN и WHERE
    B)WHERE выполняется до самого JOIN, поэтому фильтр по нему не срабатывает
    C)LEFT JOIN не получится сочетать с условием WHERE
    D)У левых строк 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-кодинге.

  8. #performance8 / 9
    EXPLAIN ANALYZE показывает Nested Loop с оценкой rows=1, а фактически — 500 000 строк, запрос на часы. В чём типичная причина и что делать?
    A)Планировщик промахнулся в кардинальности; лечат ANALYZE и CREATE STATISTICS
    B)Nested Loop безнадёжно медленный, его нужно отключить в конфиге
    C)Диск слишком медленный, поможет переход на быстрый SSD
    D)EXPLAIN показывает неправду, реальный план выполнения совсем другой
    показать ответ и разбор
    +A)Планировщик промахнулся в кардинальности; лечат ANALYZE и CREATE STATISTICS

    // разбор: Оценки кардинальности — топливо оптимизатора: при «rows=1» nested loop с индексным поиском выглядит идеально, а на полумиллионе итераций умирает. Частые виновники: зависимые колонки (город+регион), выражения и LIKE-паттерны, свежезалитые данные без ANALYZE. Сеньорский навык — читать разрыв estimated vs actual и чинить статистику, а не хинтовать вслепую.

  9. #subqueries_cte9 / 9
    Зачем нужны 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 вопросов по теме — в тренажёре, с движком повторения

Прочитать разбор и ответить самому — разные навыки. В Сеньорчике вопросы идут сессиями, а движок возвращает подтемы, где вы ошибаетесь, пока они не начнут отскакивать. Бесплатно, лимит по энергии.

Частые вопросы