Как в запросе 1С объединить раздельные поля даты и времени для фильтрации за период?

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

При работе с нетиповыми или устаревшими конфигурациями 1С программисты часто сталкиваются с архитектурным наследием, способным вызвать недоумение. Одним из таких примеров является раздельное хранение даты и времени в разных реквизитах документа. Например, когда дата документа или события хранится в реквизите ДатаПринятия (с нулевым временем, например, 15.08.2018 00:00:00), а время — в реквизите ВремяПринятия (на пустую дату, например, 01.01.0001 08:06:27).

Сложности начинаются тогда, когда нам требуется написать запрос для получения данных за интервал, который не совпадает с границами календарных суток (например, с 8 утра одной даты до 8 утра следующей даты). Обычное условие МЕЖДУ здесь применить невозможно. В этой статье мы подробно разберем несколько эффективных способов решения этой задачи: от простых логических условий в запросе до математического сложения дат и глобального исправления архитектуры базы данных.

Способ 1. Логическое разделение интервалов (оптимально для производительности)

Прежде чем переходить к сложным математическим функциям вычисления дат в запросе, рассмотрим наиболее производительный способ. Его ключевое преимущество заключается в том, что СУБД сможет использовать существующие индексы по полям ДатаПринятия и ВремяПринятия. Это критически важно для баз данных с большим объемом документов.

Для того чтобы выбрать документы за период, например, с 15.08.2018 08:00:00 по 16.08.2018 08:00:00, мы можем разбить наше условие ГДЕ на три составляющих, объединенных логическим оператором ИЛИ:

  1. Первый интервал (начальный день): Дата равна дате начала периода, а время строго больше или равно времени начала (например, 15.08.2018 и время >= 08:00:00).
  2. Второй интервал (промежуточные дни): Дата строго больше даты начала и строго меньше даты окончания (актуально, если период охватывает несколько дней).
  3. Третий интервал (конечный день): Дата равна дате окончания периода, а время строго меньше или равно времени окончания (например, 16.08.2018 и время <= 08:00:00).

Посмотрим на практическую реализацию этого запроса:


ВЫБРАТЬ
    Заявка.Ссылка КАК Ссылка,
    Заявка.ДатаПринятия КАК ДатаПринятия,
    Заявка.ВремяПринятия КАК ВремяПринятия
ИЗ
    Документ.Заявка КАК Заявка
ГДЕ
    НЕ Заявка.ПометкаУдаления
    И (
        (Заявка.ДатаПринятия = &ДатаНачала И Заявка.ВремяПринятия >= &ВремяНачала)
        ИЛИ
        (Заявка.ДатаПринятия > &ДатаНачала И Заявка.ДатаПринятия < &ДатаОкончания)
        ИЛИ
        (Заявка.ДатаПринятия = &ДатаОкончания И Заявка.ВремяПринятия <= &ВремяОкончания)
    )

При передаче параметров нам нужно предварительно подготовить переменные в коде 1С:


// Разделяем параметры на "чистые" даты и время
Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(ПериодНачала)); // 15.08.2018 00:00:00
Запрос.УстановитьПараметр("ДатаОкончания", НачалоДня(ПериодОкончания)); // 16.08.2018 00:00:00

// Время передаем на пустую базовую дату 01.01.0001
Запрос.УстановитьПараметр("ВремяНачала", Дата("00010101") + (ПериодНачала - НачалоДня(ПериодНачала))); // 01.01.0001 08:00:00
Запрос.УстановитьПараметр("ВремяОкончания", Дата("00010101") + (ПериодОкончания - НачалоДня(ПериодОкончания))); // 01.01.0001 08:00:00

Этот подход работает мгновенно даже на миллионных таблицах, так как СУБД выполняет простой поиск по индексам полей без предварительного преобразования значений в строках таблицы.

Способ 2. Сложение даты и времени через разность в секундах

