Как в запросе 1С найти клиентов, которые купили все товары из заданного списка

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

Задача поиска контрагентов, которые приобрели определенный «набор» товаров, является классическим примером реляционного деления. В 1С это реализуется через механизмы работы с временными таблицами, агрегатными функциями и условиями группировки. Проанализируем, как решить эту задачу наиболее эффективно и элегантно, учитывая, что список товаров может быть динамическим, а сами товары имеют характеристики.

Метод сравнения количества (универсальный подход)

Наиболее производительным и понятным способом в системе 1С:Предприятие считается алгоритм сравнения количества купленных уникальных позиций с количеством позиций в эталонном списке. Разберем этот алгоритм по шагам.

Шаг 1: Подготовка эталонного списка

Прежде всего, нам необходимо передать в запрос список пар «Номенклатура — Характеристика», которые мы ищем. Лучше всего сделать это через таблицу значений, которую мы поместим во временную таблицу. Проанализируем код подготовки данных:


// Предположим, у нас есть МассивТоваров, где каждый элемент - структура с Номенклатурой и Характеристикой
ТЗ_Эталон = Новый ТаблицаЗначений;
ТЗ_Эталон.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТЗ_Эталон.Колонки.Добавить("Характеристика", Новый ОписаниеТипов("СправочникСсылка.ХарактеристикиНоменклатуры"));

// Заполняем ТЗ данными
Для Каждого Стр Из МассивТоваров Цикл
    НоваяСтрока = ТЗ_Эталон.Добавить();
    ЗаполнитьЗначенияСвойств(НоваяСтрока, Стр);
КонецЦикла;

// Получаем количество уникальных позиций для сравнения
КоличествоЭталонныхТоваров = ТЗ_Эталон.Количество();

Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.УстановитьПараметр("ТЗ_Эталон", ТЗ_Эталон);
Запрос.УстановитьПараметр("КоличествоЭталона", КоличествоЭталонныхТоваров);
Запрос.УстановитьПараметр("НачалоПериода", НачалоПериода);
Запрос.УстановитьПараметр("КонецПериода", КонецПериода);

Шаг 2: Реализация запроса

В самом запросе мы сначала поместим эталон во временную таблицу и проиндексируем её. Это критически важно для производительности при соединении с большими таблицами оборотов.


ВЫБРАТЬ
    ТЗ_Эталон.Номенклатура КАК Номенклатура,
    ТЗ_Эталон.Характеристика КАК Характеристика
ПОМЕСТИТЬ ВТ_Эталон
ИЗ
    &ТЗ_Эталон КАК ТЗ_Эталон
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ПродажиОбороты.Клиент КАК Клиент
ИЗ
    РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, , ) КАК ПродажиОбороты
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Эталон КАК ВТ_Эталон
        ПО ПродажиОбороты.Номенклатура = ВТ_Эталон.Номенклатура
            И ПродажиОбороты.Характеристика = ВТ_Эталон.Характеристика
ГРУППИРОВАТЬ ПО
    ПродажиОбороты.Клиент
ИМЕЮЩИЕ
    КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ПродажиОбороты.Номенклатура.Ссылка + ПродажиОбороты.Характеристика.Ссылка) = &КоличествоЭталона

Разберем, почему мы используем именно такие конструкции:

  1. Внутреннее соединение: Оно сразу отсекает все продажи клиента, которые не входят в наш эталонный список.
  2. Группировка по клиенту: Нам нужно схлопнуть все записи о продажах до уникального покупателя.
  3. КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ...): Это ключевой момент. Если клиент покупал один и тот же Товар№1 пять раз, обычное КОЛИЧЕСТВО вернет 5, и условие не сработает. Нам же важно, сколько видов товара из списка он купил.
  4. ИМЕЮЩИЕ: Это фильтр уже по результатам агрегатных функций. Мы сравниваем число уникальных купленных позиций с числом позиций в нашем фильтре.

Метод весовых коэффициентов (Показатель)

Рассмотрим альтернативный вариант, упомянутый в обсуждении. Он часто применялся в версии 7.7, но может быть полезен и сейчас для специфических задач. Каждому товару из списка присваивается уникальный вес (или просто число 1). Однако, чтобы избежать ложных срабатываний при повторных покупках одного товара, этот метод требует предварительной очистки данных.

Проанализируем ситуацию: если мы просто будем суммировать ВЫБОР КОГДА, то клиент, купивший трижды первый товар и ни разу остальные, может получить ту же сумму, что и клиент, купивший все три товара. Поэтому в 8-й версии 1С лучше придерживаться метода с КОЛИЧЕСТВО(РАЗЛИЧНЫЕ).

Как найти клиентов, купивших ТОЛЬКО эти товары?

Иногда задача стоит жестче: найти тех, кто купил ровно эти три товара и больше ничего. Для этого нам нужно расширить наше условие ИМЕЮЩИЕ. Мы добавим сравнение количества купленных товаров из эталона с общим количеством уникальных товаров, купленных этим клиентом за период.


ИМЕЮЩИЕ
    КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ПродажиОбороты.Номенклатура) = &КоличествоЭталона
    И КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ПродажиОбороты.Номенклатура) = (
        ВЫБРАТЬ КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Пр.Номенклатура)
        ИЗ РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, , ) КАК Пр
        ГДЕ Пр.Клиент = ПродажиОбороты.Клиент
    )

Второй вариант реализации "только указанные" — использование левого соединения с проверкой на NULL для всех товаров, не входящих в список, но это значительно утяжелит запрос.

Оптимизация производительности

При работе с реальными данными объемом в сотни тысяч записей, следует учитывать несколько факторов:

Выясним причину, почему это решение называют "классическим". Оно демонстрирует умение программиста работать с множествами. Вместо того чтобы перебирать записи в цикле или строить сложные цепочки ВНУТРЕННЕЕ СОЕДИНЕНИЕ под каждый товар (что сделало бы невозможным динамическую работу со списком товаров), мы используем мощь группировок SQL.

Рассмотрим подробнее рекомендации из "Минимализмов" (пункты 53, 64). Они советуют всегда сводить задачи пересечения множеств к агрегации. Это не только сокращает объем кода, но и делает его масштабируемым: вашему запросу будет все равно, ищет он 3 товара или 30, время выполнения изменится незначительно.

← На главную