В процессе бизнес-анализа продаж часто возникает задача классификации клиентов. Классическим примером является проведение ABC-анализа, при котором нам необходимо выделить группу клиентов (обычно классы «А» и «В»), обеспечивающую основную долю выручки — например, 85%. Сложность заключается в том, что стандартный язык запросов 1С не поддерживает классические оконные функции SQL (такие как SUM() OVER()), позволяющие легко рассчитать нарастающий итог (кумулятивную сумму).
В этой статье мы подробно рассмотрим несколько практических способов решения данной задачи: от классического и ресурсоемкого самосоединения в запросе до элегантных решений средствами компоновки данных (СКД) и постобработки в коде.
Если перед нами стоит жесткое требование решить задачу исключительно в рамках одного запроса (без использования кода и СКД), единственным рабочим механизмом в 1С является левое или внутреннее соединение таблицы с самой собой.
Давайте разберем этот алгоритм по шагам:
ВТ_Продажи), в которой группируем продажи по контрагентам и рассчитываем сумму выручки.Посмотрим на пример практической реализации такого запроса:
ВЫБРАТЬ
ПродажиОбороты.Контрагент КАК Контрагент,
ПродажиОбороты.СуммаВыручкиОборот КАК Сумма
ПОМЕСТИТЬ ВТ_Продажи
ИЗ
РегистрНакопления.ВыручкаИСебестоимостьПродаж.Обороты(&НачалоПериода, &КонецПериода, , ) КАК ПродажиОбороты
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Т1.Контрагент КАК Контрагент,
Т1.Сумма КАК Сумма,
СУММА(Т2.Сумма) КАК НарастающийИтог
ПОМЕСТИТЬ ВТ_НарастающийИтог
ИЗ
ВТ_Продажи КАК Т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Продажи КАК Т2
ПО (ВЫБОР
КОГДА Т1.Сумма = Т2.Сумма
ТОГДА Т1.Контрагент <= Т2.Контрагент
ИНАЧЕ Т1.Сумма < Т2.Сумма
КОНЕЦ)
ГРУППИРОВАТЬ ПО
Т1.Контрагент,
Т1.Сумма
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_НарастающийИтог.Контрагент КАК Контрагент,
ВТ_НарастающийИтог.Сумма КАК Сумма,
ВТ_НарастающийИтог.НарастающийИтог КАК НарастающийИтог
ИЗ
ВТ_НарастающийИтог КАК ВТ_НарастающийИтог
ГДЕ
ВТ_НарастающийИтог.НарастающийИтог <= (ВЫБРАТЬ СУММА(Сумма) * 0.85 ИЗ ВТ_Продажи)
УПОРЯДОЧИТЬ ПО
Сумма УБЫВ
Важное предупреждение: Данный метод имеет квадратичную вычислительную сложность $O(N^2)$. Если в вашей исходной таблице 1000 контрагентов, СУБД выполнит до 1 000 000 операций сравнения строк, а результирующий набор данных разрастется до огромных масштабов в оперативной памяти. Для больших баз данных этот метод категорически не подходит, так как вызовет перегрузку сервера.
Если задача выполняется в рамках внешней обработки, отчета или фонового задания, наиболее оптимальным с точки зрения производительности и потребления памяти будет выгрузка первоначального запроса в ТаблицуЗначений с последующей быстрой итерацией в цикле.
Выясним причину эффективности этого подхода: сортировка таблицы значений в оперативной памяти и линейный проход по ней занимают ничтожно мало времени (сложность $O(N \log N)$ на сортировку и $O(N)$ на обход). Проанализируем пример кода:
// Шаг 1. Получаем список продаж, отсортированный по убыванию выручки
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПродажиОбороты.Контрагент КАК Контрагент,
| ПродажиОбороты.СуммаВыручкиОборот КАК Сумма
|ИЗ
| РегистрНакопления.ВыручкаИСебестоимостьПродаж.Обороты(&НачалоПериода, &КонецПериода) КАК ПродажиОбороты
|УПОРЯДОЧИТЬ ПО
| Сумма УБЫВ";
ТаблицаПродаж = Запрос.Выполнить().Выгрузить();
// Шаг 2. Рассчитываем порог в 85% от общей суммы
ОбщаяВыручка = ТаблицаПродаж.Итог("Сумма");
ПорогВыручки = ОбщаяВыручка * 0.85;
СписокТопКонтрагентов = Новый Массив;
ТекущаяСумма = 0;
// Шаг 3. В цикле собираем контрагентов до достижения порога
Для Каждого СтрокаПродаж Из ТаблицаПродаж Цикл
ТекущаяСумма = ТекущаяСумма + СтрокаПродаж.Сумма;
СписокТопКонтрагентов.Добавить(СтрокаПродаж.Контрагент);
Если ТекущаяСумма >= ПорогВыручки Тогда
Прервать; // Цель достигнута, останавливаем цикл
КонецЕсли;
КонецЦикла;
// Шаг 4. Теперь массив "СписокТопКонтрагентов" можно передать в следующий запрос
// Например, для поиска потерянных клиентов в текущем году
Система компоновки данных (СКД) предоставляет разработчику мощный функционал постобработки данных на стороне платформы без использования тяжелых запросов. Для расчета нарастающего итога мы можем использовать встроенную функцию СКД ВычислитьВыражение.
Разберем по шагам, как настроить расчет кумулятивной суммы в СКД:
СуммаНарастающимИтогом. В качестве выражения прописываем:
ВычислитьВыражение("Сумма(СуммаВыручки)", , "Группировка", "Первая", "Текущая")
ПроцентОтВыручки) со следующим выражением:
Сумма(СуммаВыручки) / ВычислитьВыражение("Сумма(СуммаВыручки)", , "ОбщийИтог") * 100
ПроцентОтВыручки <= 85.Этот способ работает чрезвычайно быстро на уровне платформы в оперативной памяти и абсолютно безопасен для производительности СУБД.
Довольно часто, выделив группу контрагентов, формирующих 85% выручки за прошлый год, бизнес-аналитики хотят сопоставить их с текущим периодом и найти тех клиентов, которые перестали покупать у нас в текущем году (так называемый анализ оттока клиентов).
Имея на руках массив топ-контрагентов прошлого года (полученный, например, вторым способом через Таблицу Значений), мы можем легко решить эту задачу с помощью левого соединения в запросе:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТопПрошлогоГода.Ссылка КАК Контрагент
|ПОМЕСТИТЬ ВТ_ТопПрошлогоГода
|ИЗ
| &МассивТопКонтрагентов КАК ТопПрошлогоГода // Передаем наш массив
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_ТопПрошлогоГода.Контрагент КАК ПотерянныйКонтрагент,
| ЕСТЬNULL(ПродажиТекущегоГода.СуммаОборот, 0) КАК ПродажиТекущегоГода
|ИЗ
| ВТ_ТопПрошлогоГода КАК ВТ_ТопПрошлогоГода
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ВыручкаИСебестоимостьПродаж.Обороты(&НачалоТекущегоГода, &КонецТекущегоГода) КАК ПродажиТекущегоГода
| ПО ВТ_ТопПрошлогоГода.Контрагент = ПродажиТекущегоГода.Контрагент
|ГДЕ
| ПродажиТекущегоГода.Контрагент ИС ССЫЛКА // Выбираем только тех, у кого нет продаж в текущем периоде
| ИЛИ ЕСТЬNULL(ПродажиТекущегоГода.СуммаОборот, 0) = 0";
Запрос.УстановитьПараметр("МассивТопКонтрагентов", СписокТопКонтрагентов);
Запрос.УстановитьПараметр("НачалоТекущегоГода", НачалоТекущегоГода);
Запрос.УстановитьПараметр("КонецТекущегоГода", КонецТекущегоГода);
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
Для реализации ABC-анализа и расчета нарастающих итогов в системе 1С:
ВычислитьВыражение в СКД, если строится интерактивный пользовательский отчет.