Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 09.04 · 30 мин
Продвинутый
Window FunctionsROW_NUMBERRANKLAGLEADOVERROWSRANGENew Analyzer

Оконные функции

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

В ClickHouse оконные функции полноценно работают с New Analyzer (enable_analyzer=1, включён по умолчанию начиная с версии 24.3). На ClickHouse 26.3 LTS New Analyzer активен, дополнительных настроек не требуется.


Три категории оконных функций

Категории оконных функций
РанжированиеРанжирование: ROW_NUMBER() -- уникальный номер строки в окне. RANK() -- ранг с пропусками при одинаковых значениях. DENSE_RANK() -- ранг без пропусков. NTILE(n) -- деление на n равных групп.
СмещениеСмещение: LAG(col, offset, default) -- значение из предыдущей строки. LEAD(col, offset, default) -- значение из следующей строки. FIRST_VALUE(col) -- первое значение в окне. LAST_VALUE(col) -- последнее. NTH_VALUE(col, n) -- n-ное значение.
Агрегатные с OVERАгрегатные с OVER: SUM(col) OVER (...) -- сумма в окне. AVG, COUNT, MIN, MAX -- любой агрегат с OVER. Для нарастающего итога: SUM(x) OVER (ORDER BY ts ROWS UNBOUNDED PRECEDING). Для скользящего среднего: AVG(x) OVER (ORDER BY ts ROWS BETWEEN 6 PRECEDING AND CURRENT ROW).

Ранжирование: ROW_NUMBER, RANK, DENSE_RANK, NTILE

-- ROW_NUMBER: уникальный номер строки
SELECT
    user_id,
    score,
    ROW_NUMBER() OVER (ORDER BY score DESC) AS position
FROM leaderboard;
user_idscoreposition
429501
179002
339003
88504
-- RANK vs 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_idscorerankdense_rank
4295011
1790022
3390022
885043

RANK пропускает позицию после одинаковых значений (2, 2, 4). DENSE_RANK не пропускает (2, 2, 3).

-- NTILE: деление на группы
SELECT
    user_id,
    revenue,
    NTILE(4) OVER (ORDER BY revenue DESC) AS quartile
FROM customers;
-- quartile: 1 = top 25%, 2 = 25-50%, 3 = 50-75%, 4 = bottom 25%

Смещение: LAG, LEAD, FIRST_VALUE, LAST_VALUE

-- LAG: значение из предыдущей строки
SELECT
    event_date,
    revenue,
    LAG(revenue, 1, 0) OVER (ORDER BY event_date) AS prev_revenue,
    revenue - LAG(revenue, 1, 0) OVER (ORDER BY event_date) AS daily_change
FROM daily_stats;
event_daterevenueprev_revenuedaily_change
2025-01-01100001000
2025-01-0212001000200
2025-01-0311001200-100
-- LEAD: значение из следующей строки
SELECT
    event_date,
    revenue,
    LEAD(revenue, 1) OVER (ORDER BY event_date) AS next_revenue
FROM daily_stats;
-- next_revenue для последней строки = NULL (нет следующей)
-- FIRST_VALUE / LAST_VALUE: первое и последнее значение в окне
SELECT
    metric,
    ts,
    value,
    FIRST_VALUE(value) OVER (PARTITION BY metric ORDER BY ts) AS initial_value,
    value - FIRST_VALUE(value) OVER (PARTITION BY metric ORDER BY ts) AS change_from_start
FROM metrics;
-- NTH_VALUE: n-ное значение
SELECT
    user_id,
    score,
    NTH_VALUE(score, 3) OVER (ORDER BY score DESC) AS third_best
FROM leaderboard;

Агрегатные функции с OVER

Любая агрегатная функция может работать как оконная с OVER:

