В практике разработчика 1С часто возникает задача сопоставления двух наборов данных. Типичный пример: маркетинговая акция, при которой премия менеджеру полагается за продажу определенного «набора» товаров (например, 3 единицы одного товара, 2 единицы другого и 2 единицы третьего). Нам необходимо выяснить, сколько таких полных наборов содержится в фактической реализации. Эту задачу можно решить как программным перебором (с оптимизацией или без), так и с помощью языка запросов. Рассмотрим эти подходы подробнее.
Если нам нужно максимально быстро сравнить две таблицы значений в памяти, не обращаясь к базе данных, использование объекта Соответствие является наиболее эффективным с точки зрения теории алгоритмов. Соответствие в 1С реализовано как хеш-таблица, что обеспечивает среднее время поиска за O(1) (константное время), в то время как обычный перебор дает O(N^2).
Проанализируем ситуацию: у нас есть ТаблицаНабора и ТаблицаПродаж. Чтобы проверить наличие каждой строки, мы сначала подготовим «индекс» в памяти. Разберем код реализации:
// Создаем хеш-таблицу для быстрого поиска
ХешТаблица = Новый Соответствие;
// Заполняем хеш на основе первой таблицы (эталонного набора)
Для Каждого СтрокаНабора Из ТаблицаНабора Цикл
// Формируем уникальный ключ, объединяя номенклатуру и количество
// В реальной задаче ключ может состоять только из номенклатуры
Ключ = СтрокаНабора.Номенклатура;
ХешТаблица.Вставить(Ключ, СтрокаНабора.Количество);
КонецЦикла;
// Проверяем вхождения из второй таблицы (продажи)
КоличествоПолныхНаборов = 0;
Для Каждого СтрокаПродаж Из ТаблицаПродаж Цикл
НужноеКоличество = ХешТаблица[СтрокаПродаж.Номенклатура];
Если НужноеКоличество <> Неопределено Тогда
// Здесь можно реализовать логику подсчета долей набора
// Например: Доля = СтрокаПродаж.Количество / НужноеКоличество;
КонецЕсли;
КонецЦикла;
Важный нюанс: использование Соответствия оправдано, когда одна из таблиц очень большая. Как показывают тесты (сообщения 50-51), на 100 000 строк такой метод отрабатывает за 2-3 секунды, что значительно быстрее неиндексированного поиска.
Платформа 1С 8.3 позволяет добавлять индексы к объекту ТаблицаЗначений. Это делает метод НайтиСтроки() крайне эффективным, так как поиск начинает работать через бинарное дерево (сложность Log(N)). Рассмотрим, как применить это для нашей задачи:
// Добавляем индекс по колонке поиска
ТаблицаПродаж.Индексы.Добавить("Номенклатура");
Для Каждого СтрокаНабора Из ТаблицаНабора Цикл
Отбор = Новый Структура("Номенклатура", СтрокаНабора.Номенклатура);
НайденныеСтроки = ТаблицаПродаж.НайтиСтроки(Отбор);
Если НайденныеСтроки.Количество() > 0 Тогда
// Логика обработки найденных совпадений
КонецЕсли;
КонецЦикла;
Экспериментальным путем (сообщение 73) выяснено, что индексированная таблица значений иногда может работать даже быстрее Соответствия на объемах до 100 000 строк из-за особенностей внутренней реализации платформы и меньших затрат на формирование ключей в виде строк.
Если данные о продажах находятся в базе данных, а «акционный набор» задан в таблице на форме, самым правильным решением будет использование временных таблиц в запросе. Это позволит использовать мощь SQL-движка для вычисления количества наборов.
Алгоритм расчета «количества проданных наборов» строится на принципе «Минимум из максимумов». Для каждого товара в реализации мы считаем, сколько раз он покрывает потребность набора, а затем берем минимальное значение среди всех позиций набора в рамках документа.
Разберем пример запроса:
// 1. Помещаем таблицу набора (из формы) во временную таблицу
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос.Текст = "ВЫБРАТЬ * ПОМЕСТИТЬ ВТ_Набор ИЗ &ТЗ_Набор КАК ТЗ_Набор";
Запрос.УстановитьПараметр("ТЗ_Набор", ТаблицаНабора);
Запрос.Выполнить();
// 2. Основной расчет
Запрос.Текст =
"ВЫБРАТЬ
| Продажи.Ответственный КАК Менеджер,
| Продажи.Ссылка КАК Документ,
| // Вычисляем минимальное количество собранных наборов в чеке
| МИНИМУМ(ЕСТЬNULL(Продажи.Количество, 0) / Набор.Количество) КАК КолвоНаборовВДокументе
|ИЗ
| ВТ_Набор КАК Набор
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК Продажи
| ПО Набор.Номенклатура = Продажи.Номенклатура
|ГДЕ
| Продажи.Ссылка.Дата МЕЖДУ &Начало И &Конец
| И Продажи.Ссылка.Проведен = ИСТИНА
|СГРУППИРОВАТЬ ПО
| Продажи.Ответственный,
| Продажи.Ссылка
|ИМЕЮЩИЕ
| МИНИМУМ(ЕСТЬNULL(Продажи.Количество, 0) / Набор.Количество) >= 1";
Рассмотрим подробнее логику соединения: мы используем ВНУТРЕННЕЕ СОЕДИНЕНИЕ. Если в документе продажи отсутствует хотя бы одна номенклатура из обязательного набора, такой документ (или строка) просто не попадет в результат или даст 0 при расчете минимума (если использовать ЛЕВОЕ СОЕДИНЕНИЕ). Функция МИНИМУМ гарантирует, что если менеджер продал 10 единиц одного товара (хватит на 3 набора) и только 2 единицы другого (хватит на 1 набор), то результатом по документу будет именно 1 набор.
Проанализируем ситуацию в зависимости от контекста задачи:
ТаблицуЗначений для последующей обработки кодом может привести к нехватке оперативной памяти и зависанию сервера.Выясним причину, по которой запросы иногда кажутся сложнее. Основная трудность заключается в правильной типизации временных таблиц. При передаче ТаблицыЗначений в запрос убедитесь, что колонки имеют четко заданные типы (используйте ОписаниеТипов), иначе СУБД не сможет эффективно построить план запроса, и производительность упадет.
В заключение отметим, что для задачи о «колбасных наборах» (сообщение 25), наиболее стабильным решением будет запрос к регистру накопления «Продажи» с соединением с временной таблицей набора. Это обеспечит и скорость, и точность данных с учетом возвратов и характеристик товаров.