Данные и хранилища
~40 мин

Продвинутый SQL

Оконные функции, CTE, EXPLAIN ANALYZE, индексы, партиционирование.

Продвинутый SQL — оконные функции, оптимизация и всё, что спрашивают на собесах

🗺️ Где это в общей картине

SQL — язык, на котором ты общаешься с DWH (ClickHouse, BigQuery), продакшн-базами (PostgreSQL), витринами данных. Фичи для ML-модели — это SQL-запросы к DWH. dbt-трансформации — это SQL. Даже PySpark поддерживает SQL. Продвинутый SQL — must have для любого, кто работает с данными.

Базовый SQL (SELECT, JOIN, GROUP BY) — это 30% того, что спрашивают. Остальные 70% — оконные функции, CTE, EXPLAIN ANALYZE и индексы. Оконные функции — топ-1 тема на собеседованиях в дата-команды. Если ты не знаешь ROW_NUMBER() OVER() — будет больно.

Оконные функции — считаем без потери строк

GROUP BY сворачивает строки: было 1000, стало 10 групп. А что если нужно и агрегат посчитать, и все строки оставить? Оконная функция делает именно это — каждая строка «видит» своё окно и считает агрегат, не схлопывая таблицу. Основные функции ранжирования: ROW_NUMBER (всегда уникальный номер), RANK (одинаковые значения → одинаковый ранг, потом пропуск), DENSE_RANK (без пропуска). Для аналитики: LAG/LEAD (предыдущее/следующее значение), SUM/AVG OVER (кумулятивные и скользящие агрегаты).

Классическая задача с каждого второго собеседования — дедупликация через ROW_NUMBER. А CTE (Common Table Expression) делает сложные запросы читаемыми — вместо вложенных SELECT пишешь цепочку WITH ... AS (...).

-- Дедупликация + CTE — оставить последнюю запись по пользователю
WITH ranked AS (
    SELECT *,
        ROW_NUMBER() OVER (
            PARTITION BY user_id ORDER BY created_at DESC
        ) AS rn
    FROM events
)
SELECT * FROM ranked WHERE rn = 1;

-- Кумулятивная сумма и day-over-day изменения
SELECT date, revenue,
    SUM(revenue) OVER (ORDER BY date) AS cumulative,
    revenue - LAG(revenue) OVER (ORDER BY date) AS day_change
FROM daily_revenue;

🔥 Любимая задача на собесе

Retention по когортам: для каждой когорты (месяц регистрации) посчитать процент вернувшихся через 1, 7, 30 дней. Это проверяет всё сразу: CTE, оконные функции, DATE_TRUNC, COUNT DISTINCT. Решение: CTE cohorts (определяем когорту), CTE activity (все активности с месяцами от регистрации), GROUP BY + FIRST_VALUE для расчёта retention в процентах.

EXPLAIN ANALYZE и индексы — почему запрос тормозит

Запрос работает 30 секунд вместо 0.1. Почему? EXPLAIN ANALYZE покажет план: Index Scan (индекс работает, быстро) или Seq Scan (перебор всей таблицы, медленно). Каждый индекс замедляет INSERT/UPDATE, поэтому создавай их только на столбцы из WHERE и JOIN ON.

-- B-tree (по умолчанию) — для =, <, >, BETWEEN
CREATE INDEX idx_orders_user ON orders(user_id);

-- Составной — порядок столбцов важен!
-- Работает для WHERE user_id = X AND created_at > Y
-- НЕ работает для WHERE created_at > Y (без user_id)
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);

-- Частичный — только подмножество строк (меньше, быстрее)
CREATE INDEX idx_active ON orders(user_id) WHERE status = 'active';

Партиционирование — делим таблицу на части

Таблица events на 500 млн строк — даже с индексом запросы медленные. Партиционирование разбивает таблицу на физические части (обычно по дате). Запрос WHERE created_at > '2024-01-01' просканирует только нужные партиции. В PostgreSQL: CREATE TABLE ... PARTITION BY RANGE (created_at), затем создаёшь партиции по месяцам. В ClickHouse и BigQuery партиционирование встроено и работает автоматически.

-- Партиционирование по дате (PostgreSQL)
CREATE TABLE events (
    id BIGSERIAL, user_id INT,
    event_type TEXT, created_at TIMESTAMPTZ
) PARTITION BY RANGE (created_at);

CREATE TABLE events_2024_01 PARTITION OF events
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE events_2024_02 PARTITION OF events
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
-- Запрос автоматически сканирует ТОЛЬКО нужные партиции

🎯 На собесе

Топ-5 задач: 1) дедупликация через ROW_NUMBER, 2) retention по когортам (CTE + оконные функции), 3) кумулятивная сумма / скользящее среднее, 4) anti-join через LEFT JOIN + IS NULL, 5) ранжирование с RANK/DENSE_RANK. Плюс вопросы: зачем EXPLAIN ANALYZE, какие бывают индексы, когда партиционировать.

Транзакции — всё или ничего

Транзакция — группа SQL-команд, которая выполняется целиком или не выполняется вообще. ACID: Atomicity (атомарность — всё или ничего), Consistency (согласованность), Isolation (изоляция — параллельные транзакции не мешают), Durability (сохранность — данные не потеряются после commit).

BEGIN;
UPDATE features SET value = 0.95 WHERE user_id = 123 AND feature = 'score';
INSERT INTO predictions (user_id, model_version, result) VALUES (123, 'v2.1', 0.87);
COMMIT;
-- Если что-то сломалось → ROLLBACK; — обе операции отменятся

🎯 Для ML

Обновляешь фичи в нескольких таблицах перед инференсом? Оберни в транзакцию. Иначе модель может прочитать старые фичи из одной таблицы и новые из другой — несогласованные данные.

Блокировки — когда два процесса хотят одно и то же

Когда два процесса одновременно обновляют одну строку — БД ставит блокировку. Shared lock (FOR SHARE) — можно читать параллельно. Exclusive lock (FOR UPDATE) — только один пишет. Deadlock — два процесса ждут друг друга, БД убивает одного.

⚠️ Как избежать deadlock

Всегда блокируй таблицы/строки в одном и том же порядке. Если процесс A блокирует users → orders, процесс B тоже должен users → orders, а не наоборот.