Рассмотрим достаточно распространенную задачу, с которой часто сталкиваются разработчики и аналитики 1С: как вывести список покупателей, у которых в каждом месяце выбранного периода (например, за весь 2019 год) была совершена хотя бы одна покупка. Постановка задачи кажется простой, но реализация запроса имеет ряд нюансов, от корректного подсчета уникальных периодов до оптимизации производительности.
Разберем по шагам, как правильно подойти к этой задаче, проанализируем различные методы решения и выясним причину, по которой некоторые прямые подходы работают медленно.
Для начала проанализируем самую очевидную логику: нам нужно сгруппировать данные по покупателю и посчитать количество уникальных месяцев, в которых у него были документы продаж. Если за год набралось ровно 12 уникальных месяцев с покупками, значит, этот клиент совершал покупки каждый месяц.
Посмотрим на пример базовой структуры запроса:
ВЫБРАТЬ
Т.Контрагент КАК Покупатель
ИЗ
ТаблицаПродаж КАК Т
ГДЕ
Т.Период МЕЖДУ &НачалоГода И &КонецГода
СГРУППИРОВАТЬ ПО
Т.Контрагент
ИМЕЮЩИЕ
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Т.ПериодМесяц) = 12
В этом коде ключевую роль играет конструкция КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ...). Она гарантирует, что даже если в одном месяце клиент сделал 10 покупок, этот месяц посчитается только один раз. Условие ИМЕЮЩИЕ отсеет всех контрагентов, у которых итоговое количество уникальных месяцев будет меньше требуемого.
Частая ошибка начинающих разработчиков — попытка считать уникальные значения напрямую по полю Период или Дата документа. Поле Дата содержит данные вплоть до секунды. Если мы применим к нему группировку, система посчитает количество различных секунд (или дней), а не месяцев. В результате клиент, купивший товар 12 раз в один месяц, попадет в отчет, что нарушает условие задачи.
Для корректного приведения даты к началу месяца воспользуемся встроенной функцией языка запросов НАЧАЛОПЕРИОДА. Разберем, как это выглядит на практике:
ВЫБРАТЬ
Счет_90.Регистратор.Покупатель КАК Покупатель
ИЗ
РегистрНакопления.Счет_90 КАК Счет_90
ГДЕ
Счет_90.Период МЕЖДУ &НачалоГода И &КонецГода
СГРУППИРОВАТЬ ПО
Счет_90.Регистратор.Покупатель
ИМЕЮЩИЕ
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ НАЧАЛОПЕРИОДА(Счет_90.Период, МЕСЯЦ)) = 12
Проанализируем ситуацию с производительностью. Если мы строим отчет за целый год, обращение к таблицам самих документов (например, Документ.РасходнаяНакладная) приведет к существенному замедлению работы программы. Таблицы документов не оптимизированы для построения сложных аналитических выборок за большие периоды.
Рассмотрим более грамотный подход: использование виртуальных таблиц оборотов регистров накопления, например РегистрНакопления.Продажи.Обороты. Платформа аппаратно оптимизирует работу с такими таблицами. Более того, если мы укажем параметр периодичности Месяц при обращении к виртуальной таблице, система автоматически создаст поле Период, которое уже будет содержать готовую дату начала месяца.
Посмотрим на пример оптимизированного запроса:
ВЫБРАТЬ
ПродажиОбороты.Контрагент КАК Покупатель
ИЗ
РегистрНакопления.Продажи.Обороты(&НачалоГода, &КонецГода, Месяц, ) КАК ПродажиОбороты
СГРУППИРОВАТЬ ПО
ПродажиОбороты.Контрагент
ИМЕЮЩИЕ
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ПродажиОбороты.Период) = 12
Этот код будет выполняться в несколько раз быстрее, так как платформа берет уже агрегированные итоги из базы данных.
В разработке на платформе 1С хардкод конкретных чисел (в нашем случае числа 12) считается крайне плохой практикой. Если маркетологу понадобится сформировать отчет не за год, а за квартал или полугодие, запрос с жестко заданным числом перестанет работать корректно.
Разберем способы динамического определения количества месяцев:
&КоличествоМесяцев.РАЗНОСТЬДАТ.Пример динамического условия:
ИМЕЮЩИЕ
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ПродажиОбороты.Период) = РАЗНОСТЬДАТ(&НачалоГода, &КонецГода, МЕСЯЦ) + 1
Рассмотрим более продвинутый архитектурный способ. В некоторых сложных задачах удобнее отталкиваться от "эталонной" таблицы периодов. В 1С для этого отлично подходит РегистрСведений.РегламентированныйПроизводственныйКалендарь или заранее сформированная временная таблица со всеми месяцами нужного интервала.
Суть алгоритма:
ЛЕВОЕ СОЕДИНЕНИЕ нашей таблицы периодов с таблицей продаж по каждому покупателю.NULL), такой контрагент отсеивается из итоговой таблицы.Такой подход делает логику прозрачнее, особенно если в дальнейшем понадобится выводить не только тех, кто покупал каждый месяц, но и показывать пропуски (в какие именно месяцы клиент ничего не купил).
Проанализируем саму постановку задачи с точки зрения бизнес-логики. Поиск клиентов, которые совершают покупки строго каждый месяц без единого пропуска, — задача весьма узкая и жесткая. На практике таким образом отсеивается подавляющее большинство покупателей, включая тех, кто делает стабильные покупки раз в полтора месяца.
В современных конфигурациях 1С (таких как ERP, Комплексная автоматизация, Управление торговлей) для маркетологов существует встроенный механизм — XYZ-анализ. Система сама вычисляет коэффициент вариации и классифицирует клиентов:
Использование встроенного отчета по XYZ-анализу часто полностью закрывает реальную потребность маркетологов в поиске «постоянных» клиентов без необходимости написания сложных самописных запросов.