Как правильно рассчитать интервалы задолженности в запросе 1С при работе с реквизитами Регистратора и обработкой NULL?

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

Разработка отчетов по дебиторской задолженности (особенно с распределением долга по интервалам старения: 0–14 дней, 15–30 дней, 60–70 дней и т.д.) — одна из классических задач в 1С. Однако при написании таких запросов разработчики часто сталкиваются с логическими ошибками и критическим падением производительности. В этой статье мы подробно разберем, почему ломаются расчеты разности дат при обращении к реквизитам поля Регистратор, как правильно обрабатывать значения NULL и пустые даты, а также как оптимизировать этот процесс с точки зрения производительности СУБД.

В чем суть проблемы? Анализируем исходный запрос

Рассмотрим ситуацию, с которой столкнулся автор темы. В его запросе к виртуальной таблице оборотов регистра накопления ВзаиморасчетыСКонтрагентами.Обороты выполняется расчет интервала задолженности по формуле:


ВЫБОР
    КОГДА РАЗНОСТЬДАТ(ВЫБОР
                    КОГДА ВзаиморасчетыСКонтрагентамиОбороты.Регистратор.ДатаПогашениеЗадолженности = ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0)
                        ТОГДА ВзаиморасчетыСКонтрагентамиОбороты.Регистратор.Дата
                    ИНАЧЕ ВзаиморасчетыСКонтрагентамиОбороты.Регистратор.ДатаПогашениеЗадолженности
                КОНЕЦ, &КонецПериода, ДЕНЬ) > 14
...

При выводе детальных записей по конкретным документам (например, платежное поручение и реализация товаров) логика вычисления интервалов перестает работать. Для документа реализации («Расход» в контексте долга) расчет отрабатывает, а для платежного поручения («Приход») поле интервала оказывается пустым, и итоговая сумма по группировке рассчитывается некорректно. Выясним, почему это происходит.

Основная причина кроется в специфике работы с составными типами данных в 1С. Поле Регистратор является составным типом, который может содержать ссылки на множество различных документов («Реализация товаров и услуг», «Отчет о розничных продажах», «Платежное поручение входящее», «Приходный кассовый ордер» и т. д.).

Когда мы пишем конструкцию Регистратор.ДатаПогашениеЗадолженности, происходят следующие процессы:

  1. Физическое отсутствие реквизита в таблице. У таких документов, как «Реализация товаров», реквизит ДатаПогашениеЗадолженности может существовать. Но у платежных документов (например, «Платежное поручение») этого реквизита физически нет в структуре метаданных.
  2. Появление значений NULL. При выполнении запроса СУБД делает неявное левое соединение (LEFT JOIN) со всеми таблицами документов-регистраторов. Для тех документов, у которых нет реквизита ДатаПогашениеЗадолженности, СУБД возвращает значение NULL.
  3. Логический сбой функций сравнения. Любая операция сравнения с NULL (например, = ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0)) или передача NULL в функцию РАЗНОСТЬДАТ() возвращает NULL (или ЛОЖЬ в условных операторах). Из-за этого вся ветка ВЫБОР КОГДА идет по неверному сценарию, возвращая 0 вместо разности сумм.

Посмотрим на пример правильной обработки таких значений.

---

Шаг 1. Исправление логики обработки NULL и пустых дат

Для того чтобы запрос работал корректно для абсолютно всех типов регистраторов, нам необходимо явно обработать значение NULL, которое возникает при обращении к отсутствующим реквизитам. Для этого используется встроенная функция языка запросов 1С ЕСТЬNULL.

Вместо громоздких условий с оператором ИЛИ:


КОГДА ВзаиморасчетыСКонтрагентамиОбороты.Регистратор.ДатаПогашениеЗадолженности = ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0)
    ИЛИ ВзаиморасчетыСКонтрагентамиОбороты.Регистратор.ДатаПогашениеЗадолженности ЕСТЬ NULL

Рекомендуется использовать изящное и быстрое приведение к пустой дате на самом первом этапе:


КОГДА ЕСТЬNULL(ВзаиморасчетыСКонтрагентамиОбороты.Регистратор.ДатаПогашениеЗадолженности, ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0)) = ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0)

Проанализируем эту конструкцию. Если у регистратора (например, платежки) нет реквизита ДатаПогашениеЗадолженности, функция ЕСТЬNULL вернет пустую дату 01.01.0001. Условие КОГДА выполнится, и запрос корректно перейдет к расчету по дате самого документа (Регистратор.Дата).

---

Шаг 2. Оптимизация производительности запроса (Разыменование полей)

Хотя исправление логики с помощью ЕСТЬNULL решает проблему правильности вычислений, этот запрос несет в себе скрытую «мину замедленного действия» для производительности системы.

Важное правило разработки 1С: Многократное обращение через точку к реквизитам поля составного типа (такого как Регистратор) порождает огромное количество левых соединений (LEFT JOIN) в SQL-запросе. Если у вашего регистра накопления может быть 30-40 различных видов регистраторов, СУБД будет вынуждена соединить таблицу оборотов со всеми 40 таблицами документов! Это гарантированно приведет к зависанию отчета на больших базах данных.