Если вам обязательно нужно получить единое поле даты и времени непосредственно в запросе (например, для последующей группировки или вывода в отчет), можно использовать функции ДОБАВИТЬКДАТЕ и РАЗНОСТЬДАТ.

Проанализируем логику этого решения: мы берем базовую дату 01.01.0001 00:00:00 и вычисляем разницу в секундах между ней и нашим реквизитом ВремяПринятия. Полученное количество секунд мы прибавляем к реквизиту ДатаПринятия.

Разберем пример запроса, реализующего эту математику:


ВЫБРАТЬ
    Заявка.Ссылка КАК Ссылка,
    ДОБАВИТЬКДАТЕ(
        Заявка.ДатаПринятия, 
        СЕКУНДА, 
        РАЗНОСТЬДАТ(ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0), Заявка.ВремяПринятия, СЕКУНДА)
    ) КАК ПолнаяДатаПринятия
ИЗ
    Документ.Заявка КАК Заявка
ГДЕ
    НЕ Заявка.ПометкаУдаления
    И ДОБАВИТЬКДАТЕ(
        Заявка.ДатаПринятия, 
        СЕКУНДА, 
        РАЗНОСТЬДАТ(ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0), Заявка.ВремяПринятия, СЕКУНДА)
    ) МЕЖДУ &НачалоПериода И &КонецПериода

Важное предупреждение: Использование функций СУБД над полями базы данных в секции ГДЕ делает запрос несаргабельным (non-sargable). СУБД не сможет применить индекс к полю ДатаПринятия, из-за чего будет вынуждена произвести полное сканирование таблицы (Table Scan). На больших объемах данных это гарантированно приведет к существенному замедлению работы системы.

Способ 3. Оптимизированное сложение (сложение дней)

Мы можем оптимизировать предыдущий способ вычислений. Вместо того чтобы рассчитывать миллионы секунд от «начала времен» для каждого документа, можно поменять слагаемые местами.

Поскольку реквизит ВремяПринятия уже содержит в себе нужное нам время и дату 01.01.0001, мы можем вычислить разность дат в днях между началом времен и ДатаПринятия, а затем добавить полученное количество дней к ВремяПринятия. Вычисление дней для СУБД является гораздо более легкой математической операцией, чем расчет секунд.

Посмотрим на структуру такого выражения:


ВЫБРАТЬ
    Заявка.Ссылка КАК Ссылка,
    ДОБАВИТЬКДАТЕ(
        Заявка.ВремяПринятия, 
        ДЕНЬ, 
        РАЗНОСТЬДАТ(ДАТАВРЕМЯ(1, 1, 1), Заявка.ДатаПринятия, ДЕНЬ)
    ) КАК ПолнаяДатаПринятия
ИЗ
    Документ.Заявка КАК Заявка

Способ 4. Применение "грубого фильтра" для ускорения вычислений

Для того чтобы совместить удобство работы с объединенной датой (Способ 3) и высокую скорость работы индексов (Способ 1), применим стратегию предварительной фильтрации, также известную как «грубый фильтр».

Суть метода заключается в том, что мы сначала ограничиваем выборку документов стандартным условием по индексированному полю ДатаПринятия без использования функций. Мы отсекаем документы, которые гарантированно не могут попасть в наш период. И только к оставшемуся небольшому числу записей СУБД применит тяжелые функции сложения даты и времени.

Рассмотрим пример запроса с грубой предварительной фильтрацией:


ВЫБРАТЬ
    Заявка.Ссылка КАК Ссылка
ИЗ
    Документ.Заявка КАК Заявка
ГДЕ
    НЕ Заявка.ПометкаУдаления
    // 1. Грубый фильтр по индексу (выбираем документы за полные дни периода)
    И Заявка.ДатаПринятия МЕЖДУ &ГрубоеНачало И &ГрубыйКонец
    // 2. Точный фильтр по рассчитанной полной дате
    И ДОБАВИТЬКДАТЕ(
        Заявка.ВремяПринятия, 
        ДЕНЬ, 
        РАЗНОСТЬДАТ(ДАТАВРЕМЯ(1, 1, 1), Заявка.ДатаПринятия, ДЕНЬ)
    ) МЕЖДУ &НачалоПериода И &КонецПериода

