Урок курса

Оконные рамки RANGE и равные значения

Продвинутый SQL: сложные запросы и задачи для собеседований

Понятие peers и рамка по умолчанию при равных значениях

В базовом нарастающем итоге с явным указанием ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW каждая физическая строка получает своё отдельное значение суммы. Если же убрать ключевое слово ROWS и оставить только ORDER BY, результат на строках с одинаковым ключом сортировки меняется.

Создадим таблицы замеров давления и результатов тестирования, на которых проверим поведение оконных агрегатов:

CREATE TABLE sensor_readings (
    reading_id INTEGER PRIMARY KEY,
    sensor_id INTEGER NOT NULL,
    elapsed_sec INTEGER NOT NULL,
    pressure REAL NOT NULL
);

INSERT INTO sensor_readings (reading_id, sensor_id, elapsed_sec, pressure) VALUES
(1, 101, 10, 2.0),
(2, 101, 12, 3.5),
(3, 101, 12, 1.5),
(4, 101, 15, 4.0),
(5, 101, 25, 5.0),
(6, 101, 26, 2.0),
(7, 102, 10, 8.0);

CREATE TABLE exam_scores (
    student_id INTEGER PRIMARY KEY,
    score INTEGER NOT NULL
);

INSERT INTO exam_scores (student_id, score) VALUES
(1, 70),
(2, 75),
(3, 75),
(4, 80),
(5, 95);

Выполним расчет нарастающего итога для датчика 101 без явного указания границ рамки:

SELECT reading_id, elapsed_sec, pressure,
       SUM(pressure) OVER (
           PARTITION BY sensor_id
           ORDER BY elapsed_sec
       ) AS running_pressure_default
FROM sensor_readings
WHERE sensor_id = 101
ORDER BY elapsed_sec, reading_id;

Результат запроса:

reading_id elapsed_sec pressure running_pressure_default
1 10 2.0 2.0
2 12 3.5 7.0
3 12 1.5 7.0
4 15 4.0 11.0
5 25 5.0 16.0
6 26 2.0 18.0

Для строк 2 и 3 значение elapsed_sec одинаково и равно 12. В столбце running_pressure_default обе строки получили результат 7.0. В сумму вошли замер первой секунды (2.0) и оба замера на двенадцатой секунде: 2.0 + 3.5 + 1.5 = 7.0.

Строки раздела, которые имеют одинаковые значения во всех столбцах предложения ORDER BY, называются сверстниками (peers). По стандарту SQL и в SQLite, если в окне указан ORDER BY, но рамка не задана явно, СУБД применяет определение по умолчанию:

RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

В рамке RANGE граница CURRENT ROW работает иначе, чем в ROWS. Она обозначает не одну текущую физическую запись, а группу всех её сверстников. При расчете агрегата для строки 2 рамка расширяется до конца группы со значением 12 и сразу захватывает строку 3. Поэтому обе строки получают идентичный накопленный итог.

Ограниченная рамка RANGE по числовой шкале

Рамка RANGE умеет ограничивать окно не количеством физических строк, а математическим смещением по числовой шкале ORDER BY. Конструкция RANGE BETWEEN N PRECEDING AND CURRENT ROW включает в расчёт только те строки, чьё значение ключа сортировки попадает в числовой интервал [val - N, val], где val — значение текущей строки, а N — неотрицательная числовая константа.

Посчитаем скользящую сумму давления за текущую секунду и три предшествующие секунды (интервал [elapsed_sec - 3, elapsed_sec]):

SELECT reading_id, elapsed_sec, pressure,
       SUM(pressure) OVER (
           PARTITION BY sensor_id
           ORDER BY elapsed_sec
           RANGE BETWEEN 3 PRECEDING AND CURRENT ROW
       ) AS sum_last_3_sec
FROM sensor_readings
WHERE sensor_id = 101
ORDER BY elapsed_sec, reading_id;

Результат выполнения:

reading_id elapsed_sec pressure sum_last_3_sec
1 10 2.0 2.0
2 12 3.5 7.0
3 12 1.5 7.0
4 15 4.0 9.0
5 25 5.0 5.0
6 26 2.0 7.0

На строке с секундой 15 окно отбирает замеры из диапазона от 12 до 15 секунд (15 - 3 = 12). В этот интервал попадают строки со значениями 12 (давление 3.5 и 1.5) и 15 (давление 4.0). Их сумма равна 3.5 + 1.5 + 4.0 = 9.0. Замер с десятой секунды в диапазон не вошел, так как 10 < 12.

На строке с секундой 25 окно ищет замеры в диапазоне от 22 до 25. Предыдущий замер был сделан на 15 секунде, поэтому в рамку вошла только сама 25-я секунда со значением 5.0.

Рамку можно расширять и вперёд с помощью FOLLOWING. Рассчитаем количество студентов из таблицы exam_scores, чьи баллы находятся в коридоре [score - 5, score + 5]:

