Зачем спрашивают ловушки, а не только задачи
На собесах аналитика SQL-секция состоит из двух частей. Первая — задачи «напишите запрос»: их типы мы разобрали в гайде «30 типов SQL-задач». Вторая — короткие теоретические вопросы с подвохом: «что вернёт 3 + NULL?», «в каком порядке выполняется запрос?», «сколько строк вернёт LEFT JOIN?». Интервьюер за две минуты проверяет, писали вы запросы руками или выучили синтаксис по курсу.
Эти вопросы любят везде, где SQL дают на скрининге: в бигтехе, банках, e-com. Судя по отзывам кандидатов, чаще всего в блицах всплывает перенос условия из WHERE в ON — разберём его отдельным блоком.
Ниже 48 вопросов с ответами, сгруппированных по темам. Формат такой: формулировка вопроса, короткий ответ, затем разбор. Если времени мало — в конце есть блиц-шпаргалка.
NULL и трёхзначная логика (вопросы 1–15)
Половина всех ловушек — про NULL. Причина одна: NULL означает «значение неизвестно», и все операции с ним живут по своим правилам.
1. Что вернёт 3 + NULL?
NULL. Любая арифметическая операция с NULL даёт NULL: 3 + NULL, 3 * NULL, NULL / 5 — всё NULL. Логика: «три плюс неизвестно что» равно «неизвестно что». То же с конкатенацией строк: 'a' || NULL вернёт NULL (в MySQL конкатенация — через CONCAT, результат тот же). Исключение — Oracle: там NULL в конкатенации ведёт себя как пустая строка, и 'a' || NULL вернёт 'a' — та же особенность, что в вопросе 15.
2. Чему равно NULL = NULL?
Не TRUE. Результат сравнения — UNKNOWN («неизвестно»). Два неизвестных значения нельзя признать равными: может, там 5 и 5, а может, 5 и 7. Проверять NULL можно только через IS NULL / IS NOT NULL. В Postgres есть ещё IS NOT DISTINCT FROM — сравнение, которое считает два NULL равными; в MySQL для этого оператор <=>.
3. Какие значения бывают у логического выражения в SQL?
Три: TRUE, FALSE и UNKNOWN. Это называется трёхзначной логикой. Ключевое следствие: WHERE пропускает только строки с TRUE — и FALSE, и UNKNOWN отбрасываются. Ещё пара правил, на которых ловят: NOT UNKNOWN = UNKNOWN, UNKNOWN AND TRUE = UNKNOWN, но UNKNOWN OR TRUE = TRUE.
4. В таблице есть строки со status = NULL. Вернёт ли их запрос WHERE status != 'paid'?
Нет. NULL != 'paid' даёт UNKNOWN, а UNKNOWN не проходит через WHERE. Хотите включить такие строки — пишите WHERE status != 'paid' OR status IS NULL. Это одна из самых частых прод-ошибок: фильтр «все, кроме оплаченных» молча теряет строки с неизвестным статусом.
5. Что вернёт NOT IN, если в списке есть NULL?
Пустой результат — так во всех классических СУБД: Postgres, MySQL, SQL Server, Oracle. Разбор по шагам: x NOT IN (1, NULL) разворачивается в x != 1 AND x != NULL. Второе сравнение — UNKNOWN, а что-угодно AND UNKNOWN не бывает TRUE. Ни одна строка не проходит. Поэтому антиджойн через NOT IN (SELECT user_id FROM orders) ломается, как только в подзапросе появляется хоть один NULL. Лечение: NOT EXISTS или явный фильтр WHERE user_id IS NOT NULL в подзапросе. Особняком ClickHouse: он не следует трёхзначной логике в IN/NOT IN — по умолчанию (настройка transform_null_in = 0) NULL в списке просто игнорируется, и 2 NOT IN (1, NULL) там вернёт строку.
Тренировка на живом примере: Как обезопасить NOT IN от NULL-ловушки и Habr · Пользователи без заказов.
6. А почему IN с NULL в списке работает «нормально»?
Потому что для IN подвох не срабатывает в ту же сторону. 1 IN (1, NULL) — это 1 = 1 OR 1 = NULL → TRUE OR UNKNOWN → TRUE. Совпадение нашлось — строка проходит. 2 IN (1, NULL) даст UNKNOWN, строка не пройдёт — но она и не должна была. А вот при отрицании UNKNOWN превращается в проблему из вопроса 5. Запомнить просто: NULL в списке безопасен для IN и фатален для NOT IN.
7. Чем отличаются COUNT(*), COUNT(колонка) и COUNT(DISTINCT колонка)?
COUNT(*) считает строки, включая любые NULL. COUNT(col) считает строки, где col не NULL. COUNT(DISTINCT col) — уникальные значения col без учёта NULL. На собесах дают таблицу из пяти строк с парой NULL и просят назвать три числа — потренируйтесь на бумажке.
8. AVG учитывает NULL?
Нет. AVG(col) = SUM(col) / COUNT(col) — и сумма, и счётчик игнорируют NULL. Если в колонке из 10 строк 5 значений по 100 и 5 NULL, AVG вернёт 100, а не 50. Когда NULL по смыслу означает ноль (например, «не было покупок»), сначала AVG(COALESCE(col, 0)).
9. Что вернёт SUM, если подходящих строк нет?
NULL, не ноль. SUM, AVG, MIN, MAX по пустому набору строк возвращают NULL; ноль возвращает только COUNT. Если дальше этот результат участвует в арифметике, NULL расползётся по всему расчёту (вопрос 1). Страховка: COALESCE(SUM(x), 0). Оговорка для ClickHouse: там агрегаты по пустому набору для обычных (не-Nullable) колонок возвращают значение по умолчанию типа — sum() даст 0, а не NULL.
10. Как GROUP BY обходится с NULL?
Складывает все NULL в одну группу. Хотя NULL = NULL не TRUE (вопрос 2), группировка использует другое сравнение — «не различимы» — и собирает все NULL вместе. DISTINCT ведёт себя так же: из десяти NULL останется один. Это осознанное исключение стандарта, и на собесах любят спросить именно про контраст с вопросом 2.
11. Куда попадут NULL при ORDER BY?
Зависит от СУБД. Postgres считает NULL самыми большими: при ASC они в конце, при DESC — в начале. MySQL наоборот — считает их самыми маленькими. В Postgres порядок задаётся явно: ORDER BY col DESC NULLS LAST. В MySQL такого синтаксиса нет, эмулируют через ORDER BY col IS NULL, col. Правильный ответ начинается со слов «зависит от базы» — это плюс балл.
12. Сджойнятся ли две строки, у которых ключ NULL?
Нет. Условие a.key = b.key при двух NULL даёт UNKNOWN — пара не образуется. Строки с NULL-ключом выпадают из INNER JOIN полностью, а в LEFT JOIN остаются без пары. Если по бизнес-логике NULL-ключи должны матчиться, нужен ON a.key IS NOT DISTINCT FROM b.key (Postgres) — но сначала стоит спросить себя, почему в ключе NULL.
13. Почему CASE col WHEN NULL THEN ... никогда не сработает?
Потому что простая форма CASE сравнивает через =: col = NULL — всегда UNKNOWN. Ветка не выберется даже для строк, где col действительно NULL. Правильно — развёрнутая форма: CASE WHEN col IS NULL THEN ... END.
14. Как избежать ошибки деления на ноль?
Классический приём — NULLIF: запись x / NULLIF(y, 0) при y = 0 превращает знаменатель в NULL, и вместо ошибки весь результат становится NULL (дальше можно обернуть в COALESCE). NULLIF(a, b) возвращает NULL, если a = b, иначе a. Обратная функция — COALESCE(a, b, ...): первый не-NULL аргумент.
15. Пустая строка '' и NULL — одно и то же?
В Postgres, MySQL, ClickHouse — разные вещи: '' это известное значение «пустая строка». В Oracle — одно и то же: '' при вставке превращается в NULL, и WHERE col = '' не вернёт ничего. Вопрос любят задавать тем, кто указал Oracle в резюме.
Порядок написания и порядок выполнения запроса (вопросы 16–23)
Второй по популярности блок. Сам вопрос звучит безобидно, но за ним стоит серия следствий, которые проверяют глубину.
16. В каком порядке пишутся и в каком выполняются операторы?
Пишется запрос так: SELECT → FROM → JOIN ... ON → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT.
Логически выполняется иначе:
FROM+JOIN/ON— собираем строки;WHERE— фильтруем строки;GROUP BY— группируем;HAVING— фильтруем группы;SELECT— вычисляем выражения, оконные функции;DISTINCT— убираем дубли;ORDER BY— сортируем;LIMIT/OFFSET— отрезаем.
Важная оговорка для сильного ответа: это логический порядок, то есть модель, по которой определяется результат. Физически оптимизатор волен переставлять шаги как угодно, лишь бы результат совпал. Все следующие вопросы блока — следствия этой схемы.
17. Почему алиас из SELECT нельзя использовать в WHERE?
Потому что WHERE выполняется на шаге 2, а алиасы рождаются в SELECT на шаге 5 — в момент фильтрации их ещё не существует. SELECT price * qty AS revenue ... WHERE revenue > 100 — ошибка в Postgres, MySQL, SQL Server и Oracle. Варианты: повторить выражение в WHERE или вынести его в подзапрос/CTE. Исключение — ClickHouse: он разворачивает алиасы по всему запросу, и такой WHERE там выполнится.
18. А в GROUP BY, HAVING и ORDER BY алиас использовать можно?
В ORDER BY — можно везде: сортировка идёт после SELECT. В GROUP BY — по стандарту нельзя, но Postgres, MySQL и ClickHouse разрешают. В HAVING — разрешают MySQL и ClickHouse (там алиас вообще виден в любой части запроса). Полный ответ «в ORDER BY можно, в остальных зависит от СУБД» звучит сильнее категоричного.
19. Почему агрегатную функцию нельзя писать в WHERE?
WHERE фильтрует отдельные строки до группировки — в этот момент COUNT(*) ещё нечего считать. Условия на агрегаты живут в HAVING, который выполняется после GROUP BY. Запрос «категории, где больше 10 заказов» — это HAVING COUNT(*) > 10, и никак иначе.
Тренировка: Habr · WHERE vs HAVING.
20. Можно ли оконную функцию использовать в WHERE?
Нет. Окна вычисляются на этапе SELECT, после WHERE и HAVING. «Оставить топ-3 в каждой группе» через WHERE ROW_NUMBER() OVER (...) <= 3 не скомпилируется. Стандартное решение — подзапрос или CTE: сначала посчитать rn, потом снаружи WHERE rn <= 3. В некоторых диалектах (Snowflake, BigQuery) есть QUALIFY — фильтр специально для окон.
Тренировка: Какие утверждения об оконных функциях верны.
21. Чем WHERE отличается от HAVING? Работает ли HAVING без GROUP BY?
WHERE фильтрует строки до группировки, HAVING — группы после. Дополнительный подвох: HAVING без GROUP BY — валидный запрос; вся таблица считается одной группой. SELECT COUNT(*) FROM orders HAVING COUNT(*) > 100 вернёт либо одну строку, либо ни одной.
22. Что вернёт LIMIT 10 без ORDER BY?
Десять произвольных строк. Без ORDER BY порядок не гарантирован ничем: ни порядком вставки, ни первичным ключом. Сегодня запрос вернёт одни строки, после VACUUM или смены плана — другие. На собесах за фразу «таблица хранит строки по порядку добавления» снимают балл.
23. Сохранится ли ORDER BY, написанный внутри подзапроса?
Нет. Сортировка в подзапросе или представлении не обязывает внешний запрос сохранять порядок. Единственный надёжный способ отсортировать результат — ORDER BY на самом внешнем уровне. Исключение по смыслу одно: ORDER BY в подзапросе имеет эффект в связке с LIMIT (например, «топ-5 внутри, потом джойн»).
JOIN: минимум и максимум строк (вопросы 24–30)
Любимый формат вопроса: «в таблице A — n строк, в B — m строк; сколько строк вернёт JOIN?» Правильный ответ — всегда диапазон.
24. Минимум и максимум строк для каждого типа JOIN
Пусть в A — n строк, в B — m строк:
| Тип JOIN | Минимум | Максимум |
|---|---|---|
| CROSS JOIN | n × m | n × m |
| INNER JOIN | 0 | n × m |
| LEFT JOIN | n | n × m |
| RIGHT JOIN | m | n × m |
| FULL OUTER JOIN | max(n, m) | max(n × m, n + m) |
Крайние случаи, которые нужно уметь объяснить. Максимум n × m достигается, когда у всех строк обеих таблиц одинаковый ключ — каждая строка A матчится с каждой строкой B. Минимум LEFT JOIN равен n, потому что каждая строка левой таблицы попадает в результат хотя бы раз — с парой или с NULL. Минимум FULL JOIN равен max(n, m): при идеальном сопоставлении один к одному совпавшие пары дают min(n, m) строк, плюс непарные строки большей таблицы; а вот если совпадений нет вообще, FULL вернёт n + m — это не минимум, что многих удивляет. Когда в одной из таблиц всего одна строка, n + m даже больше, чем n × m, — поэтому максимум FULL записан как max(n × m, n + m).
Тренировка на кардинальности: Связь «ментор и стажёры»: сколько строк вернёт запрос.
25. Может ли LEFT JOIN вернуть строк больше, чем в левой таблице?
Да — если в правой таблице ключ дублируется. Одна строка слева размножается на каждую совпавшую справа. Это называют fan-out, и это второй по частоте источник прод-багов после NULL: джойн «справочника» вдруг оказался джойном таблицы с дублями, и метрика выросла вдвое.
26. Мини-задача на дубли: A = {1, 1, 2}, B = {1, 1, 3}. Сколько строк вернёт каждый JOIN по этому ключу?
Считаем по ключам. Ключ 1: две строки слева × две справа = 4 пары. INNER — 4 строки. LEFT — 4 + строка с ключом 2 без пары = 5. RIGHT — 4 + строка с ключом 3 = 5. FULL — 4 + 1 + 1 = 6. CROSS — 3 × 3 = 9. Общее правило: совпавшие ключи дают произведение количеств с каждой стороны, и эти произведения складываются.
27. После джойна SUM(выручки) удвоился. Что произошло?
Тот самый fan-out из вопроса 25: к таблице платежей приджойнили таблицу, где ключ встречается дважды, каждая строка платежа задвоилась, сумма — тоже. Решения: агрегировать правую таблицу до джойна (подзапрос с GROUP BY) либо дедуплицировать её (DISTINCT или ROW_NUMBER() ... = 1), а перед этим проверить дубли: SELECT key, COUNT(*) ... GROUP BY key HAVING COUNT(*) > 1. Хак SUM(DISTINCT amount) решением не считается: он схлопнет и настоящие разные платежи с одинаковой суммой. На собесах этот сюжет дают как «найди ошибку в запросе».
28. Что будет, если написать две таблицы через запятую и забыть условие?
FROM a, b — это старый синтаксис CROSS JOIN: декартово произведение, n × m строк. Раньше условие связи писали в WHERE, и забытое условие означало взрыв строк. Современный синтаксис JOIN ... ON защищает лишь частично: в Postgres, SQL Server и Oracle запрос с JOIN без ON упадёт с синтаксической ошибкой, а MySQL и SQLite молча выполнят его как декартово произведение — там ON у JOIN необязателен.
29. Чем CROSS JOIN отличается от FULL JOIN?
Их путают из-за слова «все». CROSS — все комбинации строк, всегда ровно n × m, условия нет. FULL — все строки обеих таблиц: совпавшие в парах, несовпавшие с NULL. Осмысленный CROSS в аналитике один — сетка «все даты × все пользователи» для заполнения пропусков.
30. Есть ли FULL OUTER JOIN в MySQL?
Нет — и это регулярный вопрос-проверка на практический опыт. В MySQL FULL эмулируют объединением: LEFT JOIN ... UNION ALL ... RIGHT JOIN с фильтром WHERE l.key IS NULL во второй части. Именно UNION ALL: обычный UNION схлопнет строки-дубли (на данных из вопроса 26 вместо 6 строк осталось бы 3). В Postgres, SQL Server, ClickHouse FULL есть.
Условие в ON или в WHERE — главный вопрос теорсекции (вопросы 31–35)
Блок, который стоит выучить наизусть: «что изменится, если перенести условие из WHERE в ON?» В отзывах о собесах аналитиков этот вопрос упоминают чаще любого другого теоретического — особенно в банках и бигтехе.
Опорный пример:
-- users: (1, Аня), (2, Боря), (3, Вера)
-- orders: (user_id 1, 'paid'), (user_id 1, 'canceled'), (user_id 2, 'canceled')
31. INNER JOIN: условие в ON или в WHERE — есть разница?
Разницы нет. Для внутреннего джойна оба варианта возвращают одно и то же: несовпавшие строки всё равно выбрасываются, поэтому неважно, отфильтровали вы их на этапе соединения или после. Оптимизатор строит для обоих вариантов один план. Разница появляется только у внешних джойнов — и это ядро вопроса.
32. LEFT JOIN + условие на правую таблицу в WHERE?
SELECT u.name, o.status
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.status = 'paid';
Результат — одна строка: Аня. У Веры заказов нет, её o.status — NULL, а NULL = 'paid' не проходит WHERE. Боря отпал по той же причине: его единственный статус — 'canceled'. Фильтр по правой таблице в WHERE молча превратил LEFT JOIN в INNER. Каждый раз, когда видите LEFT JOIN и условие на правую таблицу в WHERE, спрашивайте себя: а левые строки без пары здесь ещё живы?
33. LEFT JOIN + то же условие в ON?
SELECT u.name, o.status
FROM users u
LEFT JOIN orders o ON o.user_id = u.id AND o.status = 'paid';
Результат — три строки: Аня + 'paid', Боря + NULL, Вера + NULL. Условие в ON фильтрует правую таблицу до соединения: у Бори просто не нашлось подходящей пары, но сам он из результата не исчез — LEFT JOIN гарантирует все строки левой таблицы. Итоговое правило для собесов: у внешних джойнов условие в ON решает, «с чем матчить», условие в WHERE — «что оставить в результате».
34. А если при LEFT JOIN написать в ON условие на ЛЕВУЮ таблицу?
Самая коварная версия. LEFT JOIN orders o ON o.user_id = u.id AND u.name != 'Аня' не удалит Аню из результата: строки левой таблицы при LEFT JOIN не удаляются условием в ON никогда. Аня останется, но пару ей искать запретили — она выйдет с NULL вместо заказов. Хотели убрать Аню целиком — условие должно быть в WHERE. Этим вопросом добивают тех, кто ответил на два предыдущих.
35. Как через LEFT JOIN найти строки без пары — и в чём там подвох?
Классический антиджойн:
SELECT u.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.user_id IS NULL;
Здесь IS NULL обязан стоять именно в WHERE (после джойна) и проверять колонку, которая в правой таблице не бывает NULL сама по себе — лучше всего ключ соединения. Если проверять «обычную» колонку, которая и в реальных строках бывает NULL, в результат попадут лишние строки.
Живая задача на этот сюжет: SQL: условие WHERE по дебетовым картам.
Группировка и агрегаты (вопросы 36–39)
36. Можно ли в SELECT колонку, которой нет ни в GROUP BY, ни в агрегате?
Нельзя — Postgres и SQL Server сразу вернут ошибку. У группы из пяти строк пять разных значений этой колонки, и база не знает, какое показать. MySQL в режиме ONLY_FULL_GROUP_BY (включён по умолчанию с 5.7) тоже падает, а в старых версиях молча возвращал произвольное значение из группы — источник легендарных багов. Исключение: колонки, функционально зависимые от ключа группировки (сгруппировали по первичному ключу id — можно выводить name). Это разрешает стандарт и MySQL; Postgres — тоже, но только когда в GROUP BY именно первичный ключ: зависимость от unique-индекса он не распознаёт.
37. DISTINCT применяется к одной колонке или ко всей строке?
Ко всей строке результата. SELECT DISTINCT a, b вернёт уникальные пары (a, b), а не «уникальные a и рядом какие-то b». Выбрать «по одной строке на каждое a» обычным DISTINCT нельзя — это задача для ROW_NUMBER() OVER (PARTITION BY a ...) с фильтром rn = 1 (в Postgres есть и расширение DISTINCT ON (a): одна строка на каждое a, какая именно — решает ORDER BY). Смежный факт: DISTINCT без агрегатов эквивалентен GROUP BY по всем колонкам выборки.
Тренировка: Habr · Удаление дублей email.
38. Что значит GROUP BY 1?
Группировку по первой колонке из SELECT — по порядковому номеру. Работает в Postgres, MySQL, ClickHouse; удобно в разведочных запросах, но в проде хрупко: переставили колонки в SELECT — изменилась группировка. Достаточно узнать конструкцию и назвать этот риск.
39. LEFT JOIN + GROUP BY: почему у клиента без заказов COUNT(*) = 1, а не 0?
Потому что строка «клиент + NULL вместо заказа» — это всё равно строка, и COUNT(*) её посчитает. Чтобы клиенты без заказов получили честный ноль, считать нужно по колонке правой таблицы: COUNT(o.id) — NULL в неё не попадает (вопрос 7). Связка «LEFT JOIN, потом COUNT» без этой детали — готовый неправильный отчёт.
UNION и операции над множествами (вопросы 40–42)
40. Чем UNION отличается от UNION ALL?
UNION склеивает результаты и убирает дубликаты, UNION ALL — склеивает как есть. Отсюда два следствия. Первое: UNION дороже — под капотом сортировка или хеширование ради дедупликации; если дублей заведомо нет, пишите UNION ALL. Второе: при дедупликации UNION считает два NULL одинаковыми — снова то же «не различимы», что в GROUP BY и DISTINCT (вопрос 10).
41. Какие требования у UNION к колонкам? Чьи имена в результате?
Одинаковое количество колонок и совместимые типы в каждой позиции; сопоставление идёт по позиции, не по имени. Имена колонок результата берутся из первого SELECT — алиасы во втором и дальше молча игнорируются, что регулярно удивляет на практике.
42. Как работает ORDER BY в запросе с UNION?
Применяется ко всему объединённому результату, а не к последней части. Отсортировать одну часть до объединения так просто нельзя — да и незачем: порядок всё равно определяет только внешний ORDER BY (вопрос 23). Если нужно «сначала все строки первого источника», добавьте колонку-маркер источника и сортируйте по ней.
Ловушки диалектов и типов (вопросы 43–48)
43. Что вернёт SELECT 5 / 2?
Зависит от СУБД — и это любимая проверка практиков. Postgres и SQL Server делят целые нацело: 2. MySQL и ClickHouse — как в математике: 2.5. Практическое следствие — расчёт конверсии: COUNT(оплативших) / COUNT(зашедших) в Postgres даст 0 почти всегда. Лечение: привести к дробному типу — ::numeric в Postgres, умножить на 1.0, или AVG(CASE WHEN ... THEN 1 ELSE 0 END).
44. Почему BETWEEN по датам теряет последний день?
BETWEEN включает обе границы, с ним всё честно. Ловушка в типах: если колонка — timestamp, условие BETWEEN '2026-01-01' AND '2026-01-31' означает «до 31 января 00:00:00» — весь день 31-го, кроме полуночи, отрезан. Надёжный шаблон для полуинтервала: WHERE ts >= '2026-01-01' AND ts < '2026-02-01'.
45. Подзапрос после знака = вернул две строки. Что будет?
Ошибка выполнения — «subquery returned more than one row». Скалярное сравнение требует ровно одного значения (ноль строк тоже допустим — подзапрос вернёт NULL; в ClickHouse и пустой скалярный подзапрос — ошибка). Если значений может быть несколько, нужен IN, либо гарантия единственности: агрегат или LIMIT 1.
46. Чем отличаются ROW_NUMBER, RANK и DENSE_RANK на одинаковых значениях?
На значениях 100, 100, 90: ROW_NUMBER — 1, 2, 3 (всегда уникален, дубли нумерует произвольно); RANK — 1, 1, 3 (дырка после дублей); DENSE_RANK — 1, 1, 2 (без дырок). Для «второй по величине зарплаты» с дублями правильный выбор — DENSE_RANK; подробный разбор — в гайде по типам задач.
Тренировка: Habr · Вторая по величине зарплата, SQL · Вторая зарплата.
47. Почему LAST_VALUE возвращает «не то»?
Из-за фрейма окна по умолчанию: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW — окно заканчивается на текущей строке (точнее, на последней строке с тем же значением ключа сортировки), и «последним значением» оказывается сама текущая строка или её дубль по ORDER BY. Чтобы получить настоящее последнее значение раздела, задайте фрейм явно: ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING. FIRST_VALUE от дефолтного фрейма не страдает — первая строка в окно попадает всегда.
48. Чем отличаются DELETE, TRUNCATE и DROP?
DELETE — построчное удаление, поддерживает WHERE, пишет каждую строку в журнал, срабатывают триггеры, откатывается в транзакции. TRUNCATE — мгновенно очищает всю таблицу целиком, WHERE нет, построчные триггеры не срабатывают, в MySQL и SQL Server сбрасывает счётчик AUTO_INCREMENT/identity, а в Postgres счётчик сохраняется — сброс только явным TRUNCATE ... RESTART IDENTITY. DROP — удаляет саму таблицу вместе со структурой. Бонусный балл — за оговорку про транзакции: в Postgres TRUNCATE откатывается, в MySQL — нет (неявный коммит).
Блиц-шпаргалка: повторить за 5 минут до интервью
3 + NULL→ NULL; вся арифметика и конкатенация с NULL → NULL (конкатенация в Oracle — исключение)NULL = NULL→ UNKNOWN; проверка толькоIS NULL- WHERE пропускает только TRUE;
col != 'x'теряет NULL-строки NOT INсо списком, где есть NULL → пустой результат; лечится NOT EXISTS- COUNT(*) считает всё; COUNT(col) и AVG(col) пропускают NULL
- SUM по пустому набору → NULL (в ClickHouse — 0); COUNT → 0
- GROUP BY, DISTINCT и UNION считают NULL-ы «одинаковыми»
- Сортировка NULL: Postgres — в конце при ASC, MySQL — в начале; NULLS FIRST/LAST
- Логический порядок выполнения: FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT
- Алиас из SELECT: в WHERE нельзя (кроме ClickHouse), в ORDER BY можно, в GROUP BY/HAVING зависит от СУБД
- Фильтр по агрегату — только в HAVING; оконные функции — только в SELECT/ORDER BY (или QUALIFY)
- LIMIT без ORDER BY — произвольные строки
- Строки при джойне A(n) × B(m): CROSS = n·m; INNER 0…n·m; LEFT n…n·m; FULL max(n,m)…max(n·m, n+m)
- Дубли ключей размножают строки: суммы после джойна проверяй на fan-out
- INNER: условие в ON и в WHERE равнозначны
- LEFT: условие на правую таблицу в WHERE превращает джойн в INNER; в ON — сохраняет все левые строки
- LEFT: условие на левую таблицу в ON не удаляет её строки — только лишает их пары
- Антиджойн: LEFT JOIN +
WHERE правый_ключ IS NULL 5 / 2= 2 в Postgres и SQL Server, 2.5 в MySQL и ClickHouse- BETWEEN по timestamp теряет последний день; пиши
>= и < - LAST_VALUE требует явный фрейм
UNBOUNDED FOLLOWING
Как готовиться дальше
Теория из этой статьи закрывает блиц-вопросы, но на собесах её всегда сочетают с задачами «напишите запрос». План на две недели: прочитать этот справочник дважды с интервалом в несколько дней, прорешать SQL-задачи из каталога — начните с задач-ловушек (NOT IN с NULL, условие WHERE, кардинальность джойна), затем пройтись по 30 типам задач. Перед реальными собесами устройте себе репетицию: технический мок-собес с разбором или AI-симулятор интервью — теоретические блицы там тоже встречаются.