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

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

При разработке отчетов и обработок в 1С нередко возникает задача: есть таблица с контрольными (граничными) датами и детальная таблица с ежедневными данными и числовыми показателями. Требуется распределить показатели второй таблицы по интервалам, заданным первой таблицей, и получить итоговые суммы для каждой граничной даты.

Проанализируем ситуацию на конкретном примере. Имеются две таблицы:

  1. Таблица 1 (границы интервалов): 15.02.2017, 20.02.2017.
  2. Таблица 2 (детальные данные): строки с 14.02.2017 по 20.02.2017 со значениями 1 в каждой строке.

В результате мы должны получить:

  1. Для даты 15.02.2017 — сумму записей за 14.02.2017 и 15.02.2017 (итог: 2).
  2. Для даты 20.02.2017 — сумму записей с 16.02.2017 по 20.02.2017 включительно (итог: 5).

Выясним причину, почему эта задача вызывает затруднения: в языке запросов платформы 1С:Предприятие отсутствуют стандартные оконные функции SQL (такие как LAG, LEAD или OVER (PARTITION BY...)). Поэтому расчет интервалов и накопительных сумм строится через группировки и соединения по неравенству. Рассмотрим подробнее несколько способов решения этой задачи.

Способ 1. Формирование интервалов через поиск предыдущей даты (рекомендуемый)

Разберем по шагам наиболее универсальный подход в запросе. Чтобы распределить детальные записи по интервалам, для каждой даты из первой таблицы необходимо определить дату, предшествующую ей. Тогда каждая контрольная дата сформирует полуинтервал: (ПредыдущаяДата, ТекущаяДата].

  1. Поместим исходные данные во временные таблицы ВТ_Таблица1 и ВТ_Таблица2 и обязательно создадим индексы по полям дат.
  2. Соединим первую таблицу саму с собой левым соединением по условию строгого неравенства: ТаблицаПредыдущих.Дата < Таблица1.Дата.
  3. Сгруппируем данные по текущей дате и найдем МАКСИМУМ(ТаблицаПредыдущих.Дата). Для самой первой даты интервала предыдущей даты не найдется, поэтому функция вернет NULL, который мы заменим на минимальную дату через функцию ЕСТЬNULL.
  4. Соединим полученную таблицу интервалов с детальной таблицей по условию попадания даты внутрь отрезка и сгруппируем результат с суммированием показателя.

Посмотрим на пример текста запроса, реализующего данный алгоритм:


ВЫБРАТЬ
    Таблица1.Дата КАК ДатаКонца,
    ЕСТЬNULL(МАКСИМУМ(ТаблицаПредыдущих.Дата), ДАТАВРЕМЯ(1, 1, 1)) КАК ДатаНачала
ПОМЕСТИТЬ ВТ_Интервалы
ИЗ
    ВТ_Таблица1 КАК Таблица1
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Таблица1 КАК ТаблицаПредыдущих
        ПО (ТаблицаПредыдущих.Дата < Таблица1.Дата)
СГРУППИРОВАТЬ ПО
    Таблица1.Дата
ИНДЕКСИРОВАТЬ ПО
    ДатаНачала,
    ДатаКонца
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Интервалы.ДатаКонца КАК Дата,
    ЕСТЬNULL(СУММА(Таблица2.Значение), 0) КАК Значение
ИЗ
    ВТ_Интервалы КАК Интервалы
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Таблица2 КАК Таблица2
        ПО (Таблица2.Дата > Интервалы.ДатаНачала)
            И (Таблица2.Дата <= Интервалы.ДатаКонца)
СГРУППИРОВАТЬ ПО
    Интервалы.ДатаКонца
УПОРЯДОЧИТЬ ПО
    Дата

Такой подход нагляден, легко отлаживается и позволяет однозначно определить границы периодов любой длительности, включая неравномерные шаги дат.

Способ 2. Вычисление через разность накопительных итогов

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

Разберем по шагам расчет на числовом примере:

  1. Накопительный итог на 15.02.2017: сумма за 14.02 и 15.02 равна 2.
  2. Накопительный итог на 20.02.2017: сумма с 14.02 по 20.02 равна 7.
  3. Сумма в интервале для даты 20.02.2017 вычисляется как разность: 7 − 2 = 5.

Посмотрим, как выглядит реализация расчета накопительного итога в запросе 1С:


