← Назад к вопросам

Объясните, что такое оконные функции в SQL. Приведите примеры использования ROW_NUMBER, RANK и LAG.

1.8 Middle🔥 121 комментариев
#SQL и базы данных

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Оконные функции в SQL (Window Functions)

Определение

Оконные функции — это мощный инструмент SQL, который позволяет вычислять значения для каждой строки в наборе ("окне"), не сжимая результаты как GROUP BY. Функция видит не только текущую строку, но и соседние строки в пределах окна.

Отличие от GROUP BY

-- GROUP BY: сжимает данные, даёт одну строку на группу
SELECT 
    category,
    COUNT(*) as count
FROM sales
GROUP BY category;
-- Результат: одна строка на категорию

-- Оконная функция: каждой строке добавляет вычисленное значение
SELECT 
    *,
    COUNT(*) OVER (PARTITION BY category) as count_in_category
FROM sales;
-- Результат: ВСЕ строки + новый столбец с подсчётом

Синтаксис:

FUNCTION() OVER (
    [PARTITION BY column]    -- Разбить на группы
    [ORDER BY column]         -- Упорядочить внутри группы
    [ROWS BETWEEN ... AND ...] -- Размер окна
)

ROW_NUMBER() — Порядковый номер

Присваивает уникальный номер каждой строке в пределах окна, начиная с 1.

Пример: Рейтинг пользователей по выручке

SELECT 
    user_id,
    revenue,
    ROW_NUMBER() OVER (ORDER BY revenue DESC) as rank_all,
    ROW_NUMBER() OVER (PARTITION BY country ORDER BY revenue DESC) as rank_in_country
FROM users
ORDER BY revenue DESC;

Результат:

user_id | revenue | rank_all | rank_in_country
--------|---------|----------|----------------
U1      | 5000    | 1        | 1 (первый в своей стране)
U2      | 4500    | 2        | 1 (первый в своей стране)
U3      | 4000    | 3        | 2 (второй в своей стране)
U4      | 3500    | 4        | 3

Практическое применение:

  • Выбрать ТОП-N пользователей в каждой когорте
  • Нумеровать события по временному порядку

RANK() и DENSE_RANK() — Ранжирование с учётом связей

RANK() — пропускает номера после связей DENSE_RANK() — не пропускает номера

SELECT 
    user_id,
    score,
    RANK() OVER (ORDER BY score DESC) as rank,
    DENSE_RANK() OVER (ORDER BY score DESC) as dense_rank
FROM leaderboard;

Результат:

user_id | score | rank | dense_rank
--------|-------|------|----------
U1      | 100   | 1    | 1
U2      | 100   | 1    | 1    <- две 1-е позиции
U3      | 90    | 3    | 2    <- RANK пропустил 2!
U4      | 85    | 4    | 3

RANK(): Если два пользователя на первом месте, следующий на 3-м DENSE_RANK(): Если два на первом, следующий на 2-м

Когда использовать:

  • RANK() для олимпиад и спорта (где позиции прыгают)
  • DENSE_RANK() для бизнес-рейтингов (последовательные номера)

LAG() и LEAD() — Смещение на соседние строки

LAG() — значение предыдущей строки LEAD() — значение следующей строки

SELECT 
    user_id,
    DATE(created_at) as date,
    revenue,
    LAG(revenue) OVER (PARTITION BY user_id ORDER BY created_at) as prev_day_revenue,
    LEAD(revenue) OVER (PARTITION BY user_id ORDER BY created_at) as next_day_revenue,
    revenue - LAG(revenue) OVER (PARTITION BY user_id ORDER BY created_at) as daily_change
FROM daily_stats
ORDER BY user_id, date;

Результат:

user_id | date       | revenue | prev_day | next_day | change
--------|------------|---------|----------|----------|-------
U1      | 2024-01-01 | 100     | NULL     | 120      | NULL (нет предыдущего)
U1      | 2024-01-02 | 120     | 100      | 110      | 20
U1      | 2024-01-03 | 110     | 120      | NULL     | -10

Параметры LAG:

LAG(column, offset, default) OVER (PARTITION BY ... ORDER BY ...)

-- offset: на сколько строк сместиться (по умолчанию 1)
-- default: значение если строка не существует (NULL по умолчанию)

Примеры:
LAG(revenue, 1) -- одна строка назад
LAG(revenue, 7) -- неделю назад (если данные ежедневные)
LAG(revenue, 1, 0) -- вместо NULL будет 0

Практические примеры

Пример 1: День-в-день рост выручки

