Чужой SQL-запрос на 120 строк с четырьмя CTE и тремя вложенными подзапросами — это то, с чем аналитик сталкивается в первую же неделю на новой работе. Кто-то уволился, остался дашборд, и надо понять, что этот запрос считает и почему цифра разошлась с реальностью. Разберу метод, которым пользуюсь сам: он превращает «стену текста» в понятную последовательность шагов.
С чего начать разбор незнакомого запроса?
Не читайте сверху вниз. SQL пишется не в том порядке, в котором выполняется. Глаз цепляется за первый SELECT, а движок начинает с FROM и JOIN — то есть с того, откуда берутся данные и как они склеиваются.
Мой первый шаг всегда одинаковый: найти самый внешний FROM и выписать, какие таблицы участвуют. Не что считается, а что является источником. Дальше смотрю на GROUP BY — он говорит, какая гранулярность у результата. Одна строка на пользователя? На пользователя и день? На заказ? Гранулярность результата — половина понимания запроса, потому что все агрегаты (SUM, COUNT, AVG) считаются именно внутри этих групп.
Только после этого читаю SELECT — теперь понятно, что за колонки в нём и на каком уровне они агрегированы.
Как раскрутить вложенные подзапросы и CTE?
Вложенность читается изнутри наружу, как матрёшка. Самый глубокий подзапрос выполняется первым, его результат становится «виртуальной таблицей» для следующего уровня.
С CTE проще: блоки WITH name AS (...) — это именованные шаги, и читать их надо строго по порядку сверху вниз, будто это переменные в скрипте. Хороший приём — мысленно (или в комментарии) подписать каждый CTE одной фразой: «активные пользователи за месяц», «первый заказ каждого клиента», «выручка по дням». Когда у всех пяти CTE есть человеческие имена-смыслы, финальный SELECT, который их соединяет, читается за минуту.
Если запрос написан через вложенные подзапросы, а не CTE, я часто первым делом переписываю его на CTE — механически, без изменения логики. Читаемость вырастает в разы, и по дороге замечаешь, что два подзапроса делают почти одно и то же.
Что такое фан-аут и как его заметить?
Фан-аут (row multiplication) — самая частая скрытая ошибка в чужих запросах. Возникает, когда JOIN соединяет таблицы по ключу, который в правой таблице не уникален. Одна строка слева умножается на количество совпадений справа, и SUM внезапно завышается в разы.
Классический пример: джойним orders с order_items, а потом суммируем orders.total. У заказа три позиции — сумма заказа посчитается трижды.
Как замечаю: смотрю на каждый JOIN и спрашиваю, уникален ли ключ соединения в той таблице, к которой присоединяемся. Если сомневаюсь — быстрый чек:
SELECT order_id, COUNT(*)
FROM order_items
GROUP BY order_id
HAVING COUNT(*) > 1;
Если строки есть — ключ не уникален, и любой SUM выше по запросу под подозрением. Про механику соединений подробно разбирал в гайде по JOIN, а сам приём с HAVING COUNT(*) > 1 — базовый инструмент поиска дублей.
Как проверить, что запрос считает именно то, что кажется?
Понять запрос глазами — это гипотеза. Проверка — это данные. Я почти всегда декомпозирую: беру самый внутренний CTE или подзапрос, запускаю его отдельно и смотрю на 10 строк результата. Совпадает ли с моим представлением? Потом добавляю следующий слой и снова смотрю. Так ошибка локализуется на конкретном шаге, а не «где-то в 120 строках».
Второй приём — проверка на одном известном объекте. Беру конкретного пользователя или заказ, про которого точно знаю ответ (посчитал руками или из другого источника), и прогоняю запрос с фильтром только на него. Если запрос выдаёт то же число — доверия больше. Если нет — вот он, баг.
Отрабатывать это удобно на живых данных: в SQL-тренажёре можно запускать запросы по шагам и сразу видеть результат каждого CTE, а не гадать по тексту.
Какие типичные ошибки прячутся в чужом SQL?
Помимо фан-аута, вот что встречаю чаще всего:
WHEREвместо условия вLEFT JOIN. Условие на правую таблицу, вынесенное вWHERE, тихо превращаетLEFT JOINвINNER JOIN— строки без совпадений отфильтровываются, хотя автор хотел их сохранить.- **
COUNT(column)вместоCOUNT(*).COUNTпо колонке не считаетNULL. Если в колонке есть пропуски, число будет меньше ожидаемого, и это легко пропустить. NOT INс подзапросом, где есть NULL. Если подзапрос вернёт хоть одинNULL, весьNOT INвернёт пусто из-за трёхзначной логики. Коварно, потому что «вчера работало».- Неявное усечение даты.** Сравнение
created_at >= '2026-01-01'приcreated_atтипа timestamp может отрезать или захватить лишний день в зависимости от таймзоны.
Все четыре ошибки роднит одно: запрос выполняется без ошибки и возвращает правдоподобное число — просто неправильное. Поэтому чтение чужого SQL — это не только «понять синтаксис», но и «проверить логику на данных».
Хотите закрепить навык — на собеседовании аналитика чтение и отладка чужого запроса встречается едва ли не чаще, чем написание своего с нуля. А если хочется системно разложить SQL по полочкам, есть бесплатный курс с нуля — от SELECT до оконных функций.