В параметры запроса мы передаем:

Способ 5. Сдвиг интервала (метод виртуальной смены)

Если ваши сутки/смена стабильно начинаются со смещением (например, всегда в 08:00), то в запросе можно применить логику виртуального сдвига времени назад на 8 часов. При таком подходе документы, созданные с 08:00 текущего дня до 08:00 следующего дня, "виртуально" переместятся в рамки одних стандартных календарных суток (с 00:00:00 до 23:59:59).

Для реализации этого метода мы объединяем дату и время, а затем вычитаем из полученного значения 8 часов с помощью функции ДОБАВИТЬКДАТЕ:


ВЫБРАТЬ
    Заявка.Ссылка КАК Ссылка
ИЗ
    Документ.Заявка КАК Заявка
ГДЕ
    НЕ Заявка.ПометкаУдаления
    И ДОБАВИТЬКДАТЕ(
        ДОБАВИТЬКДАТЕ(
            Заявка.ВремяПринятия, 
            ДЕНЬ, 
            РАЗНОСТЬДАТ(ДАТАВРЕМЯ(1, 1, 1), Заявка.ДатаПринятия, ДЕНЬ)
        ), 
        ЧАС, 
        -8
    ) МЕЖДУ &НачалоСуток И &КонецСуток

В качестве параметров &НачалоСуток и &КонецСуток в данном случае передаются стандартные границы календарного дня (например, 15.08.2018 00:00:00 и 15.08.2018 23:59:59). Это значительно упрощает интеграцию запроса с типовыми отчетами и СКД.

Способ 6. Кардинальное решение проблемы (Рефакторинг)

Разделение единой сущности "Дата и время" на два отдельных реквизита в 1С — это классическая архитектурная ошибка. Если у вас есть возможность вносить изменения в конфигурацию, настоятельно рекомендуется исправить первопричину проблемы.

Выполним рефакторинг по шагам:

  1. Создадим в документе Заявка новый реквизит ДатаВремяПринятия с типом Дата (состав: Дата и время).
  2. В модуле объекта документа в обработчике события ПередЗаписью пропишем автоматическое слияние полей:

Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    // Объединяем дату и время в один полноценный реквизит
    Если ЗначениеЗаполнено(ДатаПринятия) Тогда
        ВремяСек = ?(ЗначениеЗаполнено(ВремяПринятия), ВремяПринятия - НачалоДня(ВремяПринятия), 0);
        ДатаВремяПринятия = НачалоДня(ДатаПринятия) + ВремяСек;
    Иначе
        ДатаВремяПринятия = Справочники.ЗначенияСвойствОбъектов.ПустаяСсылка();
    КонецЕсли;
    
КонецПроцедуры
  1. Создадим и запустим разовую обработку, которая заполнит новый реквизит ДатаВремяПринятия во всех исторических документах в базе данных.
  2. Установим свойство Индексировать нового реквизита в значение "Индексировать" (или "Индексировать с доп. упорядочиванием").

После проведения данных архитектурных изменений наш итоговый запрос станет максимально простым, понятным и быстродействующим:


ВЫБРАТЬ
    Заявка.Ссылка КАК Ссылка
ИЗ
    Документ.Заявка КАК Заявка
ГДЕ
    НЕ Заявка.ПометкаУдаления
    И Заявка.ДатаВремяПринятия МЕЖДУ &НачалоПериода И &КонецПериода

Мы проанализировали различные варианты решения проблемы раздельного хранения даты и времени. Для быстрого решения без изменения структуры конфигурации используйте Способ 1 (с логическими условиями ИЛИ) или Способ 4 (с грубым фильтром). Если же вы нацелены на долгосрочную стабильность и чистоту кода приложения — отдайте предпочтение Способу 6.

← На главную