В процессе работы с номенклатурой, особенно при объединении данных из разных баз, часто возникает задача поиска и группировки дублей. Один из верных признаков дублей — пересекающиеся штрихкоды. Однако задача усложняется, если товары связаны не напрямую, а через цепочку посредников. Рассмотрим, как решить эту проблему, когда стандартные запросы оказываются бессильны.
Проанализируем ситуацию на конкретном примере. Допустим, у нас есть следующие связи "Товар - Штрихкод":
Из этих данных видно:
В итоге, все четыре товара — Товар1, Товар2, Товар3, и Товар4 — образуют одну большую группу связанных элементов, и их нужно объединить. Проблема в том, что, например, у Товара1 и Товара4 нет общих штрихкодов, связь между ними транзитивная (через Товар2).
Первая мысль, которая приходит в голову — использовать запрос с группировкой по штрихкоду. Например, найти все штрихкоды, которые используются более чем у одного товара.
ВЫБРАТЬ
Т.Штрихкод,
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Т.Номенклатура) КАК КоличествоНоменклатуры
ИЗ
ИсходнаяТаблица КАК Т
СГРУППИРОВАТЬ ПО
Т.Штрихкод
ИМЕЮЩИЕ
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Т.Номенклатура) > 1
Такой запрос покажет нам только прямые пересечения:
Но этот запрос не объединит все четыре товара в одну группу. Мы получим три отдельные пары, но не увидим, что все они являются частью одной большой цепи. Попытка соединить таблицу саму с собой по штрихкоду также даст только связи "первого уровня" и не решит проблему транзитивности.
Вывод: стандартными средствами языка запросов 1С решить эту задачу одним махом невозможно, так как он не поддерживает рекурсивные обходы (CTE - Common Table Expressions), которые есть в полноценных SQL-серверах.
Правильный подход к решению этой задачи — это алгоритмический. В теории графов эта задача называется поиском компонент связности. Наши товары — это вершины графа, а общие штрихкоды — ребра, которые их соединяют. Нам нужно найти все связанные между собой группы вершин.
В среде 1С это можно реализовать с помощью итерационного (циклического) подхода. Разберем его по шагам.
Шаг 1. Подготовка данных
Для начала, выгрузим все связи «Номенклатура - Штрихкод» в таблицу значений для удобной обработки в памяти.
// Предположим, у нас есть таблица или результат запроса
// с полями "Номенклатура" и "Штрихкод"
ТаблицаСвязей = Новый ТаблицаЗначений;
ТаблицаСвязей.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаСвязей.Колонки.Добавить("Штрихкод", Новый ОписаниеТипов("Строка"));
// ... здесь код загрузки данных в ТаблицуСвязей ...
Далее создадим таблицу, где будем хранить информацию о принадлежности каждой номенклатуры к определенной группе. Изначально каждая номенклатура будет в своей собственной группе.
ТаблицаГрупп = Новый ТаблицаЗначений;
ТаблицаГрупп.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаГрупп.Колонки.Добавить("НомерГруппы", Новый ОписаниеТипов("Число"));
// Индексируем для быстрого поиска
ТаблицаГрупп.Индексы.Добавить("Номенклатура");
// Заполняем начальными данными
УникальнаяНоменклатура = ТаблицаСвязей.Скопировать(, "Номенклатура");
УникальнаяНоменклатура.Свернуть("Номенклатура");
НомерГруппы = 1;
Для Каждого Строка Из УникальнаяНоменклатура Цикл
НоваяСтрока = ТаблицаГрупп.Добавить();
НоваяСтрока.Номенклатура = Строка.Номенклатура;
НоваяСтрока.НомерГруппы = НомерГруппы;
НомерГруппы = НомерГруппы + 1;
КонецЦикла;
Шаг 2. Итерационное объединение групп
Теперь начинается самое интересное. Мы будем в цикле выполнять запрос, который находит связанные товары, и объединять их группы до тех пор, пока объединения не прекратятся.
Создадим временную таблицу в менеджере временных таблиц и поместим туда наши исходные связи.
МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| ТаблицаСвязей.Номенклатура,
| ТаблицаСвязей.Штрихкод
|ПОМЕСТИТЬ ВТ_Связи
|ИЗ
| &ТаблицаСвязей КАК ТаблицаСвязей
|;
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Т1.Номенклатура КАК Номенклатура1,
| Т2.Номенклатура КАК Номенклатура2
|ИЗ
| ВТ_Связи КАК Т1
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Связи КАК Т2
| ПО Т1.Штрихкод = Т2.Штрихкод
| И Т1.Номенклатура > Т2.Номенклатура"; // Условие > нужно чтобы избежать дублей (А,Б) и (Б,А) и самосвязей (А,А)
Запрос.УстановитьПараметр("ТаблицаСвязей", ТаблицаСвязей);
ВыборкаПар = Запрос.Выполнить().Выбрать();
Теперь организуем цикл, который будет выполняться, пока происходят изменения.
ПродолжатьЦикл = Истина;
Пока ПродолжатьЦикл Цикл
ПродолжатьЦикл = Ложь; // Предполагаем, что изменений не будет
ВыборкаПар.Сбросить(); // Возвращаемся в начало выборки пар
Пока ВыборкаПар.Следующий() Цикл
Номенклатура1 = ВыборкаПар.Номенклатура1;
Номенклатура2 = ВыборкаПар.Номенклатура2;
// Находим текущие группы для пары номенклатуры
СтрокаГруппы1 = ТаблицаГрупп.Найти(Номенклатура1, "Номенклатура");
СтрокаГруппы2 = ТаблицаГрупп.Найти(Номенклатура2, "Номенклатура");
Если СтрокаГруппы1 = Неопределено Или СтрокаГруппы2 = Неопределено Тогда
Продолжить; // На всякий случай
КонецЕсли;
НомерГруппы1 = СтрокаГруппы1.НомерГруппы;
НомерГруппы2 = СтрокаГруппы2.НомерГруппы;
// Если номенклатуры уже в одной группе, ничего не делаем
Если НомерГруппы1 = НомерГруппы2 Тогда
Продолжить;
КонецЕсли;
// Объединяем группы. Присваиваем всем элементам большей группы номер меньшей
МинимальныйНомер = Мин(НомерГруппы1, НомерГруппы2);
МаксимальныйНомер = Макс(НомерГруппы1, НомерГруппы2);
// Находим все строки с максимальным номером и меняем на минимальный
Для Каждого СтрокаДляЗамены Из ТаблицаГрупп Цикл
Если СтрокаДляЗамены.НомерГруппы = МаксимальныйНомер Тогда
СтрокаДляЗамены.НомерГруппы = МинимальныйНомер;
КонецЕсли;
КонецЦикла;
// Фиксируем, что на этой итерации были изменения, и нужно будет запустить цикл еще раз
ПродолжатьЦикл = Истина;
КонецЦикла;
КонецЦикла;
После завершения этого цикла в ТаблицаГрупп все связанные (напрямую или транзитивно) номенклатуры будут иметь одинаковый НомерГруппы.
Шаг 3. Получение результата
Теперь можно легко получить итоговую таблицу, соединив исходные данные с таблицей групп.
ЗапросИтог = Новый Запрос;
ЗапросИтог.Текст =
"ВЫБРАТЬ
| ТГ.НомерГруппы,
| ТС.Номенклатура,
| ТС.Штрихкод
|ИЗ
| &ТаблицаСвязей КАК ТС
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ &ТаблицаГрупп КАК ТГ
| ПО ТС.Номенклатура = ТГ.Номенклатура
|
|УПОРЯДОЧИТЬ ПО
| НомерГруппы,
| Номенклатура";
ЗапросИтог.УстановитьПараметр("ТаблицаСвязей", ТаблицаСвязей);
ЗапросИтог.УстановитьПараметр("ТаблицаГрупп", ТаблицаГрупп);
Результат = ЗапросИтог.Выполнить().Выгрузить();
// В таблице "Результат" будет искомый список
Задача группировки элементов по транзитивным связям является классической алгоритмической задачей. Не пытайтесь решить ее одним запросом 1С — это приведет либо к ошибкам в логике, либо к чрезмерно сложным и медленным конструкциям. Оптимальным решением в рамках платформы 1С является итерационный алгоритм, который последовательно "склеивает" группы связанных элементов до тех пор, пока вся структура не стабилизируется. Этот подход надежен, понятен и хорошо справляется с данными объемом в десятки тысяч записей.