SELECT student_id, score,
       COUNT(*) OVER (
           ORDER BY score
           RANGE BETWEEN 5 PRECEDING AND 5 FOLLOWING
       ) AS students_in_corridor
FROM exam_scores
ORDER BY score, student_id;

Результат вычисления:

student_id score students_in_corridor
1 70 3
2 75 4
3 75 4
4 80 3
5 95 1

Для студента 1 с баллом 70 коридор составляет [65, 75]. В него попадают студент 1 (70) и два студента с баллом 75, всего 3 человека. Для студентов с баллом 75 диапазон равен [70, 80]: в него входят строки со значениями 70, 75, 75 и 80, то есть 4 человека. Для балла 95 коридор [90, 100] содержит лишь одну строку.

Общий шаблон для агрегирования по числовому диапазону шкалы:

SELECT entity_id, val_col,
       AGG(metric_col) OVER (
           PARTITION BY entity_id
           ORDER BY val_col
           RANGE BETWEEN N PRECEDING AND CURRENT ROW
       ) AS range_agg
FROM facts;

В SQLite для явных численных границ N PRECEDING или N FOLLOWING в секции ORDER BY должно быть строго одно числовое выражение.

Различия ROWS и RANGE на дублях и разрывах шкалы

Конструкция ROWS считает физические строки, а RANGE измеряет расстояние между значениями сортировки. Сопоставим обе рамки с одинаковым числовым смещением 2 на таблице sensor_readings:

SELECT reading_id, elapsed_sec, pressure,
       SUM(pressure) OVER (
           PARTITION BY sensor_id
           ORDER BY elapsed_sec, reading_id
           ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
       ) AS rows_2_preceding,
       SUM(pressure) OVER (
           PARTITION BY sensor_id
           ORDER BY elapsed_sec
           RANGE BETWEEN 2 PRECEDING AND CURRENT ROW
       ) AS range_2_preceding
FROM sensor_readings
WHERE sensor_id = 101
ORDER BY elapsed_sec, reading_id;

Результат запроса:

reading_id elapsed_sec pressure rows_2_preceding range_2_preceding
1 10 2.0 2.0 2.0
2 12 3.5 5.5 7.0
3 12 1.5 7.0 7.0
4 15 4.0 9.0 4.0
5 25 5.0 10.5 5.0
6 26 2.0 11.0 7.0

Разница между подходами видна на двух ситуациях: дублирующихся ключах и разрывах шкалы.

На строке 2 (elapsed_sec = 12): ROWS BETWEEN 2 PRECEDING AND CURRENT ROW захватывает две физические строки: текущую и предыдущую (секунды 12 и 10), получая 3.5 + 2.0 = 5.5. Вторая запись с секундой 12 ещё не обработана физическим порядком. Рамка RANGE BETWEEN 2 PRECEDING AND CURRENT ROW оценивает диапазон [10, 12] и включает всех peers текущего значения 12. В результат сразу попадают строки 1, 2 и 3, давая сумму 7.0.

На строке 4 (elapsed_sec = 15): ROWS берёт две предшествующие физические записи — обе строки со значением 12. В сумму входят 3.5 + 1.5 + 4.0 = 9.0. Рамка RANGE отбирает замеры из отрезка [13, 15]. Замеры 12-й секунды в этот отрезок не попадают. В рамке остаётся только текущая строка со значением 4.0.

На строке 5 (elapsed_sec = 25): между 15 и 25 секундами прошло 10 секунд. ROWS игнорирует этот временной разрыв и суммирует строку 5 (5.0), строку 4 (4.0) и строку 3 (1.5), получая 10.5. RANGE проверяет диапазон [23, 25], отсекает далёкие события и оставляет только 5.0.

Выбор между ROWS и RANGE определяется смыслом задачи: если метрику нужно рассчитать строго за последние N записей независимо от времени их появления, используется ROWS; если агрегат требуется за физический интервал шкалы значений, применяется RANGE.

Ловушка добавления суррогатного ключа в ORDER BY при RANGE

Для устранения неоднозначности порядка строк в окно часто добавляют уникальный идентификатор: ORDER BY elapsed_sec, reading_id. Если применить такую сортировку к рамке RANGE с числовым смещением, запрос завершится ошибкой:

SELECT reading_id, elapsed_sec, pressure,
       SUM(pressure) OVER (
           PARTITION BY sensor_id
           ORDER BY elapsed_sec, reading_id
           RANGE BETWEEN 3 PRECEDING AND CURRENT ROW
       ) AS err_sum
FROM sensor_readings
WHERE sensor_id = 101;

База данных возвращает диагностику:

RANGE with offset PRECEDING/FOLLOWING requires one ORDER BY expression