SELECT 
    DATE(transaction_date) as date,
    SUM(amount) as daily_revenue,
    LAG(SUM(amount)) OVER (ORDER BY DATE(transaction_date)) as prev_day_revenue,
    SUM(amount) - LAG(SUM(amount)) OVER (ORDER BY DATE(transaction_date)) as daily_change,
    ROUND(100.0 * (SUM(amount) - LAG(SUM(amount)) OVER (ORDER BY DATE(transaction_date))) 
          / LAG(SUM(amount)) OVER (ORDER BY DATE(transaction_date)), 2) as pct_change
FROM transactions
GROUP BY DATE(transaction_date)
ORDER BY date DESC;

Результат:

date       | daily_revenue | prev_day | change | pct_change
-----------|---------------|----------|--------|----------
2024-01-10 | 10000         | 9500     | 500    | 5.26%
2024-01-09 | 9500          | 9000     | 500    | 5.56%
2024-01-08 | 9000          | NULL     | NULL   | NULL

Пример 2: Последовательность действий пользователя

SELECT 
    user_id,
    event_date,
    event_type,
    LAG(event_type) OVER (PARTITION BY user_id ORDER BY event_date) as prev_event,
    LEAD(event_type) OVER (PARTITION BY user_id ORDER BY event_date) as next_event
FROM events
WHERE user_id = 'U123'
ORDER BY event_date;

Помогает анализировать фанели: какие события идут одно за другим?

Пример 3: Когортный анализ с ROW_NUMBER

-- Выбрать ТОП-100 пользователей по доходу в каждой стране
WITH ranked_users AS (
    SELECT 
        user_id,
        country,
        revenue,
        ROW_NUMBER() OVER (PARTITION BY country ORDER BY revenue DESC) as rank
    FROM users
)
SELECT *
FROM ranked_users
WHERE rank <= 100;

Пример 4: Бегущие итоги (Cumulative Sum)

SELECT 
    DATE(transaction_date) as date,
    amount,
    SUM(amount) OVER (ORDER BY transaction_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as cumulative_total
FROM transactions
ORDER BY date;

Результат:

date       | amount | cumulative_total
-----------|--------|------------------
2024-01-01 | 100    | 100 (только эта)
2024-01-02 | 200    | 300 (100 + 200)
2024-01-03 | 150    | 450 (100 + 200 + 150)

ROWS BETWEEN — размер окна

-- UNBOUNDED PRECEDING: от начала раздела до текущей
SUM(amount) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)

-- Последние 7 дней (7-дневное скользящее среднее)
AVG(amount) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)

-- ±1 строка от текущей
AVG(amount) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)

Полезные комбинации

FIRST_VALUE и LAST_VALUE — первое и последнее значение в окне

SELECT 
    user_id,
    event_date,
    event_type,
    FIRST_VALUE(event_type) OVER (PARTITION BY user_id ORDER BY event_date) as first_event,
    LAST_VALUE(event_type) OVER (PARTITION BY user_id ORDER BY event_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as last_event
FROM events;

PERCENT_RANK() — процентиль

SELECT 
    user_id,
    revenue,
    PERCENT_RANK() OVER (ORDER BY revenue) as percentile
FROM users;

-- Результат от 0 (минимум) до 1 (максимум)
-- Пользователь с выручкой на 50-м процентиле в середине

Производительность оконных функций

Оконные функции часто БЫСТРЕЕ, чем JOIN к самому себе:

-- МЕДЛЕННО: самоджойн
SELECT 
    a.user_id,
    a.date,
    a.revenue,
    b.revenue as prev_day_revenue
FROM daily_stats a
LEFT JOIN daily_stats b 
    ON a.user_id = b.user_id 
    AND a.date = b.date + INTERVAL 1 DAY;

-- БЫСТРО: оконная функция
SELECT 
    user_id,
    date,
    revenue,
    LAG(revenue) OVER (PARTITION BY user_id ORDER BY date) as prev_day_revenue
FROM daily_stats;

Применение для Product Analyst

Типовые задачи:

  1. Временные ряды:

    • Week-over-week growth
    • YoY сравнения
    • Тренды
  2. Ранжирование:

    • ТОП-10 пользователей по стране
    • Лидерборды
  3. Фанели:

    • Кто совершил покупку после просмотра?
    • Последовательность событий
  4. Когортный анализ:

    • Первое и последнее действие когорты
    • Дни с момента установки

Итог

Оконные функции = SQL + окно данных. Они позволяют:

  • Не сжимать данные (как GROUP BY)
  • Смотреть на соседние строки
  • Вычислять ранги, сумматы, смещения

ROW_NUMBER → номер строки RANK/DENSE_RANK → позиция с учётом связей LAG/LEAD → соседние значения SUM/AVG OVER → бегущие итоги

Это базовые инструменты для любого аналитика!