При разработке отчетов и обработок в 1С программисты часто сталкиваются с задачей фильтрации документов, имеющих выраженный период действия. В отличие от стандартной даты документа, которая фиксирует лишь момент его проведения, реквизиты ДатаНачала и ДатаОкончания задают временной интервал, в рамках которого этот документ актуален (например, маркетинговая акция, скидка или начисление).
В этой статье мы подробно разберем, как правильно построить запрос для отбора таких документов, решим проблему исчезновения документов из отчета при отсутствии движений в регистрах и изучим математически верный алгоритм пересечения временных интервалов.
Рассмотрим ситуацию, описанную автором темы. В информационной базе есть документ «Акция», у которого заданы реквизиты ДатаНачала и ДатаОкончания. Также существует регистр сведений АкционныеНачисления, куда записываются факты расхода по акциям. Автор пытается построить запрос напрямую к таблице регистра сведений:
ВЫБРАТЬ
АкционныеНачисления.Акция.ДатаНачала,
АкционныеНачисления.Акция.ДатаОкончания,
АкционныеНачисления.Период
ИЗ
РегистрСведений.АкционныеНачисления КАК АкционныеНачисления
ГДЕ
АкционныеНачисления.Период МЕЖДУ &НачалоПериода И &КонецПериода
При таком подходе возникает серьезная логическая ошибка: если акция уже началась, но по ней еще не было зарегистрировано ни одного расхода (нет записей в регистре), то документ «Акция» вообще не попадет в отчет. Таблица регистра физически не содержит записей по новым или неиспользованным акциям.
Правильное архитектурное решение: в качестве ведущей (главной) таблицы запроса мы должны использовать таблицу документов. Документ в базе данных существует всегда, независимо от наличия проводок или движений по регистрам. Регистр же мы будем присоединять с помощью левого соединения.
Проанализируем базовый шаблон правильного выбора таблиц:
ВЫБРАТЬ
Акции.Ссылка КАК Акция,
Акции.ДатаНачала КАК ДатаНачала,
Акции.ДатаОкончания КАК ДатаОкончания,
ЕСТЬNULL(Начисления.РесурсРасход, 0) КАК Расход
ИЗ
Документ.УстановкаСкидокНоменклатуры КАК Акции
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.АкционныеНачисления КАК Начисления
ПО Начисления.Акция = Акции.Ссылка
При такой структуре запроса мы гарантированно получим список всех документов акций, а в колонках начислений отобразится NULL (который мы заменяем на 0 с помощью функции ЕСТЬNULL) для тех акций, по которым не было движений.
Теперь перейдем к самой сложной части — наложению фильтра по периоду. Пользователь в отчете указывает интервал: &НачалоПериода и &КонецПериода. Нам нужно отобрать документы, период действия которых (от ДатаНачала до ДатаОкончания) хотя бы частично пересекается с выбранным интервалом отчета.
Разработчики часто совершают ошибку, пытаясь описать все возможные варианты пересечения через громоздкие условия с оператором ИЛИ (как показано в сообщениях 26 и 31 форума). Это усложняет чтение кода и замедляет выполнение SQL-запроса.
Давайте визуализируем четыре возможных сценария пересечения периода документа [ДН, ДО] и периода отчета [ПН, ПО]:
Существует простая и элегантная математическая формула, которая покрывает все четыре сценария одновременно. Два интервала пересекаются тогда и только тогда, когда начало первого интервала меньше или равно концу второго, а конец первого интервала больше или равен началу второго.
В терминах СУБД и языка запросов 1С это условие записывается всего в две строки:
ГДЕ
Акции.ДатаНачала <= &КонецПериода
И Акции.ДатаОкончания >= &НачалоПериода
Давайте проверим эту формулу на примере из сообщения 19: у документа дата начала 05.11.2019, дата окончания 10.11.2019. В отчете мы задаем период с 06.11.2019 по 07.11.2019.
Подставим значения в формулу:
Акции.ДатаНачала (05.11) <= &КонецПериода (07.11) — ИстинаАкции.ДатаОкончания (10.11) >= &НачалоПериода (06.11) — ИстинаОба условия выполняются, документ успешно попадает в выборку!
В реальных конфигурациях (например, в «Управление торговлей 10.3» или «Розница») маркетинговые акции часто создаются без указания даты окончания. Это означает, что акция действует бессрочно. В базе данных незаполненная дата хранится как пустая дата 1С — 01.01.0001 00:00:00 (или пустая дата СУБД).
Если мы применим наше универсальное условие пересечения к бессрочной акции, то проверка Акции.ДатаОкончания >= &НачалоПериода вернет Ложь, так как пустая дата 01.01.0001 всегда меньше любой реальной даты отчета. В результате все бессрочные акции исчезнут из отчета.
Чтобы избежать этой ошибки, мы должны доработать условие в секции ГДЕ, добавив проверку на пустую дату окончания:
ГДЕ
Акции.ДатаНачала <= &КонецПериода
И (Акции.ДатаОкончания >= &НачалоПериода
ИЛИ Акции.ДатаОкончания = ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0))
Обратим внимание на конструкцию АкционныеНачисления.Акция.ДатаНачала из первого сообщения форума. Обращение к реквизитам через точку от ссылочного поля (разыменование ссылки) на уровне СУБД приводит к неявному соединению таблиц.
Если поле Акция имеет составной тип данных, платформа 1С сгенерирует сложный SQL-запрос с левым соединением ко всем таблицам документов, входящим в этот тип. На больших базах данных это приводит к колоссальному падению производительности и неоптимальным планам запросов.
Правило разработки: всегда избегайте разыменования полей составного типа в секции ГДЕ. Гораздо эффективнее сначала отобрать нужные документы по индексу в рамках их собственной физической таблицы, а затем соединить результат с регистром сведений.
С учетом всех разобранных правил и нюансов, составим финальный, оптимизированный и отказоустойчивый запрос для получения акций за период:
ВЫБРАТЬ
ДокументАкции.Ссылка КАК ДокументАкция,
ДокументАкции.ДатаНачала КАК ДатаНачала,
ДокументАкции.ДатаОкончания КАК ДатаОкончания,
ЕСТЬNULL(РегистрНачисления.Период, ДАТАВРЕМЯ(1, 1, 1)) КАК ПериодРасхода,
ЕСТЬNULL(РегистрНачисления.СуммаРасход, 0) КАК СуммаРасхода
ИЗ
Документ.УстановкаСкидокНоменклатуры КАК ДокументАкции
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.АкционныеНачисления КАК РегистрНачисления
ПО РегистрНачисления.Акция = ДокументАкции.Ссылка
ГДЕ
ДокументАкции.ДатаНачала <= КОНЕЦПЕРИОДА(&КонецПериода, ДЕНЬ)
И (ДокументАкции.ДатаОкончания >= НАЧАЛОПЕРИОДА(&НачалоПериода, ДЕНЬ)
ИЛИ ДокументАкции.ДатаОкончания = ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0))
Разберем детально, что мы сделали в этом запросе:
Документ.УстановкаСкидокНоменклатуры, чтобы гарантировать вывод всех документов, даже если по ним нет записей в регистре.ЛЕВОЕ СОЕДИНЕНИЕ с таблицей регистра сведений по ссылке документа.ЕСТЬNULL для корректного вывода нулей и пустых дат вместо неопределенных значений СУБД.НАЧАЛОПЕРИОДА и КОНЕЦПЕРИОДА к параметрам запроса, чтобы исключить влияние времени (часы, минуты, секунды) на корректность отбора.Используя данный подход, вы сможете быстро и без потери производительности системы строить любые отчеты, анализирующие работу периодических документов в системе 1С:Предприятие.