Как в запросе 1С связать последовательные события входа и выхода без группировок и подзапросов

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Рассмотрим нетривиальную олимпиадную задачу по языку запросов: имеется таблица проходов сотрудников через турникет со следующей структурой: Сотрудник, Время (уникальное значение для каждой строки) и признак Вход (булево: Истина — вход, Ложь — выход). В течение рабочего дня один и тот же сотрудник может входить и выходить неоднократно.

Сформулируем цель: одним запросом получить итоговую таблицу интервалов, содержащую поля Сотрудник, ВремяПрихода, ВремяУхода и длительность нахождения внутри помещения. При этом действуют строгие ограничения: запрещено использовать агрегатные функции (МИНИМУМ, МАКСИМУМ), предложение СГРУППИРОВАТЬ ПО, конструкцию В (IN), а также временные таблицы и вложенные подзапросы в секции ВЫБРАТЬ. Решение должно строиться исключительно на соединениях таблиц (JOIN).

Алгоритм решения через тройное соединение (Anti-Join)

Разберем логику построения такого запроса. Чтобы получить корректную пару «вход — выход», необходимо связать событие прихода с ближайшим последующим событием ухода того же сотрудника так, чтобы между ними не было никаких промежуточных проходов.

Для этого применим метод исключения промежуточных точек через левое соединение:

  1. Выбираем таблицу приходов и связываем ее через ВНУТРЕННЕЕ СОЕДИНЕНИЕ с таблицей уходов по совпадению сотрудника и условию Приходы.Время < Уходы.Время. Это действие формирует все возможные пары, где уход произошел позже прихода.
  2. Добавляем ЛЕВОЕ СОЕДИНЕНИЕ с той же исходной таблицей (назовем ее «Вставки»), проверяя наличие любых промежуточных событий между текущим приходом и текущим уходом: Приходы.Время < Вставки.Время И Вставки.Время < Уходы.Время.
  3. Накладываем фильтр в секции ГДЕ: оставляем только истинные события входа для первой таблицы, события выхода для второй таблицы и проверяем условие Вставки.Время ЕСТЬ 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

Анализ производительности и проблем алгоритма на больших данных

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

  1. Высокая алгоритмическая сложность: Построение диапазонных связей через неравенства T1.Время < T3.Время И T3.Время < T2.Время порождает декартово произведение строк по каждому сотруднику. Сложность выборки возрастает до $O(N^3)$, что приводит к генерации огромного количества временных данных в tempdb СУБД.
  2. Неэффективная работа индексов: Индекс по полям (Сотрудник, Время) отлично работает для точечных поисков или простых срезов, но при множественных перекрестных соединениях с диапазонами СУБД вынуждена многократно сканировать интервалы, вызывая предельную нагрузку на дисковую подсистему и процессор.
  3. Логические краевые случаи: Если в системе возникает технический сбой турникета («дребезг» при повторном прикладывании пропуска, когда фиксируются подряд события Вход1, Вход2, Выход), алгоритм вернет только пару [Вход2, Выход], а запись Вход1 будет полностью утеряна, так как между ней и выходом обнаружено промежуточное событие Вход2.

Альтернативные и оптимальные подходы в 1С

Выясним, какими методами следует решать подобные задачи в реальной разработке на платформе 1С:

  1. Обработка на встроенном языке (линейный проход): Получаем простым запросом упорядоченный список событий:
    
    ВЫБРАТЬ
        СКУД.Сотрудник КАК Сотрудник,
        СКУД.Время КАК Время,
        СКУД.Вход КАК Вход
    ИЗ
        РегистрСведений.СКУД КАК СКУД
    УПОРЯДОЧИТЬ ПО
        Сотрудник,
        Время
    

    Затем в цикле выборки Выборка.Следующий() обходим строки за один проход со сложностью $O(N)$, запоминая время последнего открытого входа в переменной. Это позволяет программно обрабатывать дублирующие входы, незакрытые смены и не перегружать СУБД тяжелыми соединениями.

  2. Хранение готовых интервалов в регистрах: Оптимальная архитектура учета рабочего времени предполагает фиксацию уже готовых интервалов в момент записи события выхода (поиск незакрытого входа и сохранение строки с готовыми полями ВремяПрихода, ВремяУхода и Длительность).
  3. Оконные функции во внешних источниках: При хранении данных СКУД во внешней реляционной СУБД сопоставление соседних строк выполняется за один проход по индексу с помощью оконной функции LEAD() OVER (PARTITION BY Сотрудник ORDER BY Время).
← На главную