ВЫБРАТЬ
    Таблица1.Дата КАК Дата,
    ЕСТЬNULL(СУММА(Таблица2.Значение), 0) КАК НакопленныйИтог
ПОМЕСТИТЬ ВТ_Накопления
ИЗ
    ВТ_Таблица1 КАК Таблица1
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Таблица2 КАК Таблица2
        ПО (Таблица2.Дата <= Таблица1.Дата)
СГРУППИРОВАТЬ ПО
    Таблица1.Дата
ИНДЕКСИРОВАТЬ ПО
    Дата
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Текущие.Дата КАК Дата,
    Текущие.НакопленныйИтог - ЕСТЬNULL(МАКСИМУМ(Предыдущие.НакопленныйИтог), 0) КАК Значение
ИЗ
    ВТ_Накопления КАК Текущие
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Накопления КАК Предыдущие
        ПО (Предыдущие.Дата < Текущие.Дата)
СГРУППИРОВАТЬ ПО
    Текущие.Дата,
    Текущие.НакопленныйИтог
УПОРЯДОЧИТЬ ПО
    Дата

Особенности производительности соединений по неравенству

Проанализируем важный технический аспект работы СУБД с такими запросами. Соединения по условиям <=, < или МЕЖДУ выполняются значительно медленнее, чем эквисоединения по точному равенству (=). СУБД не может применить алгоритм соединений хешированием (Hash Match) и вынуждена использовать соединения вложенными циклами (Nested Loops) или слиянием (Merge Join), что при больших объемах данных приводит к резкому росту нагрузки на процессор и диск.

Чтобы избежать падения производительности, соблюдайте следующие правила:

  1. Обязательно индексируйте временные таблицы: конструкция ИНДЕКСИРОВАТЬ ПО ПолеДата позволяет оптимизатору СУБД использовать поиск по индексу вместо сканирования всей таблицы целиком.
  2. Ограничивайте выборку: отсекайте записи за пределами максимального и минимального периодов до выполнения соединений по неравенству.

Способ 3. Программный обход во встроенном языке (алгоритм двух указателей)

Если обе таблицы уже получены во встроенном языке (например, хранятся в объектах ТаблицаЗначений) и содержат сотни тысяч строк, то выполнять самосоединения по неравенству в запросе неэффективно. Гораздо быстрее обработать такие массивы алгоритмом за один линейный проход.

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

  1. Сортируем обе таблицы по возрастанию даты с помощью метода Сортировать("Дата Возр").
  2. Используем два индекса: один указывает на текущую строку первой таблицы, второй — на текущую строку второй таблицы.
  3. Пока дата второй таблицы меньше либо равна дате текущей контрольной точки, накапливаем сумму и сдвигаем указатель второй таблицы.
  4. Как только дата превышает границу, фиксируем результат, переходим к следующей контрольной точке и продолжаем суммирование.

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


// Подготовим таблицы к последовательному обходу
Таблица1.Сортировать("Дата Возр");
Таблица2.Сортировать("Дата Возр");

ТаблицаРезультат = Новый ТаблицаЗначений;
ТаблицаРезультат.Колонки.Добавить("Дата", Новый ОписаниеТипов("Дата"));
ТаблицаРезультат.Колонки.Добавить("Значение", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));

ИндексТ2 = 0;
ВсегоСтрокТ2 = Таблица2.Количество();

Для Каждого СтрТ1 Из Таблица1 Цикл
    ТекущаяСумма = 0;
    
    Пока ИндексТ2 < ВсегоСтрокТ2 Цикл
        СтрТ2 = Таблица2[ИндексТ2];
        Если СтрТ2.Дата <= СтрТ1.Дата Тогда
            ТекущаяСумма = ТекущаяСумма + СтрТ2.Значение;
            ИндексТ2 = ИндексТ2 + 1;
        Иначе
            Прервать;
        КонецЕсли;
    КонецЦикла;
    
    НовСтр = ТаблицаРезультат.Добавить();
    НовСтр.Дата = СтрТ1.Дата;
    НовСтр.Значение = ТекущаяСумма;
КонецЦикла;

Такой программный обход выполняется за время O(N + M), где N и M — количество строк в исходных таблицах, что исключает декартово умножение и гарантирует мгновенную обработку больших коллекций в оперативной памяти.

Способ 4. Использование Системы компоновки данных (СКД)

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

← На главную