Как узнать количество вхождений одной таблицы значений в другую и рассчитать количество проданных наборов

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

В практике разработчика 1С часто возникает задача сопоставления двух наборов данных. Типичный пример: маркетинговая акция, при которой премия менеджеру полагается за продажу определенного «набора» товаров (например, 3 единицы одного товара, 2 единицы другого и 2 единицы третьего). Нам необходимо выяснить, сколько таких полных наборов содержится в фактической реализации. Эту задачу можно решить как программным перебором (с оптимизацией или без), так и с помощью языка запросов. Рассмотрим эти подходы подробнее.

Метод 1: Программный поиск с использованием объекта Соответствие

Если нам нужно максимально быстро сравнить две таблицы значений в памяти, не обращаясь к базе данных, использование объекта Соответствие является наиболее эффективным с точки зрения теории алгоритмов. Соответствие в 1С реализовано как хеш-таблица, что обеспечивает среднее время поиска за O(1) (константное время), в то время как обычный перебор дает O(N^2).

Проанализируем ситуацию: у нас есть ТаблицаНабора и ТаблицаПродаж. Чтобы проверить наличие каждой строки, мы сначала подготовим «индекс» в памяти. Разберем код реализации:


// Создаем хеш-таблицу для быстрого поиска
ХешТаблица = Новый Соответствие;

// Заполняем хеш на основе первой таблицы (эталонного набора)
Для Каждого СтрокаНабора Из ТаблицаНабора Цикл
    // Формируем уникальный ключ, объединяя номенклатуру и количество
    // В реальной задаче ключ может состоять только из номенклатуры
    Ключ = СтрокаНабора.Номенклатура; 
    ХешТаблица.Вставить(Ключ, СтрокаНабора.Количество);
КонецЦикла;

// Проверяем вхождения из второй таблицы (продажи)
КоличествоПолныхНаборов = 0;
Для Каждого СтрокаПродаж Из ТаблицаПродаж Цикл
    НужноеКоличество = ХешТаблица[СтрокаПродаж.Номенклатура];
    Если НужноеКоличество <> Неопределено Тогда
        // Здесь можно реализовать логику подсчета долей набора
        // Например: Доля = СтрокаПродаж.Количество / НужноеКоличество;
    КонецЕсли;
КонецЦикла;

Важный нюанс: использование Соответствия оправдано, когда одна из таблиц очень большая. Как показывают тесты (сообщения 50-51), на 100 000 строк такой метод отрабатывает за 2-3 секунды, что значительно быстрее неиндексированного поиска.

Метод 2: Использование индексов Таблицы Значений

Платформа 1С 8.3 позволяет добавлять индексы к объекту ТаблицаЗначений. Это делает метод НайтиСтроки() крайне эффективным, так как поиск начинает работать через бинарное дерево (сложность Log(N)). Рассмотрим, как применить это для нашей задачи:


// Добавляем индекс по колонке поиска
ТаблицаПродаж.Индексы.Добавить("Номенклатура");

Для Каждого СтрокаНабора Из ТаблицаНабора Цикл
    Отбор = Новый Структура("Номенклатура", СтрокаНабора.Номенклатура);
    НайденныеСтроки = ТаблицаПродаж.НайтиСтроки(Отбор);
    
    Если НайденныеСтроки.Количество() > 0 Тогда
        // Логика обработки найденных совпадений
    КонецЕсли;
КонецЦикла;

Экспериментальным путем (сообщение 73) выяснено, что индексированная таблица значений иногда может работать даже быстрее Соответствия на объемах до 100 000 строк из-за особенностей внутренней реализации платформы и меньших затрат на формирование ключей в виде строк.

Метод 3: Решение задачи через Запрос (оптимально для отчетов)

Если данные о продажах находятся в базе данных, а «акционный набор» задан в таблице на форме, самым правильным решением будет использование временных таблиц в запросе. Это позволит использовать мощь SQL-движка для вычисления количества наборов.

Алгоритм расчета «количества проданных наборов» строится на принципе «Минимум из максимумов». Для каждого товара в реализации мы считаем, сколько раз он покрывает потребность набора, а затем берем минимальное значение среди всех позиций набора в рамках документа.

Разберем пример запроса:


// 1. Помещаем таблицу набора (из формы) во временную таблицу
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос.Текст = "ВЫБРАТЬ * ПОМЕСТИТЬ ВТ_Набор ИЗ &ТЗ_Набор КАК ТЗ_Набор";
Запрос.УстановитьПараметр("ТЗ_Набор", ТаблицаНабора);
Запрос.Выполнить();

// 2. Основной расчет
Запрос.Текст = 
"ВЫБРАТЬ
|   Продажи.Ответственный КАК Менеджер,
|   Продажи.Ссылка КАК Документ,
|   // Вычисляем минимальное количество собранных наборов в чеке
|   МИНИМУМ(ЕСТЬNULL(Продажи.Количество, 0) / Набор.Количество) КАК КолвоНаборовВДокументе
|ИЗ
|   ВТ_Набор КАК Набор
|       ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК Продажи
|       ПО Набор.Номенклатура = Продажи.Номенклатура
|ГДЕ
|   Продажи.Ссылка.Дата МЕЖДУ &Начало И &Конец
|   И Продажи.Ссылка.Проведен = ИСТИНА
|СГРУППИРОВАТЬ ПО
|   Продажи.Ответственный,
|   Продажи.Ссылка
|ИМЕЮЩИЕ
|   МИНИМУМ(ЕСТЬNULL(Продажи.Количество, 0) / Набор.Количество) >= 1";

Рассмотрим подробнее логику соединения: мы используем ВНУТРЕННЕЕ СОЕДИНЕНИЕ. Если в документе продажи отсутствует хотя бы одна номенклатура из обязательного набора, такой документ (или строка) просто не попадет в результат или даст 0 при расчете минимума (если использовать ЛЕВОЕ СОЕДИНЕНИЕ). Функция МИНИМУМ гарантирует, что если менеджер продал 10 единиц одного товара (хватит на 3 набора) и только 2 единицы другого (хватит на 1 набор), то результатом по документу будет именно 1 набор.

Сравнение методов и выбор лучшего

Проанализируем ситуацию в зависимости от контекста задачи:

  1. Объем данных: Если вы работаете с отчетом за месяц по всей компании, используйте запрос (Метод 3). Выгрузка всех продаж в ТаблицуЗначений для последующей обработки кодом может привести к нехватке оперативной памяти и зависанию сервера.
  2. Сложность логики: Если условия акции сложные (например, «любые 3 товара из группы» или «взаимозаменяемые позиции»), реализовать это программным перебором (Метод 1 или 2) будет проще, чем писать громоздкий SQL-запрос.
  3. Интерактивность: Если расчет происходит «на лету» в форме при подборе товаров, лучше всего подойдет поиск по индексированной ТЗ (Метод 2), так как это обеспечит мгновенный отклик интерфейса без лишних вызовов сервера БД.

Выясним причину, по которой запросы иногда кажутся сложнее. Основная трудность заключается в правильной типизации временных таблиц. При передаче ТаблицыЗначений в запрос убедитесь, что колонки имеют четко заданные типы (используйте ОписаниеТипов), иначе СУБД не сможет эффективно построить план запроса, и производительность упадет.

В заключение отметим, что для задачи о «колбасных наборах» (сообщение 25), наиболее стабильным решением будет запрос к регистру накопления «Продажи» с соединением с временной таблицей набора. Это обеспечит и скорость, и точность данных с учетом возвратов и характеристик товаров.

← На главную