Чтобы избежать этой проблемы, рассмотрим два альтернативных подхода.

Способ А. Ограничение типов с помощью функции ВЫРАЗИТЬ

Если мы заранее знаем, какие типы документов могут содержать реквизит ДатаПогашениеЗадолженности (например, только документ «РеализацияТоваровУслуг»), мы должны явно сказать об этом оптимизатору запросов 1С при помощи ключевого слова ВЫРАЗИТЬ:


ВЫРАЗИТЬ(ВзаиморасчетыСКонтрагентамиОбороты.Регистратор КАК Документ.РеализацияТоваровУслуг).ДатаПогашениеЗадолженности

В этом случае платформа сгенерирует SQL-запрос с соединением только к одной таблице реализации, полностью игнорируя остальные документы, что ускорит работу БД в десятки раз.

Способ Б. Вынесение расчетов во временную таблицу (Рекомендуется)

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

Посмотрим на пример реализации такого пакета запросов:


ВЫБРАТЬ
    ВзаиморасчетыОбороты.ДоговорКонтрагента КАК ДоговорКонтрагента,
    ВзаиморасчетыОбороты.Сделка КАК Сделка,
    ВзаиморасчетыОбороты.Регистратор КАК Регистратор,
    ВзаиморасчетыОбороты.Организация КАК Организация,
    ВзаиморасчетыОбороты.Контрагент КАК Контрагент,
    ВзаиморасчетыОбороты.СуммаВзаиморасчетовПриход КАК СуммаПриход,
    ВзаиморасчетыОбороты.СуммаВзаиморасчетовРасход КАК СуммаРасход,
    // Вычисляем базовую дату для расчетов, избавляясь от NULL
    ВЫБОР
        КОГДА ЕСТЬNULL(ВЫРАЗИТЬ(ВзаиморасчетыОбороты.Регистратор КАК Документ.РеализацияТоваровУслуг).ДатаПогашениеЗадолженности, ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0)) = ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0)
            ТОГДА ВзаиморасчетыОбороты.Регистратор.Дата
        ИНАЧЕ ВЫРАЗИТЬ(ВзаиморасчетыОбороты.Регистратор КАК Документ.РеализацияТоваровУслуг).ДатаПогашениеЗадолженности
    КОНЕЦ КАК БазоваяДатаРасчета
ПОМЕСТИТЬ ВТ_ДанныеОборотов
ИЗ
    РегистрНакопления.ВзаиморасчетыСКонтрагентами.Обороты(, &КонецПериода, Регистратор, ) КАК ВзаиморасчетыОбороты
ГДЕ
    ВзаиморасчетыОбороты.Контрагент = &Контрагент
    И ВзаиморасчетыОбороты.Сделка = &Сделка
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ.ДоговорКонтрагента,
    ВТ.Сделка,
    ВТ.Регистратор,
    ВТ.Организация,
    ВТ.Контрагент,
    ВТ.СуммаПриход,
    ВТ.СуммаРасход,
    // Теперь расчет интервалов стал простым, понятным и очень быстрым
    ВЫБОР
        КОГДА РАЗНОСТЬДАТ(ВТ.БазоваяДатаРасчета, &КонецПериода, ДЕНЬ) МЕЖДУ 15 И 30
            ТОГДА ВТ.СуммаПриход - ВТ.СуммаРасход
        ИНАЧЕ 0
    КОНЕЦ КАК От15До30,
    
    ВЫБОР
        КОГДА РАЗНОСТЬДАТ(ВТ.БазоваяДатаРасчета, &КонецПериода, ДЕНЬ) МЕЖДУ 61 И 70
            ТОГДА ВТ.СуммаПриход - ВТ.СуммаРасход
        ИНАЧЕ 0
    КОНЕЦ КАК От60До70
ИЗ
    ВТ_ДанныеОборотов КАК ВТ
ИТОГИ
    СУММА(СуммаПриход),
    СУММА(СуммаРасход),
    СУММА(От15До30),
    СУММА(От60До70)
ПО
    Контрагент,
    Сделка

---

Архитектурный совет: как сделать еще лучше

Рассмотренный нами пример оптимизации запроса через временные таблицы и ограничение типов ВЫРАЗИТЬ — это отличное техническое решение «в лоб», когда конфигурацию нельзя менять. Однако, если у вас есть доступ к изменению метаданных (разработка ведется в собственной или кастомизированной конфигурации), самым правильным решением будет денормализация данных.

Вместо того чтобы высчитывать дату погашения долга «на лету» в тяжелом отчете, добавьте реквизит ДатаОплаты или СрокОплаты непосредственно в сам регистр накопления ВзаиморасчетыСКонтрагентами. Этот реквизит должен заполняться при проведении документов (например, браться из договора, соглашения или спецификации сделки) и записываться напрямую в движения регистра.

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


ВЫБОР 
    КОГДА РАЗНОСТЬДАТ(ВзаиморасчетыОбороты.ДатаОплаты, &КонецПериода, ДЕНЬ) > 14 
        ТОГДА ...

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

Используйте предложенные методики, следите за обращениями через точку от составных типов и всегда перепроверяйте результаты работы функций с потенциальными значениями NULL!

← На главную