SQL на собеседовании в 2026-м редко проваливается из-за «не назвал все виды JOIN из учебника». Чаще ломается другое: человек пишет SELECT * и надеется, что интервьюер догадается. Нанимающий аналитика, backend или QA хочет увидеть, что вы умеете собрать нужные строки, не удвоить их джойном и объяснить, почему запрос может тормозить. Это проверка мышления на данных, не олимпиада по стандарту SQL:2016.
Ниже — темы, которые реально ставят, полные скетчи запросов в цитатах и как практиковаться на живой схеме, а не на мемах про DELETE без WHERE. Карта обучения — SQL roadmap. Общий каркас интервью — вопросы на собеседовании. Роли, где SQL в явном виде: вакансии с SQL.
Коротко:
- INNER/LEFT, условие в ON vs WHERE, размножение строк — база, без которой окна рано.
- GROUP BY и HAVING — разные этажи фильтра. Окна — «оставить строки, добавить ранг/сумму».
- Индекс и план: гипотеза, EXPLAIN, селективность. Не «надо кэш» первым словом.
- Пишите читаемый запрос: явные JOIN, понятные алиасы, без SELECT * в финале.
- Готовьте 3–4 запроса по своей предметке: список, агрегат, «последняя запись», дедуп.
Какой уровень на какую роль
QA: найти заказы пользователя, посчитать, проверить, что UI совпал с таблицей. Аналитик: воронка, повторные покупки, окна по дате. Backend: тот же JOIN плюс транзакция и «почему seq scan». Data engineer — ещё инкремент и качество. Не готовьте аналитические окна на QA-слот, если в вакансии «написать SELECT». Не приходите на аналитика только с SELECT по одной таблице.
Диалект: Postgres чаще на CIS product, иногда MySQL/ClickHouse. Синтаксис окон почти везде похож. Если в вакансии ClickHouse — готовьте движок и ограничения JOIN, не притворяйтесь, что это тот же Postgres.
JOIN: задача, которую ставят всегда
Формулировка: «пользователи и заказы. Вывести всех пользователей и сумму оплаченных заказов за 90 дней. У кого заказов нет — ноль, не пропасть из списка».
SELECT u.id, u.email, COALESCE(SUM(o.amount), 0) AS paid_90d FROM users u LEFT JOIN orders o ON o.user_id = u.id AND o.status = 'paid' AND o.created_at >= CURRENT_DATE - INTERVAL '90 days' GROUP BY u.id, u.email;
Почему LEFT: INNER выкинет пользователей без оплат. Почему фильтр дат и статуса в ON, не в WHERE: условие на o в WHERE после LEFT превращает его в INNER — строки с NULL-заказом отвалятся. COALESCE, чтобы в отчёте был 0, не NULL, если так просит заказчик.
Риск размножения: если у заказа несколько строк позиций и я по ошибке джойню items без агрегата — сумма раздуется. Тогда либо сначала агрегировать заказы в подзапросе, либо считать SUM по уникальному order_id аккуратно. На интервью я это проговариваю, не жду, пока найдут за меня.
Второй классический вопрос: «разница JOIN в ON и фильтр в WHERE». Ответ выше. Третий: «нарисуйте, сколько строк получится» на маленькой таблице — потренируйте на бумаге.
Группировка, HAVING, подзапрос
«Клиенты, у которых больше трёх оплаченных заказов за месяц».
SELECT user_id, COUNT(*) AS paid_cnt FROM orders WHERE status = 'paid' AND created_at >= DATE_TRUNC('month', CURRENT_DATE) GROUP BY user_id HAVING COUNT(*) > 3;
WHERE режет строки до группировки. HAVING — группы после. Писать HAVING на сырую дату вместо WHERE — ошибка: движок сначала тащит лишнее. Подзапрос уместен, если дальше джойнить к users за email: сначала ids, потом справочник, чтобы не тащить email в GROUP BY зря, если диалект требует все неагрегированные поля.
Окна: «последний статус» и дедуп
Формулировка: «по каждому заказу последняя строка из истории статусов».
WITH ranked AS ( SELECT s.order_id, s.status, s.changed_at, ROW_NUMBER() OVER (PARTITION BY s.order_id ORDER BY s.changed_at DESC, s.id DESC) AS rn FROM order_status_history s ) SELECT order_id, status, changed_at FROM ranked WHERE rn = 1;
ROW_NUMBER, не RANK, если коллизия по времени: RANK даст две «последние». Второй ключ id — стабильный тай-брейк. Альтернатива DISTINCT ON (order_id) в Postgres: SELECT DISTINCT ON (order_id) order_id, status, changed_at FROM order_status_history ORDER BY order_id, changed_at DESC, id DESC; — короче, менее переносимо. На интервью называю оба и спрашиваю, какой диалект у них.
SUM() OVER (PARTITION BY user_id ORDER BY created_at) — накопительно. Не путать с GROUP BY: строки не схлопываются. Если попросили «топ-3 заказа на пользователя» — тот же ROW_NUMBER, затем WHERE rn <= 3.
Это не трюк взлома и не «достать чужие данные». Это отчётный паттерн, который ждут аналитики и backend.
Индекс и «почему тормозит»
Вопрос: «список заказов пользователя по дате, таблица большая. Что смотрите?»
Сначала тот же запрос в EXPLAIN ANALYZE, не оптимизация вслепую. Смотрю seq scan vs index scan, сколько строк читаем vs сколько отдаём. Фильтр user_id + сортировка created_at — гипотеза: составной индекс (user_id, created_at). Отдельный индекс только по дате часто бесполезен для «моего пользователя».
SELECT o.id, o.created_at, o.status FROM orders o WHERE o.user_id = 1042 ORDER BY o.created_at DESC LIMIT 50;
Не тяну JOIN на историю статусов, если на списке три поля шапки. Кэш — после того, как запрос дешёвый. Иначе кэшируем медленный мусор. Честно: без селективности не обещаю «композитный всегда». Могу предложить гипотезу и проверку на копии, не создавать индекс на каждый столбец — запись тоже стоит.
Не предлагайте «выключить seq scan глобально» и не рассказывайте, как обходить права. План и индекс — достаточно.
NULL, дубли, аккуратность
COUNT(*) считает строки, COUNT(col) пропускает NULL. NOT IN с NULL в подзапросе — ловушка, лучше NOT EXISTS. Равенство NULL не через =, а IS NULL. На интервью это отделяют взрослых от тех, кто учил только INNER JOIN по id.
Пользователи без заказа через NOT EXISTS:
SELECT u.id FROM users u WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'paid' );
Эквивалент LEFT JOIN ... WHERE o.id IS NULL. NOT IN (SELECT user_id FROM orders) развалится, если user_id в подзапросе бывает NULL. Это я и скажу, если предложат NOT IN.
Как решать вслух за 15 минут
- Пересказать задачу своими словами и спросить зерно: «без заказов — ноль или не показывать?»
- Назвать таблицы и зерно строки результата.
- Набросать FROM/JOIN, потом WHERE, потом агрегат или окно.
- Проговорить риск удвоения и NULL.
- Если успели — индекс под этот паттерн доступа.
Пишите читабельно: один JOIN на строку, не запятые в FROM как в 1998. Интервьюер читает ход мысли. Живой прогон объяснения — в подготовке к интервью.
Воронка и повторные покупки — скетч для аналитика
Если слот аналитический, почти наверняка дадут «конверсия» или «вернувшиеся». Не начинайте с красивого дашборда. Сначала зерно: что считается заказом, какой статус «успех», какое окно возврата.
Доля пользователей с хотя бы одной оплатой за 90 дней:
SELECT COUNT(*) FILTER (WHERE paid_users.user_id IS NOT NULL)::numeric / COUNT(*) AS cr FROM users u LEFT JOIN ( SELECT DISTINCT user_id FROM orders WHERE status = 'paid' AND created_at >= CURRENT_DATE - INTERVAL '90 days' ) paid_users ON paid_users.user_id = u.id WHERE u.registered_at < CURRENT_DATE - INTERVAL '90 days';
Повторная покупка: у кого COUNT(paid) >= 2 за окно. Не среднее по больнице без сегмента новичок/старый. Окно и статус проговариваю до того, как писать. FILTER / DISTINCT в подзапросе — чтобы не раздуть джойном позиций. Если диалект без FILTER — CASE WHEN внутри COUNT.
Ограничение: без витрины это тяжёлый запрос на сырых orders. На интервью скажу, что в проде вынес бы агрегат в ежедневную таблицу, а здесь показываю логику. Не обещаю «ClickHouse за пять минут», если в вакансии Postgres.
Backend-слоту этот отчёт могут не дать — тогда вернитесь к списку по user_id и к «последнему статусу». Не тащите воронку в QA-экран без нужды: вас услышат как человека не на ту роль.
Неделя практики без «100 задач с литкода SQL»
- Схема из 4 таблиц: users, orders, items, status_history. Свои 20 строк.
- Три запроса: LEFT+SUM, HAVING, последняя запись окном.
- EXPLAIN на список по user_id. Сменить индекс, посмотреть план.
- Вслух один запрос без редактора, потом сверить.
Свой домен сильнее случайных «сотрудник-отдел» из учебника, если вы бьётесь в заказы или тикеты. Для аналитического портфолио тот же принцип, что в резюме: один разбор с выводом, не галерея дашбордов.
На экране часто дают «исправьте запрос», не «напишите с нуля». Типичные дыры: JOIN без ключа, фильтр на NULL через =, GROUP BY без всех неагрегированных полей в строгом режиме, окно без ORDER BY, LIMIT без ORDER BY. Читайте чужой SQL вслух: что зерно строки, где удвоение. Это тот же навык, что ревью. Не переписывайте всё сразу — сначала назовите баг.
Плохо: SELECT u.email, o.amount FROM users u, orders o WHERE u.id = o.user_id AND o.created_at > '2026-01-01'; — запятая в FROM, нет статуса, дата в строке без таймзоны. Лучше явный JOIN, status = 'paid', timestamptz, и вопрос: «нужны все заказы или только оплаченные». Если в черновике SELECT * — перед сдачей сузьте поля. Интервьюер смотрит, умеете ли вы остановиться, не только умеете ли вы джойнить.
Частые вопросы
Можно ли гуглить синтаксис на экране?
Спрашивайте правила. Часто можно уточнить DISTINCT ON vs окно. Нельзя — молча открыть готовую шпаргалку на чужую задачу. Честный «в Postgres я бы взял DISTINCT ON, напомню синтаксис» лучше выдуманного Oracle.
Нужно ли учить NoSQL к SQL-раунду?
Нет, пока его нет в вакансии. Один абзац «когда документ, когда связь» — максимум.
Что, если я backend и слабо в окнах?
Закройте JOIN, GROUP BY, индекс, транзакцию. Окно — «последняя запись», один паттерн. Этого хватает на многие backend-слоты. На аналитика — окна обязательны.
Спросят ли про транзакции?
На backend да: isolation одной фразой, не список аббревиатур без сценария двух писателей. На QA — редко глубже «что будет, если два заказа». На аналитика — чаще read-only отчёт.
Большое тестовое «постройте DWH за выходные»?
Это объём работы. Отказ от тестового, если часов больше лимита.
Помогает ли сопроводительное?
Короткое: какой отчёт уже строили, ссылка. Сопроводительное, шаблоны — короткое письмо.
Что сделать сейчас
Нарисуйте четыре таблицы своей предметки. Напишите три запроса из этого текста своими именами полей. Проговорите LEFT vs WHERE вслух. Затем смотрите роли в каталоге SQL и готовьте те паттерны, которые в объявлении: отчёт, список API или проверка QA — не все сразу.
Накануне экрана достаточно трёх листков: LEFT+SUM без потери пользователей, «последняя запись» окном, список по user_id с гипотезой индекса. Скажите каждый вслух, не глядя в редактор. Если завтра аналитика — добавьте воронку и определение «успеха». Если QA — хватит SELECT и JOIN на проверку UI. Не тащите чужой «топ-100 SQL вопросов»: интервьюер всё равно даст свою схему.