SQL для аналитики
Аналитический SQL — это не CRUD. Транзакционная база оптимизирована под запись отдельных строк; аналитик почти всегда делает обратное: читает миллионы строк, агрегирует, ранжирует и считает величины поверх целых наборов данных. Из этого вырастают три навыка, которые проверяют на собеседовании чаще всего, и все три собраны в этой теме.
Первый — оконные функции: способ посчитать что-то поверх группы строк, не схлопывая их в одну (нарастающий итог, скользящее среднее, ранг внутри категории, разница со вчерашним днём). Второй — аналитические паттерны: повторяющиеся формы запросов, которые аналитик пишет снова и снова (gaps-and-islands, сессии, воронки, retention, когорты, дедупликация). Третий — оптимизация: как заставить всё это работать над таблицей в 300M строк, читая план EXPLAIN, а не угадывая.
Ключевые слова SQL (OVER, PARTITION BY, GROUP BY, EXPLAIN) везде пишутся заглавными английскими — это стандарт и в русской речи.
Карта темы
- Оконные функции —
OVER,PARTITION BY,ORDER BY;ROW_NUMBERпротивRANKпротивDENSE_RANK;LAG/LEAD; нарастающие итоги и скользящие средние; рамкиROWSпротивRANGE; top-N в группе и чем окно отличается отGROUP BY. - Аналитические паттерны — gaps-and-islands, сессионизация, воронки, retention и когорты, дедупликация, self-join, pivot/unpivot, генерация ряда дат и форма запроса поверх лога событий.
- Оптимизация SQL — индексы (когда помогают и когда мешают), чтение плана
EXPLAIN, отказ от полного сканирования, стратегии join, почемуSELECT *— запах, партиционирование, CTE против подзапросов против temp-таблиц и роль кардинальности для оптимизатора.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Считать, что PARTITION BY схлопывает строки как GROUP BY | Ожидание одной строки на группу там, где окно сохраняет все строки |
Фильтровать оконную функцию прямо в WHERE | Ошибка синтаксиса; фильтр по результату окна требует обёртки в подзапрос/CTE |
Не различать ROWS и RANGE в рамке | Нарастающий итог «прыгает» на одинаковых датах или считает не те строки |
Писать DATE(created_at) = '...' поверх индекса | Функция над колонкой делает предикат non-sargable, индекс не используется |
SELECT * в колоночном хранилище | Читаются все колонки с диска вместо нужных — дорого, хотя скан одной колонки дёшев |
Гасить дубликаты join'а через SELECT DISTINCT | Скрывается настоящая причина fan-out, а сортировка/дедуп дорого стоит |
Значение для собеседований
SQL спрашивают как проверку модели мышления, а не синтаксиса. Кандидат, который говорит «оконная функция считает поверх набора строк, но возвращает каждую строку, поэтому её нельзя положить в WHERE — нужен внешний запрос», сразу опережает того, кто заучил список функций. Задачи почти всегда сводятся к одному из паттернов темы: посчитать retention, разбить поток на сессии, взять top-N в категории, найти самый длинный streak.
Что обычно проверяют:
- Разницу между
PARTITION BYиGROUP BYи почему окно сохраняет строки. - Как
ROW_NUMBER,RANK,DENSE_RANKведут себя на ничьих. - Как собрать нарастающий итог и скользящее среднее и где
ROWSрасходится сRANGE. - Как прочитать
EXPLAIN, сделать предикат sargable и выбрать broadcast- или shuffle-join.
Типичный неверный ответ: «оконная функция — это то же самое, что GROUP BY, просто по-другому написано». На деле GROUP BY схлопывает группу в одну строку, а окно вычисляет значение для каждой строки, оставляя их все, — это и есть причина, по которой на нём считают ранги, разницы и нарастающие итоги.