Урок курса
Оконные рамки ROWS
Продвинутый SQL: сложные запросы и задачи для собеседованийСинтаксис границ оконной рамки ROWS
В базовых запросах с накопительным итогом конструкция ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW включала в расчёт все строки от начала раздела до текущей позиции. Если аналитическая задача требует ограничить расчёт только соседними строками, в секции OVER() задают явные смещения.
Подготовим таблицу замеров температуры на серверах:
CREATE TABLE server_readings (
reading_id INTEGER PRIMARY KEY,
server_id INTEGER NOT NULL,
measured_at TEXT NOT NULL,
temp_c REAL
);
INSERT INTO server_readings (reading_id, server_id, measured_at, temp_c) VALUES
(1, 101, '2024-03-01 08:00:00', 40.0),
(2, 101, '2024-03-01 12:00:00', NULL),
(3, 101, '2024-03-05 09:00:00', 50.0),
(4, 101, '2024-03-06 10:00:00', 60.0),
(5, 101, '2024-03-07 11:00:00', 70.0),
(6, 202, '2024-03-01 08:00:00', 25.0),
(7, 202, '2024-03-02 08:00:00', 35.0);
Базовый состав строк при сортировке выглядит так:
| reading_id | server_id | measured_at | temp_c |
|---|---|---|---|
| 1 | 101 | 2024-03-01 08:00:00 | 40.0 |
| 2 | 101 | 2024-03-01 12:00:00 | NULL |
| 3 | 101 | 2024-03-05 09:00:00 | 50.0 |
| 4 | 101 | 2024-03-06 10:00:00 | 60.0 |
| 5 | 101 | 2024-03-07 11:00:00 | 70.0 |
| 6 | 202 | 2024-03-01 08:00:00 | 25.0 |
| 7 | 202 | 2024-03-02 08:00:00 | 35.0 |
Конструкция оконной рамки имеет вид:
ROWS BETWEEN <начало> AND <конец>
Для границ начала и конца допустимы пять выражений:
UNBOUNDED PRECEDING— от самой первой строки раздела;N PRECEDING— наNфизических строк назад от текущей;CURRENT ROW— текущая строка;N FOLLOWING— наNфизических строк вперёд от текущей;UNBOUNDED FOLLOWING— до самой последней строки раздела.
SQLite строго регламентирует допустимый порядок категорий границ: тип границы FOLLOWING не может стоять в позиции начала рамки, если в конце указан PRECEDING или CURRENT ROW. При попытке нарушить порядок типов границ:
SELECT
reading_id,
SUM(temp_c) OVER (
ORDER BY measured_at
ROWS BETWEEN 1 FOLLOWING AND 1 PRECEDING
) AS invalid_sum
FROM server_readings;
СУБД вернёт синтаксическую ошибку (unsupported frame specification).
При этом числовая инверсия внутри одного типа границы синтаксической ошибкой в SQLite не является: выражения вида ROWS BETWEEN 2 PRECEDING AND 5 PRECEDING или ROWS BETWEEN 2 FOLLOWING AND 1 FOLLOWING успешно выполняются, но формируют пустую рамку, для которой функции вроде SUM возвращают NULL.
Порядок строк в окне ORDER BY должен быть строго детерминированным. Если поле времени содержит одинаковые значения, к нему добавляют уникальный идентификатор строки (например, ORDER BY measured_at, reading_id), иначе при пересчёте одинаковые строки могут попадать в рамку в случайном порядке.
Физические строки и календарные интервалы
Рамка ROWS оперирует исключительно количеством физических строк в упорядоченном наборе данных. Она не анализирует календарные даты, метки времени и интервалы между ними.
Рассчитаем сумму температур по серверу 101 с рамкой из двух предшествующих физических строк:
SELECT
reading_id,
measured_at,
temp_c,
SUM(temp_c) OVER (
PARTITION BY server_id
ORDER BY measured_at, reading_id
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS sum_last_3_physical_rows
FROM server_readings
WHERE server_id = 101
ORDER BY measured_at, reading_id;
Результат выполнения:
| reading_id | measured_at | temp_c | sum_last_3_physical_rows |
|---|---|---|---|
| 1 | 2024-03-01 08:00:00 | 40.0 | 40.0 |
| 2 | 2024-03-01 12:00:00 | NULL | 40.0 |
| 3 | 2024-03-05 09:00:00 | 50.0 | 90.0 |
| 4 | 2024-03-06 10:00:00 | 60.0 | 110.0 |
| 5 | 2024-03-07 11:00:00 | 70.0 | 180.0 |
Обратите внимание на строку 3 (2024-03-05 09:00:00). Между ней и предыдущим измерением от 2024-03-01 12:00:00 прошло почти четыре календарных дня. Однако рамка ROWS BETWEEN 2 PRECEDING AND CURRENT ROW требует взять ровно две предшествующие записи из таблицы: строку 1 и строку 2. В сумму 90.0 вошли значения строк 1 (40.0), 2 (NULL) и 3 (50.0).
Отсюда вытекает типичная ошибка: попытка посчитать «недельное скользящее среднее» через ROWS BETWEEN 6 PRECEDING AND CURRENT ROW. Если в таблице зафиксированы не все календарные дни (например, пропущены выходные или были сбои сбора данных), такое окно объединит 7 физических строк. По календарю эти 7 строк могут растянуться на 10, 14 или 30 дней. Рамка ROWS гарантирует число строк в расчёте, но никогда не гарантирует интервал времени.
Усечение рамки на краях раздела и поведение NULL
Когда текущая строка находится близко к началу или концу раздела (PARTITION BY), запрошенное количество предшествующих или последующих строк может физически отсутствовать. В этом случае СУБД не выдаёт ошибку и не заполняет недостающие строки нулями, а автоматически усекает рамку до фактически существующих записей внутри текущего раздела.
Проверим работу симметричного окна ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING, а также поведение функций подсчёта и усреднения:
SELECT
server_id,
reading_id,
measured_at,
temp_c,
COUNT(*) OVER (
PARTITION BY server_id
ORDER BY measured_at, reading_id
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS frame_row_count,
COUNT(temp_c) OVER (
PARTITION BY server_id
ORDER BY measured_at, reading_id
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS frame_val_count,
AVG(temp_c) OVER (
PARTITION BY server_id
ORDER BY measured_at, reading_id
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS moving_avg_3
FROM server_readings
ORDER BY server_id, measured_at, reading_id;
Результат выполнения:
| server_id | reading_id | measured_at | temp_c | frame_row_count | frame_val_count | moving_avg_3 |
|---|---|---|---|---|---|---|
| 101 | 1 | 2024-03-01 08:00:00 | 40.0 | 2 | 1 | 40.0 |
| 101 | 2 | 2024-03-01 12:00:00 | NULL | 3 | 2 | 45.0 |
| 101 | 3 | 2024-03-05 09:00:00 | 50.0 | 3 | 2 | 55.0 |
| 101 | 4 | 2024-03-06 10:00:00 | 60.0 | 3 | 3 | 60.0 |
| 101 | 5 | 2024-03-07 11:00:00 | 70.0 | 2 | 2 | 65.0 |
| 202 | 6 | 2024-03-01 08:00:00 | 25.0 | 2 | 2 | 30.0 |
| 202 | 7 | 2024-03-02 08:00:00 | 35.0 | 2 | 2 | 30.0 |
Механизм усечения проявляется на границах разделов:
- Для строки 1 предшествующей строки в разделе
server_id = 101нет. Рамка усекается слева и содержит только строки 1 и 2 (frame_row_count = 2). - Для строки 5 последующей строки в разделе нет. Рамка усекается справа и содержит только строки 4 и 5 (
frame_row_count = 2). - Переход к серверу 202 изолирует данные: строки сервера 101 не проникают в окно сервера 202. Для строки 6 рамка усечена слева (строки 6 и 7), для строки 7 — усечена справа (строки 6 и 7).
Наличие NULL в столбце temp_c разделяет поведение COUNT(*) и агрегатных функций по столбцу:
COUNT(*)считает все физические строки рамки. Для строки 2 в рамку попали строки 1, 2 и 3, поэтомуframe_row_countравен3.COUNT(temp_c)считает только строки с непустыми значениями. Во второй строке значениеNULL, поэтомуframe_val_countравен2.AVG(temp_c)игнорируетNULL. Для строки 2 среднее вычисляется по значениям строк 1 (40.0) и 3 (50.0):(40.0 + 50.0) / 2 = 45.0. Значение делится именно на число непустых элементов (2), а не на общее количество строк в рамке (3). Если бы все значения в рамке оказалисьNULL, функцииSUMиAVGвернули быNULL, аCOUNT(temp_c)вернул бы0.
Шаблоны скользящих показателей и остатка до конца раздела
На практике смещения рамки ROWS используют для трёх основных задач: расчёта предшествующего скользящего окна, сглаживания по окрестности и вычисления остатка до конца партиции.
Шаблон скользящего среднего по текущей и двум предшествующим физическим строкам:
SELECT
server_id,
measured_at,
temp_c,
ROUND(AVG(temp_c) OVER (
PARTITION BY server_id
ORDER BY measured_at ASC, reading_id ASC
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
), 2) AS moving_avg_temp
FROM server_readings;
Добавление reading_id ASC после даты гарантирует детерминированность: если за одну секунду поступят два замера, они выстроятся в строгом порядке первичного ключа.
Второй шаблон — симметричное скользящее окно для сглаживания метрики по соседям слева и справа:
AVG(temp_c) OVER (
PARTITION BY server_id
ORDER BY measured_at ASC, reading_id ASC
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
)
Третий шаблон — агрегация от текущей строки до конца раздела с помощью границы UNBOUNDED FOLLOWING. Она применяется для расчёта остатка метрики:
SELECT
server_id,
reading_id,
measured_at,
temp_c,
SUM(temp_c) OVER (
PARTITION BY server_id
ORDER BY measured_at, reading_id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
) AS remaining_sum
FROM server_readings
ORDER BY server_id, measured_at, reading_id;
Результат запроса остатка:
| server_id | reading_id | measured_at | temp_c | remaining_sum |
|---|---|---|---|---|
| 101 | 1 | 2024-03-01 08:00:00 | 40.0 | 220.0 |
| 101 | 2 | 2024-03-01 12:00:00 | NULL | 180.0 |
| 101 | 3 | 2024-03-05 09:00:00 | 50.0 | 180.0 |
| 101 | 4 | 2024-03-06 10:00:00 | 60.0 | 130.0 |
| 101 | 5 | 2024-03-07 11:00:00 | 70.0 | 70.0 |
| 202 | 6 | 2024-03-01 08:00:00 | 25.0 | 60.0 |
| 202 | 7 | 2024-03-02 08:00:00 | 35.0 | 35.0 |
В первой строке раздела server_id = 101 рамка охватывает строки с 1 по 5. Значения 40.0 + 50.0 + 60.0 + 70.0 дают общую сумму раздела 220.0. По мере движения вниз предшествующие строки исключаются из рамки, пока на последней строке раздела (строка 5) в окне не останется только текущее значение 70.0.
Задача 1
Дана таблица ежедневных заказов: 1 марта (100 руб.), 2 марта (200 руб.), 10 марта (300 руб.), 11 марта (400 руб.). В окне вычисляется выражение: SUM(amount) OVER (ORDER BY order_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW). Какое значение вернёт запрос для строки за 10 марта?
Задача 2
В таблице 5 строк одного раздела со значениями amount: 10, NULL, 30, 40, 50. Вычисляется колонка: AVG(amount) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING). Какое среднее значение будет рассчитано для третьей строки (где amount = 30)?
Задача 3
В разделе пять строк с номерами 1–5. Для первой строки сопоставьте рамки и номера вошедших строк: A — ROWS BETWEEN 2 PRECEDING AND CURRENT ROW; B — ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING; C — ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING. Выберите верное сопоставление.
Задача 4
Скользящая температура
Что уже дано
Таблица metrics(metric_id INTEGER PRIMARY KEY, server_id INTEGER NOT NULL, measured_at TEXT NOT NULL, temp_c REAL NOT NULL). До 500 записей. ID положительные; время UTC формата YYYY-MM-DD HH:MM:SS в 2024 году; температура — целое значение от −100 до 100, сохранённое как REAL. Время может повторяться.
Что нужно сделать
Для каждого замера найдите среднюю температуру по текущему и двум предыдущим замерам того же сервера в порядке measured_at, metric_id. В начале раздела используйте все доступные записи. Округлите среднее до двух знаков.
Ввод и вывод
Отправьте один SELECT или WITH-запрос; stdin и печать не используются. Ровно 4 столбцов в указанном порядке и с этими именами: server_id, measured_at, temp_c, moving_avg_temp. server_id — INTEGER, measured_at — TEXT, temp_c и moving_avg_temp — REAL; NULL нет. Сортировка: server_id ASC, measured_at ASC, metric_id ASC. Пустая таблица — пустой результат. В тестовых результатах разделитель |, NULL — пустое поле. Допускается любой эквивалентный запрос.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 5
Оставшиеся платежи клиента
Что уже дано
Таблица payments(payment_id INTEGER PRIMARY KEY, client_id INTEGER NOT NULL, payment_date TEXT NOT NULL, amount INTEGER NOT NULL). До 500 строк; ID положительные, amount от 0 до 100000. Даты YYYY-MM-DD в 2024 году; у клиента возможны несколько платежей за день.
Что нужно сделать
Для каждого платежа посчитайте сумму текущего и всех последующих платежей того же клиента в порядке payment_date, payment_id.
Ввод и вывод
Отправьте один SELECT или WITH-запрос; stdin и печать не используются. Ровно 4 столбцов в указанном порядке и с этими именами: client_id, payment_date, amount, remaining_client_total. client_id, amount, remaining_client_total — INTEGER; payment_date — TEXT; NULL нет. Сортировка: client_id ASC, payment_date ASC, payment_id ASC. Пустая таблица — пустой результат. В тестовых результатах разделитель |, NULL — пустое поле. Допускается любой эквивалентный запрос.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
