10 пар SQL-команд, которые путают на собесе аналитика

Прокачай SQL для собеса
500+ задач по SQL: оконные функции, JOIN, CTE — с разбором каждой
Тренировать SQL в Telegram

Зачем учить пары команд

На собеседовании аналитика проверяют не «знание SQL», а понимание нюансов. Большинство кандидатов уверенно пишут SELECT и JOIN, но плывут на парах команд, которые делают похожие вещи разными способами.

Эти 10 пар собраны из реальных собесов в Яндексе, Озоне, Сбере, Тинькофф, Wildberries и других продуктовых компаниях. Каждая — частый вопрос, который интервьюеры задают чтобы отличить «писал SQL раз в месяц» от «работаю с базой каждый день».

Под каждой парой — короткий разбор и ссылка на полную статью с примерами и подводными камнями.

WHERE vs HAVING

WHERE фильтрует строки до агрегации. HAVINGпосле GROUP BY. WHERE не работает с агрегатами (COUNT, SUM), HAVING — наоборот, без них почти бесполезен. Если фильтр не использует агрегат — пишите его в WHERE: оптимизатор отбросит лишние строки до группировки.

SELECT department, COUNT(*) AS cnt
FROM employees
WHERE salary > 100000      -- до агрегации
GROUP BY department
HAVING COUNT(*) > 5;       -- после агрегации

Полный разбор WHERE vs HAVING →

LEFT JOIN vs INNER JOIN

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

Полный разбор LEFT vs INNER JOIN →

UNION vs UNION ALL

UNION объединяет результаты и убирает дубликаты (внутри делает сортировку и удаление). UNION ALL оставляет всё как есть — быстрее в разы. Если вы знаете, что дублей не будет, или они нужны для подсчёта — берите UNION ALL.

Полный разбор UNION vs UNION ALL →

EXISTS vs IN

IN проверяет вхождение значения в список. EXISTS проверяет, есть ли хотя бы одна строка в подзапросе. На больших таблицах EXISTS обычно быстрее, потому что прекращает проверку на первой найденной строке. Главная ловушка — NOT IN с NULL'ами в подзапросе всегда возвращает ложь, в отличие от NOT EXISTS.

Полный разбор EXISTS vs IN →

DISTINCT vs GROUP BY

Оба убирают дубликаты. DISTINCT — простой и читается прозрачнее. GROUP BY — мощнее: позволяет применить агрегаты по группам. Производительность примерно одинаковая на современных движках, но GROUP BY чаще оптимизатор разворачивает в hash-aggregate, а DISTINCT — в sort-distinct.

Полный разбор DISTINCT vs GROUP BY →

RANK vs ROW_NUMBER vs DENSE_RANK

Все три — оконные функции для ранжирования. ROW_NUMBER присваивает уникальный номер строке (даже если значения в ORDER BY одинаковые). RANK даёт одинаковый номер дублям, но следующий номер пропускает (1, 1, 3). DENSE_RANK — одинаковый номер дублям, но без пропусков (1, 1, 2).

Полный разбор RANK vs ROW_NUMBER vs DENSE_RANK →

Прокачай SQL для собеса
500+ задач по SQL: оконные функции, JOIN, CTE — с разбором каждой
Тренировать SQL в Telegram

CTE vs подзапрос

CTE (WITH) — именованный блок, который читается сверху вниз. Подзапрос — анонимная вложенность. CTE удобнее для длинных запросов, потому что можно ссылаться на него несколько раз. Раньше CTE в Postgres материализовался (фактически создавал временную таблицу), сейчас — может встраиваться оптимизатором как обычный подзапрос. Рекурсия — только в CTE.

Полный разбор CTE vs подзапрос →

COUNT(*) vs COUNT(column)

COUNT(*) считает все строки, включая те, где все значения NULL. COUNT(column) считает только строки, где column IS NOT NULL. На собесе любят спросить: «Что вернёт COUNT(email) если у трёх пользователей email пустой?» — правильный ответ: общее количество минус 3.

Полный разбор COUNT(*) vs COUNT(column) →

DELETE vs TRUNCATE

DELETE удаляет строки построчно — медленнее, пишется в WAL, можно с WHERE, можно откатить транзакцией. TRUNCATE очищает таблицу целиком — быстрее, не пишется построчно в лог, не запускает row-level триггеры, не возвращает количество удалённых строк.

Полный разбор DELETE vs TRUNCATE →

GROUP BY vs PARTITION BY

GROUP BY сворачивает строки в группы — на выходе одна строка на группу. PARTITION BY (внутри оконных функций) разбивает данные на группы, но строки остаются — каждая строка получает агрегат своей партиции рядом. Если нужно посчитать долю строки от суммы её группы — это PARTITION BY.

-- доля заказа от выручки магазина
SELECT
  store, order_id, amount,
  amount * 1.0 / SUM(amount) OVER (PARTITION BY store) AS share
FROM orders;

Полный разбор GROUP BY vs PARTITION BY →

Частые ошибки

1. Использовать оконные функции в WHERE. Оконки выполняются после WHERE — нужно завернуть в CTE или подзапрос.

2. Забыть про NULL'ы в NOT IN. Если в подзапросе хотя бы одно NULL — NOT IN вернёт пусто. Используйте NOT EXISTS.

3. Считать, что UNION всегда сохранит порядок. UNION может перетасовать строки при удалении дублей. Если порядок важен — внешний ORDER BY.

4. Брать COUNT(*) чтобы посчитать заполненные значения. Считаете людей с заполненным email? Только COUNT(email).

5. Применять WHERE к правой таблице после LEFT JOIN. Это превращает LEFT JOIN в INNER JOIN. Условие на правую таблицу — в ON, не в WHERE.

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

FAQ

Какие пары SQL-команд чаще всего спрашивают на собесе?

По опыту собесов в продуктовых компаниях — WHERE vs HAVING, LEFT JOIN vs INNER JOIN, UNION vs UNION ALL и оконные функции (RANK, ROW_NUMBER, DENSE_RANK). Эти четыре есть почти на каждом интервью аналитика.

Чем отличается DISTINCT от GROUP BY на больших данных?

Производительность близкая, но оптимизатор по-разному строит план. GROUP BY чаще получает hash-aggregate, DISTINCT — sort-distinct. На очень больших таблицах разница до 2× в пользу GROUP BY, но на средних — почти неразличима.

Когда EXISTS быстрее IN?

На больших подзапросах. EXISTS прерывается на первой найденной строке — оптимизатор может использовать short-circuit. IN сначала формирует полный набор значений. На маленьких списках разницы нет.

Можно ли использовать оконные функции в WHERE?

Нельзя. Оконки выполняются на этапе SELECT, после WHERE. Чтобы фильтровать по результату оконки — заворачивайте в CTE или подзапрос и фильтруйте по полю.

Что вернёт COUNT(*) если все строки имеют NULL в одной колонке?

COUNT(*) вернёт общее количество строк — он не зависит от значений. А вот COUNT(column) для этой колонки вернёт 0.


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