-- Нарастающий итог (running total)
SELECT
    event_date,
    revenue,
    SUM(revenue) OVER (ORDER BY event_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_total
FROM daily_stats;
event_daterevenuerunning_total
2025-01-0110001000
2025-01-0212002200
2025-01-0311003300
-- Скользящее среднее за 7 дней
SELECT
    event_date,
    revenue,
    AVG(revenue) OVER (ORDER BY event_date
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS moving_avg_7d
FROM daily_stats;
-- PARTITION BY: отдельное окно для каждого раздела
SELECT
    metric_name,
    ts,
    value,
    SUM(value) OVER (PARTITION BY metric_name ORDER BY ts) AS metric_running_total,
    AVG(value) OVER (PARTITION BY metric_name ORDER BY ts
        ROWS BETWEEN 4 PRECEDING AND CURRENT ROW
    ) AS metric_moving_avg_5
FROM metrics;

Фреймы: ROWS vs RANGE

Фрейм определяет, какие строки относительно текущей входят в окно:

-- ROWS: фиксированное число строк
SUM(x) OVER (ORDER BY ts ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
-- Текущая строка + 2 предыдущие (всегда ровно 3 строки)

-- RANGE: по значению
SUM(x) OVER (ORDER BY ts RANGE BETWEEN 3600 PRECEDING AND CURRENT ROW)
-- Все строки, где ts >= текущий_ts - 3600 секунд
ФреймГраницы по…Количество строк
ROWSПозиции строкФиксированное
RANGEЗначениям ORDER BYПеременное
WARNING

RANGE с DateTime в ClickHouse требует числового интервала в секундах, а не INTERVAL-выражения. Вместо RANGE BETWEEN INTERVAL 1 HOUR PRECEDING используйте RANGE BETWEEN 3600 PRECEDING AND CURRENT ROW. Это связано с тем, что DateTime хранится как UInt32 (секунды с epoch).


ClickHouse-специфичные ограничения

  1. Нет GROUPS frame. Стандартный SQL определяет три типа фреймов: ROWS, RANGE, GROUPS. ClickHouse не поддерживает GROUPS — только ROWS и RANGE.

  2. Ограниченная поддержка RANGE. RANGE работает с числовыми типами и DateTime (как UInt32). Не работает с String, Array и другими нечисловыми типами.

  3. RANGE с DateTime. Интервал задаётся в секундах (числом), не через INTERVAL:

-- Правильно: RANGE с числовым интервалом (3600 секунд = 1 час)
SELECT ts, value,
    AVG(value) OVER (ORDER BY ts RANGE BETWEEN 3600 PRECEDING AND CURRENT ROW)
FROM sensors;

-- Ошибка: INTERVAL не поддерживается в RANGE frame
-- RANGE BETWEEN INTERVAL 1 HOUR PRECEDING AND CURRENT ROW  -- НЕ работает
  1. New Analyzer обязателен. Оконные функции полноценно работают только с New Analyzer (enable_analyzer=1). В ClickHouse 26.3 LTS New Analyzer включён по умолчанию. Если по какой-то причине он отключён:
SET enable_analyzer = 1;  -- включить New Analyzer для текущей сессии

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

Пагинация через ROW_NUMBER

-- Получить строки 21-30 (страница 3 по 10 элементов)
SELECT * FROM (
    SELECT
        *,
        ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn
    FROM orders
)
WHERE rn BETWEEN 21 AND 30;

Процент от общего

SELECT
    category,
    revenue,
    revenue * 100.0 / SUM(revenue) OVER () AS pct_of_total
FROM category_stats;

Top-N в каждой группе

SELECT * FROM (
    SELECT
        region,
        product,
        revenue,
        ROW_NUMBER() OVER (PARTITION BY region ORDER BY revenue DESC) AS rn
    FROM sales
)
WHERE rn <= 3;
-- Топ-3 продукта по выручке в каждом регионе

Межстрочная разница (замена deprecated runningDifference)

-- Вместо deprecated runningDifference(ts):
SELECT
    ts,
    value,
    ts - LAG(ts, 1) OVER (ORDER BY ts) AS time_diff,
    value - LAG(value, 1) OVER (ORDER BY ts) AS value_diff
FROM metrics;
TIP

Оконные функции — современная замена deprecated-функций neighbor(), runningDifference() и runningAccumulate() (deprecated с ClickHouse 24.8). Подробнее о миграции — в уроке 08 этого модуля.


Ключевые выводы

  1. Три категории: ранжирование (ROW_NUMBER, RANK, DENSE_RANK, NTILE), смещение (LAG, LEAD, FIRST_VALUE, LAST_VALUE), агрегатные с OVER.
  2. PARTITION BY делит данные на независимые окна. ORDER BY определяет порядок внутри окна.
  3. ROWS — фиксированное число строк. RANGE — по значениям ORDER BY (числовые/DateTime).
  4. GROUPS фрейм не поддерживается в ClickHouse. RANGE с DateTime требует числового интервала (секунды).
  5. New Analyzer обязателен (enable_analyzer=1, включён по умолчанию с 24.3+).
  6. Оконные функции заменяют deprecated neighbor()/runningDifference()/runningAccumulate().
Frame Clause в SQL: ROWS, RANGE, GROUPS — глубокое погружение LAG и LEAD: доступ к соседям окна

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 10