Рассмотрим нетривиальную олимпиадную задачу по языку запросов: имеется таблица проходов сотрудников через турникет со следующей структурой: Сотрудник, Время (уникальное значение для каждой строки) и признак Вход (булево: Истина — вход, Ложь — выход). В течение рабочего дня один и тот же сотрудник может входить и выходить неоднократно.
Сформулируем цель: одним запросом получить итоговую таблицу интервалов, содержащую поля Сотрудник, ВремяПрихода, ВремяУхода и длительность нахождения внутри помещения. При этом действуют строгие ограничения: запрещено использовать агрегатные функции (МИНИМУМ, МАКСИМУМ), предложение СГРУППИРОВАТЬ ПО, конструкцию В (IN), а также временные таблицы и вложенные подзапросы в секции ВЫБРАТЬ. Решение должно строиться исключительно на соединениях таблиц (JOIN).
Разберем логику построения такого запроса. Чтобы получить корректную пару «вход — выход», необходимо связать событие прихода с ближайшим последующим событием ухода того же сотрудника так, чтобы между ними не было никаких промежуточных проходов.
Для этого применим метод исключения промежуточных точек через левое соединение:
ВНУТРЕННЕЕ СОЕДИНЕНИЕ с таблицей уходов по совпадению сотрудника и условию Приходы.Время < Уходы.Время. Это действие формирует все возможные пары, где уход произошел позже прихода.ЛЕВОЕ СОЕДИНЕНИЕ с той же исходной таблицей (назовем ее «Вставки»), проверяя наличие любых промежуточных событий между текущим приходом и текущим уходом: Приходы.Время < Вставки.Время И Вставки.Время < Уходы.Время.ГДЕ: оставляем только истинные события входа для первой таблицы, события выхода для второй таблицы и проверяем условие Вставки.Время ЕСТЬ NULL. Если между входом и выходом не нашлось ни одного промежуточного события, то перед нами строго последовательная пара.Посмотрим на текст готового запроса для консоли запросов 1С:
ВЫБРАТЬ
"Иванов" КАК Сотрудник,
ДАТАВРЕМЯ(2018, 2, 19, 23, 0, 0) КАК Время,
ИСТИНА КАК Вход
ПОМЕСТИТЬ Дано
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
"Иванов",
ДАТАВРЕМЯ(2018, 2, 19, 23, 59, 59),
ЛОЖЬ
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
"Иванов",
ДАТАВРЕМЯ(2018, 2, 20, 0, 0, 0),
ИСТИНА
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
"Иванов",
ДАТАВРЕМЯ(2018, 2, 20, 8, 0, 0),
ЛОЖЬ
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Приходы.Сотрудник КАК Сотрудник,
Приходы.Время КАК ВремяПрихода,
Уходы.Время КАК ВремяУхода,
РАЗНОСТЬДАТ(Приходы.Время, Уходы.Время, СЕКУНДА) КАК СекундВнутри
ИЗ
Дано КАК Приходы
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Дано КАК Уходы
ПО Приходы.Сотрудник = Уходы.Сотрудник
И Приходы.Время < Уходы.Время
ЛЕВОЕ СОЕДИНЕНИЕ Дано КАК Вставки
ПО (Вставки.Сотрудник = Приходы.Сотрудник)
И Приходы.Время < Вставки.Время
И Вставки.Время < Уходы.Время
ГДЕ
Приходы.Вход
И НЕ Уходы.Вход
И Вставки.Время ЕСТЬ NULL
Проанализируем поведение данного запроса в реальных базах данных на больших объемах (от десятков и сотен тысяч записей). Несмотря на логическую элегантность, такое решение неприменимо в высоконагруженных промышленных системах по следующим причинам:
T1.Время < T3.Время И T3.Время < T2.Время порождает декартово произведение строк по каждому сотруднику. Сложность выборки возрастает до $O(N^3)$, что приводит к генерации огромного количества временных данных в tempdb СУБД.(Сотрудник, Время) отлично работает для точечных поисков или простых срезов, но при множественных перекрестных соединениях с диапазонами СУБД вынуждена многократно сканировать интервалы, вызывая предельную нагрузку на дисковую подсистему и процессор.Выясним, какими методами следует решать подобные задачи в реальной разработке на платформе 1С:
ВЫБРАТЬ
СКУД.Сотрудник КАК Сотрудник,
СКУД.Время КАК Время,
СКУД.Вход КАК Вход
ИЗ
РегистрСведений.СКУД КАК СКУД
УПОРЯДОЧИТЬ ПО
Сотрудник,
Время
Затем в цикле выборки Выборка.Следующий() обходим строки за один проход со сложностью $O(N)$, запоминая время последнего открытого входа в переменной. Это позволяет программно обрабатывать дублирующие входы, незакрытые смены и не перегружать СУБД тяжелыми соединениями.
ВремяПрихода, ВремяУхода и Длительность).LEAD() OVER (PARTITION BY Сотрудник ORDER BY Время).