Часть 14 из 16
Представление помогает смотреть на сводку, но агрегат GROUP BY скрывает отдельные продажи или выступления внутри группы. Иногда нам нужны сразу оба уровня: каждая строка расписания и её место, либо каждая продажа и накопленная выручка. Для этого существуют оконные функции.
В этой части построим нумерацию слотов и рейтинг событий, а затем сравним оконный расчёт с группировкой, чтобы было ясно, какой инструмент выбирать.
Нумеруем слоты в событии
SELECT event_id,starts_at,artist_id,
ROW_NUMBER() OVER (
PARTITION BY event_id ORDER BY starts_at
) AS slot_number
FROM performances
ORDER BY event_id,starts_at;
PARTITION BY начинает нумерацию отдельно внутри каждого события. Внутренний ORDER BY задаёт расчёт, внешний — порядок показа всего результата.
Ранжируем выручку
WITH per_event AS (
SELECT event_id,SUM(price) AS revenue
FROM ticket_sales GROUP BY event_id
)
SELECT event_id,revenue,
DENSE_RANK() OVER (ORDER BY revenue DESC) AS revenue_rank
FROM per_event;
DENSE_RANK присвоит одинаковый ранг при равной выручке без пропуска следующего номера. ROW_NUMBER всегда выдаёт разные номера строк.
Смотрим на предыдущий слот
SELECT event_id,starts_at,
LAG(ends_at) OVER (
PARTITION BY event_id ORDER BY starts_at
) AS previous_end
FROM performances;
LAG берёт значение предыдущей строки. У первого слота предыдущего значения нет, поэтому будет NULL.
Где читать подробности
Оконные функции поддерживаются в современных MySQL 8.4 и 9.7. Описание синтаксиса есть в официальном справочнике. Дальше проверим, какой план выбрал сервер, и настроим права пользователя.
Чем окно отличается от GROUP BY
Если у события пять выступлений, запрос с GROUP BY event_id вернёт одну строку на событие. ROW_NUMBER() OVER (...) оставляет все пять строк и добавляет каждой номер. Поэтому можно показать расписание и одновременно обозначить порядок выступлений.
PARTITION BY event_id делит результат на отдельные группы расчёта; ORDER BY starts_at задаёт очередность внутри группы. Если два слота начинаются одновременно, укажите дополнительное поле сортировки, например performance_id, чтобы порядок был детерминированным.
Считаем накопительную выручку
SELECT event_id, sold_at, price,
SUM(price) OVER (
PARTITION BY event_id
ORDER BY sold_at, sale_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_revenue
FROM ticket_sales
ORDER BY event_id, sold_at, sale_id;
Рамка окна начинается с первой строки события и заканчивается текущей. Результат показывает, как накапливается выручка после каждой продажи. Это не меняет строки таблицы и не сохраняет расчёт.
В MySQL 8.4 и 9.7 поддерживаются оконные функции; их синтаксис описан в официальном справочнике. В следующей части проверим реальный план запроса, а не будем угадывать его скорость по виду SQL.
Ранги для ничьих и стабильный порядок
У ROW_NUMBER() каждая строка получает уникальный номер, даже если две продажи имеют одинаковое время. Чтобы номер не менялся от запуска к запуску, добавьте уникальный ключ последним полем сортировки: ORDER BY sold_at, sale_id. RANK() и DENSE_RANK() предназначены для рейтингов: одинаковые значения делят место.
RANK пропускает последующий номер, DENSE_RANK — нет.
Окно не ускоряет автоматически
Оконный запрос всё ещё должен прочитать строки, упорядочить разделы и вычислить значения. На небольшом фестивале это удобно и быстро, но при больших объёмах полезно изучить EXPLAIN и индексы.
Не переносите вычисляемый результат в отдельную таблицу без причины: тогда понадобится поддерживать его актуальность при каждом изменении продажи.
Практика: выберите несколько продаж одного события, добавьте running_revenue и вручную проверьте накопительную сумму. Затем удалите PARTITION BY event_id в копии запроса и сравните: сумма начнёт накапливаться по всем событиям вместе. Это наглядно показывает, за что отвечает секционирование окна.
Группировка или оконная функция?
| Задача | Используйте | Что случится со строками |
|---|---|---|
| Одна итоговая строка на событие | GROUP BY | Строки сворачиваются в группы |
| Оставить каждый слот и добавить номер | ROW_NUMBER() OVER | Исходные строки сохраняются |
| Сравнить с предыдущей продажей | LAG() OVER | К строкам добавляется прошлое значение |
Сначала решите, хотите ли вы свести детали в итог или сохранить каждую из них. Это подсказывает, какую конструкцию применить.
← Предыдущая статья: Представления MySQL: сохраняем повторяемый отчёт
Следующая статья: EXPLAIN и права MySQL: ускоряем и защищаем базу →
0
19