В практике разработки на платформе 1С нередко возникает задача генерации полного набора сочетаний из нескольких исходных наборов данных. В математике эта операция называется декартовым произведением. Например, если у нас есть несколько независимых списков параметров (цвета, размеры, материалы или склады, номенклатура, типы цен), требуется построить единый список всех возможных вариантов без повторений.
Разберем основные подходы к решению этой задачи: от простого запроса к фиксированному числу таблиц до универсального итеративного алгоритма для произвольного числа наборов данных, а также выясним нюансы работы с производительностью и памятью.
Прежде чем приступать к комбинированию строк, важно убедиться, что исходные списки содержат только уникальные элементы. Если в исходных таблицах присутствуют повторяющиеся значения, итоговая выборка будет раздута одинаковыми комбинациями. Для этого выполним предварительное сворачивание каждой исходной таблицы значений перед передачей в обработку:
Таблица1.Свернуть("Значение");
Таблица2.Свернуть("Значение");
Таблица3.Свернуть("Значение");
Метод Свернуть позволяет быстро очистить коллекции от дублей, что существенно уменьшит итоговое количество итераций.
Если количество таблиц фиксировано и невелико (например, 2–4 таблицы), наиболее компактным и быстрым способом в 1С является формирование запроса с соединением таблиц без условий связи (декартово произведение). В языке запросов 1С перечисление источников через запятую в секции ИЗ выполняет именно перекрестное соединение.
Посмотрим на пример формирования такого запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| Т1.Значение КАК Поле1,
| Т2.Значение КАК Поле2,
| Т3.Значение КАК Поле3
|ИЗ
| &ТЗ1 КАК Т1,
| &ТЗ2 КАК Т2,
| &ТЗ3 КАК Т3";
Запрос.УстановитьПараметр("ТЗ1", Таблица1);
Запрос.УстановитьПараметр("ТЗ2", Таблица2);
Запрос.УстановитьПараметр("ТЗ3", Таблица3);
РезультатТаблица = Запрос.Выполнить().Выгрузить();
Ключевое слово РАЗЛИЧНЫЕ исключает дублирующиеся строки на уровне СУБД, даже если предварительное сворачивание таблиц не выполнялось.
Когда число входящих наборов данных заранее неизвестно и они передаются, например, в виде массива таблиц значений МассивТаблиц, текст запроса можно сформировать программно в цикле:
Запрос = Новый Запрос;
ТекстПоля = "";
ТекстИсточники = "";
Для Номер = 0 По МассивТаблиц.ВГраница() Цикл
ИмяПараметра = "ТЗ" + Формат(Номер, "ЧГ=0");
ИмяАлиаса = "Т" + Формат(Номер, "ЧГ=0");
Разделитель = ?(Номер = 0, "", ", ");
ТекстПоля = ТекстПоля + Разделитель + ИмяАлиаса + ".Значение КАК Поле" + Формат(Номер, "ЧГ=0");
ТекстИсточники = ТекстИсточники + Разделитель + "&" + ИмяПараметра + " КАК " + ИмяАлиаса;
Запрос.УстановитьПараметр(ИмяПараметра, МассивТаблиц[Номер]);
КонецЦикла;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ " + ТекстПоля + "
|ИЗ " + ТекстИсточники;
РезультатТаблица = Запрос.Выполнить().Выгрузить();
У выполнения декартова произведения через запросы есть существенное ограничение: запрос пытается вычислить и материализовать всю выборку целиком. Если таблиц много или в них по тысяче строк (1000 × 1000 × 1000 = 1 000 000 000 комбинаций), база данных исчерпает свободное место в системной базе tempdb или платформа завершит процесс с ошибкой нехватки оперативной памяти.
Чтобы иметь возможность получать комбинации постепенно (потоковая обработка), не перегружая память и не прибегая к рекурсивным вызовам процедур, применим алгоритм со счетчиком индексов (работающий по принципу смешанной системы счисления или механического одометра):
// МассивКоллекций - массив массивов или таблиц с элементами для перебора
КоличествоКоллекций = МассивКоллекций.Количество();
// Проверим, что нет пустых коллекций
Для Каждого Коллекция Из МассивКоллекций Цикл
Если Коллекция.Количество() = 0 Тогда
Возврат; // При пустом множестве декартово произведение пусто
КонецЕсли;
КонецЦикла;
// Инициализируем текущие индексы нулями
Индексы = Новый Массив(КоличествоКоллекций);
Для Индекс = 0 По КоличествоКоллекций - 1 Цикл
Индексы[Индекс] = 0;
КонецЦикла;
Пока Истина Цикл
// 1. Получаем текущую комбинацию значений
ТекущаяКомбинация = Новый Массив;
Для Индекс = 0 По КоличествоКоллекций - 1 Цикл
ЗначениеЭлемента = МассивКоллекций[Индекс][Индексы[Индекс]];
ТекущаяКомбинация.Добавить(ЗначениеЭлемента);
КонецЦикла;
// 2. Обрабатываем полученную комбинацию (запись в файл, расчет, фильтрация)
// ОбработатьКомбинацию(ТекущаяКомбинация);
// 3. Сдвигаем счетчик индексов справа налево
Позиция = КоличествоКоллекций - 1;
Пока Позиция >= 0 Цикл
Индексы[Позиция] = Индексы[Позиция] + 1;
Если Индексы[Позиция] < МассивКоллекций[Позиция].Количество() Тогда
// Переноса разряда нет, переходим к следующей комбинации
Прервать;
Иначе
// Произошло переполнение в текущем разряде - сбрасываем в ноль и идем влево
Индексы[Позиция] = 0;
Позиция = Позиция - 1;
КонецЕсли;
КонецЦикла;
// Если переполнение дошло до крайней левой позиции, весь перебор закончен
Если Позиция < 0 Тогда
Прервать;
КонецЕсли;
КонецЦикла;
Преимущества такого подхода:
O(N), где N — количество коллекций, независимо от миллиардов возможных исходов.ТаблицаЗначений.Проанализируем ключевые правила, которых следует придерживаться при работе с массивными декартовыми произведениями:
ТаблицаЗначений. Если необходимо выгрузить данные во внешнюю систему, используйте потоковую запись через объекты ЗаписьТекста, ЗаписьJSON или ЗаписьXML.ФоновыеЗадания.Выполнить, передав в каждый поток свою порцию данных.ГДЕ до объединения таблиц. Это позволит СУБД отсечь ненужные ветки перебора на самом раннем этапе.