SQL (ANSI)ПрограммированиеРазработчик SQL

Вычислите пиковое число одновременно активных резервирований в системе бронирования отеля, используя строго операции на множестве ANSI SQL, без использования срезов времени или процедурных циклов?

Проходите собеседования с ИИ помощником Hintsage

Ответ на вопрос

История вопроса: Анализ временных интервалов ставил перед реляционными базами данных задачи с 1970-х годов, так как SQL изначально был разработан без родных типов интервалов. Ранние решения полагались на итерацию с помощью курсоров или декартовы произведения между интервалами, что приводило к квадратичной сложности. Введение оконных функций в SQL:2003 и спецификации ROWS BETWEEN позволило создать эффективные агрегаты, заложив основание для современных расчетов конкурентоспособности событий.

Проблема: Определение максимальной конкурентности требует понимания точных моментов, когда происходят изменения состояния — в частности, когда резервирование начинается или заканчивается. Наивный подход расширяет каждый интервал на дискретные временные единицы (срезы времени), что является вычислительно обременительным для длительных пребываний. Основная задача заключается в том, чтобы подсчитать, сколько интервалов перекрываются в любой конкретный момент, не материализуя каждое мгновение временной линии.

Решение: Примените шаблон симуляции дискретных событий. Трансформируйте таблицу интервалов в поток событий, используя UNION ALL, присваивая вес +1 каждому времени заезда (начало) и -1 каждому времени выезда (конец). Хронологически отсортировав эти события и применив накопительную сумму через функции окон SUM() OVER (ORDER BY ...), вы получите активное количество в каждой точке перехода. Максимальное значение этой накопительной суммы представляет пиковую конкурентоспособность.

WITH events AS ( SELECT check_in AS event_time, 1 AS delta FROM reservations UNION ALL SELECT check_out AS event_time, -1 AS delta FROM reservations ), concurrency AS ( SELECT event_time, SUM(delta) OVER ( ORDER BY event_time, delta DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS active_count FROM events ) SELECT MAX(active_count) AS peak_concurrency FROM concurrency;

Ситуация из жизни

Описание проблемы: Сеть люксовых курортов столкнулась с таинственными инцидентами овербукинга в праздничные выходные, несмотря на то что их система доступности сообщала о наличии мест. Устаревший запрос рассчитывал ежедневную заполняемость, разделяя каждое резервирование на отдельные ночные строки с использованием рекурсивного CTE, что генерировало миллионы строк для месячных пребываний. Во время анализа в Новый Год этот подход требовал 12 секунд на выполнение и блокировал таблицу транзакций бронирования, не позволяя делать резервирования в реальном времени.

Решение А: Расширение срезов с помощью таблиц подсчета. Операционная группа первоначально предложила предварительно сгенерировать календарную таблицу и объединить её с резервированием, используя event_date BETWEEN check_in AND check_out. Этот метод обеспечивает интуитивно понятные ежедневные агрегаты, совместимые со стандартными предложениями GROUP BY. Плюсы: концептуально прост для бизнес-аналитиков, легко интегрируется с существующими BI-инструментами. Минусы: генерирует O(N × D) строк, где D — средняя продолжительность, что вызывает экспоненциальный рост; катастрофически проваливается с гранулярностью уровня минуты или долгосрочными арендами; потребляет чрезмерное пространство tempdb.

Решение B: Дерево интервалов с материализованными путями. Старший архитектор предложил реализовать структуру сегментного дерева с использованием вложенных наборов для индексирования границ интервалов, что позволяет выполнять запросы на перекрытие за логарифмическое время. Плюсы: оптимальная теоретическая сложность для частых обновлений и точечных запросов. Минусы: требует сложных триггеров для поддержания баланса дерева; нарушает портативность ANSI SQL, полагаясь на процедурные расширения; вводит увеличение записи, что ухудшает OLTP нагрузку во время всплесков бронирования.

Решение C: Хронологический поток событий с накопительными суммами (выбрано). Команда базы данных приняла подход, основанный на событиях, рассматривая каждую границу резервирования как дельта-операцию. Это сократило набор данных с миллионов строк до ровно 2N событий (начала и конца для каждого резервирования). Плюсы: сложность O(N log N), обусловленная операцией сортировки, постоянный объем памяти и полная совместимость с ANSI SQL на PostgreSQL, Oracle и SQL Server. Минусы: требует тщательной обработки одновременных событий и не позволяет на самом деле определить, какие конкретные резервирования способствовали пику без дополнительных объединений.

Результат: Задержка запроса сократилась с 12 секунд до 45 миллисекунд. Анализ показал, что настоящим узким местом была не заполняемость комнат (500 единиц), а ёмкость лифтов, так как 320 гостей пытались одновременно заехать в 6 вечера. Этот вывод побудил внедрить ступенчатые уровни заезда вместо строительства нового крыла, сэкономив 2 миллиона долларов на капитальных затратах и устранив блокировки.

Что часто упускают кандидаты

Почему решение требует ORDER BY event_time, delta DESC, и что происходит, если вы исключите вторичную сортировку по delta?

Кандидаты часто игнорируют семантику граничных условий при общих временных метках. Когда один гость выезжает ровно в 10:00, а другой заезжает в 10:00, порядок обработки определяет, будет ли комната выглядеть занятой двумя гостями одновременно. Сортируя с delta DESC, мы гарантируем, что -1 (выезд) обрабатывается перед +1 (заезд) при совпадающих временных метках. Без этой вторичной сортировки накопительная сумма временно падает, а затем поднимается, потенциально регистрируя призрачный пик, когда предыдущее состояние фактически было выше. Это тонкое упорядочение предотвращает ошибки на единицу, которые приводят к уязвимостям овербукинга в производственных системах.

Как бы вы изменили этот запрос, чтобы определить, какие конкретные резервирования были активны в момент пиковой конкурентоспособности, а не просто количество?

Большинство кандидатов пытаются фильтровать внутри одного и того же CTE, не замечая, что пик может охватывать непрерывный интервал, а не отдельную точку. Надежный подход требует стратегии с двумя проходами: сначала изолировать временную метку, где active_count равен максимуму, используя подзапрос или CTE, затем вернуться к исходной таблице резервирований, используя предикат перекрытия r.check_in <= peak.event_time AND r.check_out > peak.event_time. Кандидаты упускают, что несколько временных меток могут иметь одинаковое максимальное значение, что требует использования DISTINCT или EXISTS для предотвращения дублирования списков резервирования, когда пик удерживается на нескольких переходах событий.

Какие модификации необходимы, если бизнес-правила изменяются так, что время выезда является включительным (гость занимает номер до 11:59 PM), а не исключительным, и как это влияет на взвешивание событий?

Кандидаты часто упускают семантику границ интервала. Включительные конечные точки [start, end] создают перекрытия, когда одно резервирование заканчивается, а другое начинается в тот же день. Решение требует преобразования включающих границ в исключительные, добавляя бесконечно малую дельту (или следующую дискретную временную единицу) к времени выезда перед созданием -1 события. В качестве альтернативы измените логику объединения, чтобы использовать check_out >= event_time, сохраняя логику накопления суммы. Неспособность сделать это преобразует модель дискретного события из полуоткрытых интервалов в закрытые интервалы, что приводит к тому, что алгоритм сообщает о конфликтах, где их нет, и недооценивает истинную емкость на ровно одну единицу в периоды высокой смены.