При работе с запросами в 1С 7.7 часто возникает задача получить уникальный список каких-либо данных, например, контрагентов, которым были продажи за определенный период. Проблема может возникнуть, когда на одного контрагента приходится несколько документов — в результате обхода запроса его наименование выводится многократно. Разберем, почему так происходит и как этого избежать.
Проанализируем ситуацию на конкретном примере. Задача — выбрать всех клиентов, которым продавали определенную номенклатуру, и получить их список без дублей.
Исходный код автора выглядел так:
Процедура Clients()
// ... пропущена подготовка параметров ...
ТекстЗапроса =
"
|Период с НачДата по КонДата;
|Обрабатывать НеПомеченныеНаУдаление;
|Номенклатура = Документ.Реализация.Номенклатура;
|РеализацияТоваров = Документ.Реализация.ТекущийДокумент;
|Контрагент = Документ.Реализация.Контрагент;
|ТочкаДоставки = Документ.Реализация.ТочкаДоставки;
|Условие(Номенклатура в грУвелка);
|Группировка Контрагент Упорядочить по Контрагент.Наименование;
|Группировка РеализацияТоваров; ";
Запрос = СоздатьОбъект("Запрос");
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
// ... обработка ошибки ...
Возврат;
КонецЕсли;
// Предполагаемый обход результата, который приводит к дублям
Пока Запрос.Группировка("Контрагент") = 1 Цикл
Пока Запрос.Группировка("РеализацияТоваров") = 1 Цикл
// В этом цикле Запрос.Контрагент будет повторяться
// для каждого документа одного и того же клиента
Сообщить("Клиент: " + Запрос.Контрагент.Наименование);
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Выясним причину появления дублей и рассмотрим несколько способов решения этой задачи.
Основная причина проблемы кроется в структуре самого запроса и способе его обхода. Давайте посмотрим на строки группировок:
|Группировка Контрагент Упорядочить по Контрагент.Наименование;
|Группировка РеализацияТоваров;
Здесь создается двухуровневая структура: на первом уровне — уникальные контрагенты, а на втором, вложенном уровне для каждого контрагента — все его документы реализации, удовлетворяющие условиям. При обходе такого результата во вложенном цикле (по документам) мы, естественно, будем получать одного и того же контрагента из внешней группировки столько раз, сколько у него было документов.
Важный момент: автор добавил группировку по документам не случайно. Ему нужно было получить из документа дополнительный реквизит — ТочкаДоставки. Это распространенное заблуждение, что для доступа к реквизиту документа обязательно нужна группировка по самому документу.
Самый правильный и производительный способ — это исправить сам запрос так, чтобы он сразу возвращал нужный результат без избыточных данных.
Случай 1: Нужны только уникальные контрагенты
Если ваша задача — получить только список уникальных клиентов, то вторая группировка по документам просто не нужна. Достаточно оставить одну группировку по контрагентам. Запрос вернет уникальный перечень, и дублей не будет.
ТекстЗапроса =
"
|Период с НачДата по КонДата;
|Контрагент = Документ.Реализация.Контрагент;
|Номенклатура = Документ.Реализация.Номенклатура;
|Условие(Номенклатура в грУвелка);
|Группировка Контрагент Упорядочить по Контрагент.Наименование;
";
При обходе такого запроса достаточно одного цикла, и каждый контрагент встретится лишь один раз.
Случай 2: Нужны уникальные пары "Контрагент – ТочкаДоставки"
Это как раз ситуация автора. Ему, помимо контрагента, нужна была и точка доставки. Чтобы решить эту задачу, не нужно группировать по всему объекту Документ. Вместо этого, нужно получить нужный реквизит как переменную запроса и добавить группировку именно по нему.
Рассмотрим, как это сделать по шагам:
ТекущийДокумент).Вот как будет выглядеть исправленный и корректный текст запроса:
ТекстЗапроса =
"
|Период с НачДата по КонДата;
|Обрабатывать НеПомеченныеНаУдаление;
|Номенклатура = Документ.Реализация.Номенклатура;
|Контрагент = Документ.Реализация.Контрагент;
|ТочкаДоставки = Документ.Реализация.ТочкаДоставки; // Получаем реквизит напрямую
|Условие(Номенклатура в грУвелка);
|Группировка Контрагент Упорядочить по Контрагент.Наименование;
|Группировка ТочкаДоставки; // Группируем по нужному реквизиту
";
Теперь результат запроса будет содержать уникальные пары Контрагент – ТочкаДоставки. При обходе такого запроса вы сможете получить все необходимые данные без лишних дублей на уровне контрагентов.
Иногда запрос намеренно строится так, что возвращает избыточные данные, а их последующая обработка и "сворачивание" выполняются программно. Это может быть полезно в сложных сценариях, но для простой задачи получения уникальных значений является менее оптимальным путем. Тем не менее, рассмотрим эти методы, так как они являются важной частью инструментария разработчика 1С 7.7.
Метод А: Использование объекта СписокЗначений
Объект СписокЗначений хранит коллекцию значений и автоматически обеспечивает их уникальность. Можно выполнить исходный, "неправильный" запрос, а затем в цикле добавлять контрагентов в список значений. Повторные добавления одного и того же элемента будут проигнорированы.
// ... после Запрос.Выполнить() ...
СписокКлиентов = СоздатьОбъект("СписокЗначений");
Пока Запрос.Группировка("Контрагент") = 1 Цикл
// Добавляем контрагента в список.
// Если он там уже есть, ничего не произойдет.
СписокКлиентов.ДобавитьЗначение(Запрос.Контрагент);
КонецЦикла;
// Теперь в СписокКлиентов содержатся только уникальные клиенты
// Можно обойти его и вывести результат
Для сч = 1 по СписокКлиентов.РазмерСписка() Цикл
ТекКонтрагент = СписокКлиентов.ПолучитьЗначение(сч);
Сообщить(ТекКонтрагент.Наименование);
КонецЦикла;
Этот метод прост и эффективен, если вам нужен только плоский список уникальных значений одного реквизита.
Метод Б: Выгрузка в ТаблицуЗначений и свёртка
Это более мощный и универсальный подход. Результат запроса можно выгрузить в объект ТаблицаЗначений, а затем использовать его метод Свернуть(). Этот метод группирует строки по указанным колонкам, опционально суммируя значения в других числовых колонках.
Посмотрим на пример. Предположим, мы хотим получить не только уникальных контрагентов, но и общую сумму продаж для каждого.
// В запросе нужно добавить получение числового реквизита, например, Суммы
ТекстЗапроса =
"
|...
|Сумма = Документ.Реализация.Сумма;
|Контрагент = Документ.Реализация.Контрагент;
|...
|// Группировки здесь не так важны, так как мы выгружаем детальные данные
|Группировка Документ.Реализация;
";
Запрос = СоздатьОбъект("Запрос");
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Возврат;
КонецЕсли;
ТЗ_Результат = СоздатьОбъект("ТаблицаЗначений");
// Выгружаем результат "как есть", со всеми строками и дублями контрагентов
Запрос.Выгрузить(ТЗ_Результат, 0, 0);
// Теперь "сворачиваем" таблицу.
// 1-й параметр: колонки для группировки (уникальные значения).
// 2-й параметр: колонки для суммирования.
ТЗ_Результат.Свернуть("Контрагент", "Сумма");
// После выполнения метода Свернуть() в ТЗ_Результат останутся только
// строки с уникальными контрагентами и просуммированными для них суммами.
ТЗ_Результат.ВыбратьСтроки();
Пока ТЗ_Результат.ПолучитьСтроку() = 1 Цикл
Сообщить("Клиент: " + ТЗ_Результат.Контрагент.Наименование + ", Сумма: " + ТЗ_Результат.Сумма);
КонецЦикла;
Метод Свернуть() — чрезвычайно полезный инструмент для агрегации данных, который позволяет избежать сложных конструкций с функциями Сумма() внутри самого текста запроса 1С 7.7.
Подведем итог: для решения задачи по устранению дублей всегда в первую очередь старайтесь оптимизировать сам запрос. Это наиболее эффективный путь. Если же требуется сложная последующая обработка данных, используйте программные методы с объектами СписокЗначений или ТаблицаЗначений.