Причина ограничения: числовое смещение 3 PRECEDING вычитается из значения ключа сортировки. Если в ORDER BY указано два столбца с разными единицами измерения (elapsed_sec и reading_id), математическая операция вычитания смещения становится неопределённой. SQLite требует строго одно выражение сортировки при использовании явных числовых границ в RANGE.

Даже в диалектах, допускающих составной ключ в неявной рамке RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, добавление уникального идентификатора в окно меняет логику расчёта. Первичный ключ делает каждую пару значений уникальной, что уничтожает группы peers: строки с одинаковым временем перестанут считаться сверстниками и начнут суммироваться раздельно.

Для сохранения логики логического диапазона и одновременного получения детерминированного вывода уникальный ключ указывается только во внешнем ORDER BY запроса, но не внутри секции OVER():

SELECT reading_id, elapsed_sec, pressure,
       SUM(pressure) OVER (
           PARTITION BY sensor_id
           ORDER BY elapsed_sec
           RANGE BETWEEN 3 PRECEDING AND CURRENT ROW
       ) AS pressure_window_sum
FROM sensor_readings
WHERE sensor_id = 101
ORDER BY elapsed_sec, reading_id;

В таком виде оконная функция объединяет сверстников по секундной шкале, а внешний порядок гарантирует воспроизводимый список строк на выходе.

Задача 1

Дана таблица фактов: (id, score, val) со строками (1, 10, 5), (2, 20, 10), (3, 20, 15), (4, 30, 20). Вычисляется запрос: SELECT id, SUM(val) OVER (ORDER BY score RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS total FROM facts ORDER BY id. Какое значение total будет получено для строки с id = 2?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Дана числовая шкала шагов прогресса: шаг 1 (100 очков), шаг 2 (200 очков), шаг 10 (300 очков). Аналитик хочет посчитать сумму очков за текущий шаг и шаги в пределах отставания не более чем на 2 шага (то есть для шага X суммировать шаги от X - 2 до X). Какой тип оконной рамки корректно выполнит это условие на строке шага 10?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Есть строки (id, x): (1, 1), (2, 1), (3, 2), (4, 5). Для строки id = 2 сопоставьте рамки и вошедшие значения x: A — ORDER BY x, id ROWS BETWEEN CURRENT ROW AND CURRENT ROW; B — ORDER BY x RANGE BETWEEN CURRENT ROW AND CURRENT ROW; C — ORDER BY x RANGE BETWEEN CURRENT ROW AND 1 FOLLOWING. Выберите верный вариант.

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 4

Давление за пять секунд

Что уже дано

Таблица sensor_readings(reading_id INTEGER PRIMARY KEY, sensor_id INTEGER NOT NULL, elapsed_sec INTEGER NOT NULL, pressure REAL NOT NULL). До 500 строк. ID положительные, elapsed_sec от 0 до 100000, pressure — целое от 0 до 1000, сохранённое как REAL. В одну секунду допустимы несколько замеров.

Что нужно сделать

Для каждого замера найдите сумму давления всех замеров того же датчика в замкнутом числовом интервале [elapsed_sec − 5, elapsed_sec]. Все замеры с одинаковой секундой входят вместе.

Ввод и вывод

Отправьте один SELECT или WITH-запрос; stdin и печать не используются. Ровно 4 столбцов в указанном порядке и с этими именами: sensor_id, elapsed_sec, pressure, pressure_window_sum. sensor_id и elapsed_sec — INTEGER; pressure и pressure_window_sum — REAL; NULL нет. Сортировка: sensor_id ASC, elapsed_sec ASC, reading_id ASC. Пустая таблица — пустой результат. В тестовых результатах разделитель |, NULL — пустое поле. Допускается любой эквивалентный запрос.

РешениеSQL · SQLite

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 5

Коридоры баллов независимых экзаменов

Что уже дано

Таблица exam_scores(student_id INTEGER PRIMARY KEY, exam_id INTEGER NOT NULL, score INTEGER NOT NULL). До 500 студентов; student_id и exam_id положительные, score от 0 до 100. В этой выгрузке каждый студент относится к одному экзамену. Баллы студентов одного или разных экзаменов могут совпадать.

Что нужно сделать

Для каждого студента посчитайте студентов того же экзамена, включая его самого, с баллом в замкнутом коридоре [score − 5, score + 5]. Экзамены оцениваются независимо: результаты других экзаменов в его коридор не входят.

Ввод и вывод

Отправьте один SELECT или WITH-запрос; stdin и печать не используются. Ровно 4 столбцов в указанном порядке и с этими именами: exam_id, student_id, score, students_in_corridor. Все четыре столбца — INTEGER; NULL нет. Сортировка: exam_id ASC, score ASC, student_id ASC. Пустая таблица — пустой результат. В тестовых результатах разделитель |, NULL — пустое поле. Допускается любой эквивалентный запрос.

РешениеSQL · SQLite

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку