При работе с базами данных 1С, особенно в системах учета товаров, часто возникает задача по поиску сущностей, которые связаны со *всеми* элементами другого заданного набора. В нашем случае, это выбор поставщиков, которые поставляли *каждую* позицию из определенного списка номенклатуры. Эта задача является классическим примером так называемого «реляционного деления» (relational division) в теории баз данных.
Давайте подробно разберем, как решить эту задачу в 1С, проанализируем типичные ошибки и предложим эффективное решение на примере запроса.
Первоначальная постановка задачи (Сообщение 0) звучит так: «Необходимо выбрать всех поставщиков из поступлений товаров по номенклатуре из списка. Таким образом, чтобы каждая номенклатура была у каждого поставщика в любом из поступлений.» Это ключевой момент, который требует внимания. Мы должны найти поставщиков, у которых в совокупности всех их операций (или в любом конкретном поступлении, если это трактовать строго, но обычно подразумевается совокупность) присутствует *весь* заданный нами список номенклатуры.
Типичная ошибка при решении подобных задач — это попытка использовать простой фильтр Номенклатура В(&Номенклатура), как это было предложено в (Сообщение 1). Давайте посмотрим на этот ошибочный запрос:
ТабНом=этотобъект.Номенклатура.ВыгрузитьКолонку("Номенклатура");
ЗапроСЗаказов=новый запрос;
ЗапросЗаказов.Текст="ВЫБРАТЬ
| ЗаказПоставщикуТовары.Ссылка.Контрагент
|ИЗ
| Документ.ЗаказПоставщику.Товары КАК ЗаказПоставщикуТовары
|ГДЕ
| ЗаказПоставщикуТовары.Номенклатура В(&Номенклатура)";
ЗапросЗаказов.УстановитьПараметр("Номенклатура", табном);
ИтогЗаказов= ЗапросЗаказов.Выполнить().Выгрузить();
ИтогЗаказов.Свернуть("Контрагент");
Почему этот подход не решает нашу задачу? Мы видим, что запрос просто выбирает всех поставщиков, которые поставляли *хотя бы одну* номенклатуру из переданного списка. Он не гарантирует, что поставщик поставлял *все* позиции из списка. Например, если наш список состоит из "Товар А", "Товар Б" и "Товар В", а поставщик "Ромашка" поставлял только "Товар А", этот запрос все равно включит "Ромашку" в результат. А нам нужно, чтобы поставщик поставлял и "Товар А", и "Товар Б", и "Товар В". Метод Свернуть("Контрагент") лишь убирает дубликаты поставщиков, но никак не влияет на логику отбора по полноте списка номенклатуры.
Также важно отметить, что в исходной задаче речь идет о «поступлениях товаров», тогда как в примере запроса используется Документ.ЗаказПоставщику.Товары. Если мы говорим о фактически полученных товарах, то корректнее было бы использовать табличную часть Документ.ПоступлениеТоваровУслуг.Товары или аналогичные регистры (например, регистр накопления «ТоварыНаСкладах», если нужно учитывать только наличие, а не документ-основание) в зависимости от специфики вашей конфигурации 1С. В дальнейшем решении мы будем ориентироваться на источник данных, который максимально соответствует «поступлениям товаров».
Корректное решение для задачи реляционного деления обычно включает в себя несколько шагов, которые удобно реализовать с помощью пакетных запросов и временных таблиц в 1С. Этот подход был предложен в (Сообщение 6) и является наиболее понятным и производительным способом для таких задач.
Мы разберем решение по шагам:
Шаг 1: Определяем общее количество номенклатуры в нашем списке.
Нам нужен эталон — сколько уникальных позиций номенклатуры мы ищем. Это будет наше целевое количество. Мы создаем временную таблицу ВсяНоменклатура, в которую помещаем подсчитанное количество уникальных позиций из нашего входящего массива &МассивНоменклатуры.
"ВЫБРАТЬ
| КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Номенклатура.Ссылка) КАК КоличествоНоменклатуры
|ПОМЕСТИТЬ ВсяНоменклатура
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Ссылка В(&МассивНоменклатуры)
|;"
Здесь КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Номенклатура.Ссылка) гарантирует, что мы считаем только уникальные позиции, даже если в нашем входящем массиве ссылок были дубликаты (что маловероятно для ссылок, но как мера предосторожности полезно). Результатом этой временной таблицы будет одна строка с одним числовым полем КоличествоНоменклатуры, содержащим размер нашего целевого списка.
Шаг 2: Для каждого поставщика подсчитываем, сколько уникальных позиций из нашего списка он поставлял.
На этом шаге мы формируем список поставщиков и для каждого из них определяем, сколько уникальных позиций из нашего целевого списка номенклатуры он когда-либо поставлял. Мы будем использовать таблицу Документ.ЗаказПоставщику.Товары как пример, но, как уже упоминалось, вы можете заменить ее на Документ.ПоступлениеТоваровУслуг.Товары или другой источник данных, который наиболее точно отражает поступления.
"////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ЗаказПоставщикуТовары.Ссылка.Контрагент,
| КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ЗаказПоставщикуТовары.Номенклатура) КАК КоличествоНоменклатуры
|ПОМЕСТИТЬ КонтрагентыСНоменклатурой
|ИЗ
| Документ.ЗаказПоставщику.Товары КАК ЗаказПоставщикуТовары
|ГДЕ
| ЗаказПоставщикуТовары.Номенклатура В(&МассивНоменклатуры)
|СГРУППИРОВАТЬ ПО
| ЗаказПоставщикуТовары.Ссылка.Контрагент
|;"
В этой временной таблице КонтрагентыСНоменклатурой мы группируем данные по полю Контрагент (поставщику) и для каждой группы подсчитываем КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ЗаказПоставщикуТовары.Номенклатура). Условие ГДЕ ЗаказПоставщикуТовары.Номенклатура В(&МассивНоменклатуры) здесь критично: оно фильтрует только те позиции, которые входят в наш список, и не учитывает другие товары, которые поставщик мог поставлять.
Результатом этой временной таблицы будет список поставщиков, где для каждого будет указано, сколько уникальных позиций из нашего искомого списка он поставлял.
Шаг 3: Объединяем результаты и фильтруем поставщиков.
На заключительном этапе мы сравниваем количество уникальной номенклатуры, поставленной каждым контрагентом (из КонтрагентыСНоменклатурой), с общим количеством номенклатуры в нашем списке (из ВсяНоменклатура). Если эти количества совпадают, значит, поставщик поставлял *весь* требуемый список номенклатуры.
"////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| КонтрагентыСНоменклатурой.Контрагент
|ИЗ
| КонтрагентыСНоменклатурой КАК КонтрагентыСНоменклатурой
| ПОЛНОЕ СОЕДИНЕНИЕ ВсяНоменклатура КАК ВсяНоменклатура
| ПО (ИСТИНА)
|ГДЕ
| ВсяНоменклатура.КоличествоНоменклатуры = КонтрагентыСНоменклатурой.КоличествоНоменклатуры"
Здесь мы используем ПОЛНОЕ СОЕДИНЕНИЕ, но с условием ПО (ИСТИНА). Это специальный прием в запросах 1С (аналог декартова произведения, но в данном случае он просто позволяет соединить каждую строку одной таблицы с каждой строкой другой, что фактически сводится к соединению с единственной строкой ВсяНоменклатура). Главное, что нас интересует — это условие в ГДЕ: ВсяНоменклатура.КоличествоНоменклатуры = КонтрагентыСНоменклатурой.КоличествоНоменклатуры.
Именно это условие гарантирует, что в финальный результат попадут только те поставщики, которые поставляли *все* позиции из нашего первоначального списка.
Теперь давайте соберем весь пакетный запрос и рассмотрим, как подготовить параметры в коде 1С.
// 1. Подготовка списка номенклатуры для параметра
// Предположим, что "этотобъект.Номенклатура" - это табличная часть формы
// или другого объекта, содержащая колонку "Номенклатура"
ТабНом = этотобъект.Номенклатура.ВыгрузитьКолонку("Номенклатура");
// Если "Номенклатура" в табличной части содержит ссылки, то ТабНом
// уже является массивом ссылок. Если это строковые представления или что-то другое,
// нужно преобразовать в массив ссылок на элементы справочника Номенклатура.
ЗапросПоставщиков = Новый Запрос;
ЗапросПоставщиков.Текст =
"ВЫБРАТЬ
| КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Номенклатура.Ссылка) КАК КоличествоНоменклатуры
|ПОМЕСТИТЬ ВсяНоменклатура
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Ссылка В(&МассивНоменклатуры)
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ЗаказПоставщикуТовары.Ссылка.Контрагент,
| КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ЗаказПоставщикуТовары.Номенклатура) КАК КоличествоНоменклатуры
|ПОМЕСТИТЬ КонтрагентыСНоменклатурой
|ИЗ
| Документ.ЗаказПоставщику.Товары КАК ЗаказПоставщикуТовары
|ГДЕ
| ЗаказПоставщикуТовары.Номенклатура В(&МассивНоменклатуры)
|
|СГРУППИРОВАТЬ ПО
| ЗаказПоставщикуТовары.Ссылка.Контрагент
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| КонтрагентыСНоменклатурой.Контрагент
|ИЗ
| КонтрагентыСНоменклатурой КАК КонтрагентыСНоменклатурой
| ПОЛНОЕ СОЕДИНЕНИЕ ВсяНоменклатура КАК ВсяНоменклатура
| ПО (ИСТИНА)
|ГДЕ
| ВсяНоменклатура.КоличествоНоменклатуры = КонтрагентыСНоменклатурой.КоличествоНоменклатуры";
// 2. Установка параметра
ЗапросПоставщиков.УстановитьПараметр("МассивНоменклатуры", ТабНом);
// 3. Выполнение запроса и получение результата
ИтогПоставщиков = ЗапросПоставщиков.Выполнить().Выгрузить();
// Теперь в переменной ИтогПоставщиков содержится ТаблицаЗначений
// с одной колонкой "Контрагент", содержащей ссылки на тех поставщиков,
// которые поставляли всю номенклатуру из списка.
Источник данных о поступлениях: Как мы уже упоминали, в примере запроса используется Документ.ЗаказПоставщику.Товары. Если мы действительно хотим получить данные о фактических поступлениях, необходимо заменить этот источник на Документ.ПоступлениеТоваровУслуг.Товары или другой аналогичный документ в вашей конфигурации, который отражает факт получения товара от поставщика. Например:
"ИЗ
| Документ.ПоступлениеТоваровУслуг.Товары КАК ПоступлениеТоваровУслугТовары
|ГДЕ
| ПоступлениеТоваровУслугТовары.Номенклатура В(&МассивНоменклатуры)
|
|СГРУППИРОВАТЬ ПО
| ПоступлениеТоваровУслугТовары.Ссылка.Контрагент
|;"
Вы также можете рассмотреть использование регистров накопления, например, РегистрНакопления.Закупки.Обороты или РегистрНакопления.ТоварыВОрганизациях.Обороты, если вам нужна агрегированная информация за определенный период и данные регистров содержат нужные разрезы (контрагент, номенклатура).
Трактовка условия "в любом из поступлений": (Сообщение 7) поднимает важный вопрос трактовки. Предложенное нами решение находит поставщиков, которые поставляли *весь список номенклатуры в совокупности своих поступлений* (то есть, если поставщик А в одном поступлении привез "Товар1", а в другом "Товар2", и наш список состоит из "Товар1" и "Товар2", то он будет выбран). Это наиболее распространенная трактовка для таких задач.
Если же требование более строгое, и мы ищем поставщиков, у которых *каждое отдельное поступление* должно содержать *всю* номенклатуру из списка (что значительно более редкое и сложное условие), то запрос потребуется значительно усложнить. В этом случае, на этапе формирования временной таблицы КонтрагентыСНоменклатурой, нам пришлось бы группировать данные не только по Контрагент, но и по Ссылка документа поступления. И уже для каждого такого документа сравнивать количество номенклатуры. Затем выбрать контрагентов, у которых *хотя бы один* такой "полный" документ был найден. Однако для большинства бизнес-сценариев предложенное нами решение будет корректным.
Обработка параметра: Убедитесь, что переменная ТабНом (или МассивНоменклатуры) действительно содержит массив ссылок на элементы справочника "Номенклатура". Если этотобъект.Номенклатура.ВыгрузитьКолонку("Номенклатура") возвращает не ссылки, а, например, наименования или коды, вам потребуется дополнительно получить ссылки, используя запросы к справочнику "Номенклатура".
Пример получения ссылок из списка наименований:
// Допустим, ТабНаименованийНоменклатуры - это таблица значений с колонкой "Наименование",
// куда пользователь ввел наименования номенклатуры.
МассивНаименований = ТабНаименованийНоменклатуры.ВыгрузитьКолонку("Наименование");
ЗапросСсылок = Новый Запрос;
ЗапросСсылок.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Наименование В(&СписокНаименований)";
ЗапросСсылок.УстановитьПараметр("СписокНаименований", МассивНаименований);
ТабСсылокНоменклатуры = ЗапросСсылок.Выполнить().Выгрузить();
МассивНоменклатуры = ТабСсылокНоменклатуры.ВыгрузитьКолонку("Ссылка");
// Теперь МассивНоменклатуры можно использовать в основном запросе.
Производительность запроса: Использование временных таблиц (конструкция ПОМЕСТИТЬ) в 1С-запросах является хорошей практикой для сложных пакетных запросов. Промежуточные результаты материализуются, что может улучшить производительность, особенно если объемы данных велики. СУБД может эффективнее оптимизировать запросы, когда этапы разбиты.
Мы рассмотрели задачу реляционного деления и предложили эффективное решение с использованием пакетного запроса и временных таблиц в 1С. Этот подход позволяет точно отобрать поставщиков, которые поставляли *всю* необходимую номенклатуру из заданного списка. Важно внимательно выбирать источник данных (например, Документ.ПоступлениеТоваровУслуг.Товары вместо Документ.ЗаказПоставщику.Товары) и корректно передавать параметры в запрос.
Если у вас возникнут вопросы или потребуется уточнение для конкретной конфигурации, всегда готовы помочь. Такой подход к построению запросов является универсальным и может быть адаптирован для решения множества подобных аналитических задач в 1С.