При разработке отчетов в Системе компоновки данных (СКД) программисты часто сталкиваются с задачей анализа хронологической последовательности событий. Классический пример — необходимость получить все продажи или документы контрагента, которые были оформлены строго после его самой первой покупки (или любого другого ключевого события). Попытки решить эту задачу «в лоб» через простые параметры или стандартные функции работы с датами в выражениях СКД часто приводят к ошибкам или неработающим условиям.
В этой статье мы подробно и по шагам разберем четыре эффективных способа реализации этой задачи. Мы начнем с классического метода запросов и закончим современными возможностями платформы «1С:Предприятие 8», такими как связи наборов данных и аналитические (оконные) функции.
Это классический и наиболее универсальный метод, который будет работать на любых версиях платформы 1С. Суть подхода заключается в том, что мы сначала определяем дату ключевого события (например, минимальную дату покупки для каждого клиента) в первом подзапросе, помещаем этот результат во временную таблицу, индексируем её, а затем соединяем с основной таблицей продаж по условию превышения даты.
Проанализируем этот запрос по шагам. Сначала создадим пакетный запрос:
// Шаг 1. Получаем дату первого события (минимальную дату продажи) для каждого контрагента
ВЫБРАТЬ
Продажи.Контрагент КАК Контрагент,
МИНИМУМ(Продажи.Период) КАК ДатаПервойПокупки
ПОМЕСТИТЬ ВТ_ПерваяПокупка
ИЗ
РегистрНакопления.Продажи КАК Продажи
ГДЕ
Продажи.Период МЕЖДУ &НачалоПериода И &КонецПериода
СГРУППИРОВАТЬ ПО
Продажи.Контрагент
ИНДЕКСИРОВАТЬ ПО
Контрагент,
ДатаПервойПокупки
;
////////////////////////////////////////////////////////////////////////////////
// Шаг 2. Выбираем последующие продажи, которые произошли позже полученной даты
ВЫБРАТЬ
Продажи.Контрагент КАК Контрагент,
Продажи.Период КАК Период,
Продажи.Номенклатура КАК Номенклатура,
Продажи.СуммаОборот КАК Сумма
ИЗ
РегистрНакопления.Продажи КАК Продажи
// Используем внутреннее соединение, чтобы исключить клиентов без покупок
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ПерваяПокупка КАК ВТ_ПерваяПокупка
ПО Продажи.Контрагент = ВТ_ПерваяПокупка.Контрагент
И Продажи.Период > ВТ_ПерваяПокупка.ДатаПервойПокупки
Рассмотрим важные нюансы оптимизации этого решения:
МИНИМУМ, чтобы гарантировать получение ровно одной (самой ранней) даты для каждого контрагента.ИНДЕКСИРОВАТЬ ПО для полей связи. Это существенно ускорит выполнение запроса СУБД на реальных базах данных.Продажи.Period > ВТ_ПерваяПокупка.ДатаПервойПокупки. Если в вашей базе документы могут проводиться в одну и ту же секунду и вам нужно исключить именно самый первый документ, то сравнение «больше» отработает корректно. Если же вы хотите сравнивать только по дням без учета времени, используйте функцию НАЧАЛОПЕРИОДА для обеих дат.Система компоновки данных предоставляет мощный встроенный механизм — связи наборов данных. Вместо создания одного сложного SQL-запроса мы можем разделить логику на два простых, независимых набора данных (запроса), а СКД сама выполнит их соединение и передаст дату как параметр.
Разберем, как это настроить:
ВЫБРАТЬ
Продажи.Контрагент КАК Контрагент,
МИНИМУМ(Продажи.Период) КАК ДатаПервойПокупки
ИЗ
РегистрНакопления.Продажи КАК Продажи
СГРУППИРОВАТЬ ПО
Продажи.Контрагент
ВЫБРАТЬ
Продажи.Контрагент КАК Контрагент,
Продажи.Период КАК Период,
Продажи.СуммаОборот КАК Сумма
ИЗ
РегистрНакопления.Продажи КАК Продажи
ГДЕ
Продажи.Контрагент = &Контрагент
И Продажи.Период > &ДатаПервойПокупки
Контрагент источника с параметром &Контрагент приемника.ДатаПервойПокупки источника с параметром &ДатаПервойПокупки приемника.Почему важен флаг «Список параметров»? Если этот флаг не установлен, платформа будет выполнять запрос-приемник отдельно для каждого контрагента (в цикле). Это критически снизит производительность отчета. При установленном флаге СКД передаст список контрагентов и дат единым пакетом, что отработает мгновенно.
Если разработка ведется на современных версиях платформы «1С:Предприятие 8» (начиная с релиза 8.3.20), то в языке запросов стал доступен стандарт SQL-оконных функций. Это позволяет нам полностью отказаться от классического соединения таблиц и группировок, вычислив минимальную дату «на лету» в каждой строке.
Посмотрим на пример реализации такого запроса:
ВЫБРАТЬ
Продажи.Контрагент КАК Контрагент,
Продажи.Период КАК Период,
Продажи.СуммаОборот КАК Сумма,
// Вычисляем минимальную дату в разрезе контрагента прямо в строке выборки
МИНИМУМ(Продажи.Период) OVER (PARTITION BY Продажи.Контрагент) КАК ДатаПервойПокупки
ПОМЕСТИТЬ ВТ_ПродажиСДатойПервойПокупки
ИЗ
РегистрНакопления.Продажи КАК Продажи
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ.Контрагент,
ВТ.Период,
ВТ.Сумма
ИЗ
ВТ_ПродажиСДатойПервойПокупки КАК ВТ
ГДЕ
ВТ.Период > ВТ.ДатаПервойПокупки
В данном случае конструкция OVER (PARTITION BY ...) указывает СУБД, что нужно рассчитать минимальное значение поля Период для каждого конкретного контрагента, не сворачивая при этом строки в группировку. Во втором запросе пакета нам остается лишь отфильтровать строки, где текущий период больше полученного минимального.
Бывают ситуации, когда нам не нужно физически отсекать строки запросом, а требуется вывести полную картину продаж, но при этом рассчитать сумму продаж клиента, совершенных строго после первой сделки (например, для вычисления нарастающего итога или процента удержания клиента). Эту задачу можно переложить на ресурсы СКД с помощью функции ВычислитьВыражение.
Выясним, как это настроить в схеме компоновки данных:
Сумма пишем выражение, которое будет рассчитывать нарастающий итог, начиная со второй записи в группировке (то есть после первой покупки):
ВычислитьВыражение("Сумма(Сумма)", "Период", , "Вторая", "Текущая", "Контрагент")
Параметры функции ВычислитьВыражение в данном случае означают следующее:
"Сумма(Сумма)" — выражение, которое рассчитывается."Период" — поле, по которому упорядочиваются данные для расчета."Вторая" — граница начала расчета (мы пропускаем первую запись, которая является первой покупкой)."Текущая" — конечная граница расчета (текущая строка отчета)."Контрагент" — имя группировки, в рамках которой происходят вычисления (при изменении контрагента расчет начнется заново).Каждый из рассмотренных способов имеет свои преимущества в зависимости от условий задачи: