При разработке отчетов и обработок в системе 1С:Предприятие часто возникает задача анализа динамики данных. Одной из таких задач является вычисление интервала времени (например, просрочки) между двумя последовательными событиями — текущим и предыдущим платежом. Поскольку в языке запросов (до определенных версий платформы) и в стандартной выборке нет прямого обращения к "соседней" записи, разработчику приходится применять специальные алгоритмы.
В этой статье мы подробно разберем несколько способов решения этой задачи: от классического перебора выборки в цикле до использования современных возможностей платформы, таких как оконные функции и система компоновки данных (СКД).
Рассмотрим наиболее простой и понятный метод, который можно применить при программном выводе данных в макет или таблицу значений. Основная идея заключается в том, чтобы на каждой итерации цикла сохранять текущее значение даты в специальную переменную, которая станет "предыдущей" для следующего шага.
Проанализируем алгоритм реализации этого метода:
ПредыдущаяДата до начала цикла и инициализируем её значением Неопределено или пустой датой.ПредыдущаяДата значение текущей даты.Пример программного кода:
ПредыдущаяДата = Неопределено;
Пока Выборка.Следующий() Цикл
Если ПредыдущаяДата = Неопределено Тогда
// Это первая строка, сравнивать не с чем
РазностьВДнях = 0;
Иначе
// В 1С разность дат возвращается в секундах
// Делим на 86 400 (количество секунд в сутках)
РазностьВДнях = (Выборка.ДатаПлатежа - ПредыдущаяДата) / 86400;
КонецЕсли;
// Выводим результат в область макета
ОбластьСтрока.Параметры.ДатаПлатежа = Выборка.ДатаПлатежа;
ОбластьСтрока.Параметры.Просрочка = РазностьВДнях;
ТабДок.Вывести(ОбластьСтрока);
// Запоминаем текущую дату для следующего шага цикла
ПредыдущаяДата = Выборка.ДатаПлатежа;
КонецЦикла;
Обратите внимание, что при простом вычитании Дата1 - Дата2 результат всегда получается в секундах. Чтобы получить целое количество дней, можно использовать функцию Цел() или Окр().
Если вы строите отчет с использованием СКД, то писать код в цикле не требуется. В системе предусмотрены мощные функции для работы с наборами данных. Для обращения к предыдущей записи используется функция ВычислитьВыражение.
Рассмотрим, как настроить такое поле в схеме компоновки данных:
РазницаДней.РазностьДат(ВычислитьВыражение("ДатаПлатежа", , , "Предыдущая", "Предыдущая"), ДатаПлатежа, "День")
Разберем подробнее параметры функции ВычислитьВыражение в данном контексте:
"ДатаПлатежа" — имя поля, значение которого мы хотим получить."Предыдущая") указывают системе, что нужно взять значение именно из предыдущей строки относительно текущей.Этот метод является наиболее предпочтительным для отчетов, так как он корректно работает с группировками, иерархией и сортировками, которые пользователь может настроить самостоятельно.
Для разработчиков, использующих современные версии платформы (начиная с 8.3.20), доступен наиболее эффективный способ — оконные функции прямо в языке запросов. Это позволяет переложить расчеты на сторону СУБД, что значительно ускоряет выполнение при больших объемах данных.
Проанализируем, как выглядит такой запрос:
ВЫБРАТЬ
Продажи.Дата КАК ТекущаяДата,
ПРЕДЫДУЩЕЕ(Продажи.Дата) КАК ПредыдущаяДата,
РАЗНОСТЬДАТ(ПРЕДЫДУЩЕЕ(Продажи.Дата), Продажи.Дата, ДЕНЬ) КАК РазницаДней
ИЗ
Документ.РеализацияТоваровУслуг КАК Продажи
УПОРЯДОЧИТЬ ПО
Продажи.Дата
Использование ключевого слова ПРЕДЫДУЩЕЕ (аналог LAG в SQL) позволяет получить значение из предшествующей записи без использования циклов или сложных соединений. Выясним причину, почему это удобно: вы сразу получаете готовую таблицу с вычисленными интервалами, которую можно выгрузить в ТЗ или вывести в отчет.
Если ваша платформа не поддерживает оконные функции и вы не используете СКД, задачу можно решить классическим приемом "самосоединения" в запросе. Выясним, как это работает по шагам:
Т1.Дата > Т2.Дата с подсчетом количества записей.Т1.НомерСтроки = Т2.НомерСтроки + 1.Посмотрим на пример такого запроса:
// Сначала подготавливаем таблицу с индексами (номерами строк)
ВЫБРАТЬ
Платежи.Дата КАК Дата,
АВТОНОМЕРЗАПИСИ() КАК НомерСтроки
ПОМЕСТИТЬ ВТ_Данные
ИЗ
РегистрНакопления.Взаиморасчеты КАК Платежи;
// Соединяем таблицу с её копией со смещением индекса на 1
ВЫБРАТЬ
Т1.Дата КАК ТекущаяДата,
Т2.Дата КАК ПредыдущаяДата,
РАЗНОСТЬДАТ(Т2.Дата, Т1.Дата, ДЕНЬ) КАК ДнейМеждуПлатежами
ИЗ
ВТ_Данные КАК Т1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Данные КАК Т2 ПО Т1.НомерСтроки = Т2.НомерСтроки + 1
Важное примечание: Использование АВТОНОМЕРЗАПИСИ() доступно не во всех контекстах, иногда номер строки приходится вычислять через коррелированные подзапросы или временные таблицы, что может быть ресурсозатратно.
При реализации любого из перечисленных выше методов следует учитывать несколько важных нюансов:
РАЗНОСТЬДАТ вернет NULL при соединении или ошибку при вычитании, если не предусмотреть проверку ЕСТЬNULL в запросе или Если ЗначениеЗаполнено() в коде.НачалоДня() для обеих дат перед вычитанием.УПОРЯДОЧИТЬ ПО в запросе или используйте индексацию, иначе результат будет непредсказуемым.Подводя итог, можно сказать, что выбор метода зависит от конкретной архитектурной задачи. В простых обработках достаточно цикла с переменной, в сложных отчетах лучше использовать СКД, а для высоконагруженных систем оптимальным выбором станут оконные функции в запросах.