При разработке отчетов и механизмов выборки данных часто возникает задача получить список объектов с уникальным значением определенного реквизита. Например, требуется отобрать партии товаров так, чтобы для каждого номера ГТД в выборку попала ровно одна партия, исключив дублирование номеров грузовых таможенных деклараций.
У разработчиков нередко возникает закономерный вопрос: можно ли прямо внутри текста запроса сослаться на уже сформированные на предыдущих шагах строки или динамически отсекать дубли? Рассмотрим архитектурные особенности языка запросов в 1С:Предприятии и разберем практические способы решения этой задачи для платформ 7.7 и 8.x.
Встроенный язык запросов 1С построен на принципах реляционной алгебры и декларативного описания данных. Запрос описывает конечное результирующее множество, а не пошаговый процедурный алгоритм его формирования. Это накладывает следующие ограничения:
ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)), которые в классическом SQL позволяют пронумеровать строки внутри группы и отобрать запись с номером 1.Тем не менее, данная задача успешно решается через правильное построение группировок, использование агрегатных функций или работу с подзапросами.
Проанализируем базовый механизм встроенного языка запросов платформы 7.7. Чтобы получить уникальные ГТД и для каждого из них взять только одну соответствующую партию, необходимо правильно выстроить иерархию группировок.
Посмотрим на пример построения текста запроса по справочнику партий:
ТекстЗапроса = "
|Обрабатывать НеПомеченныеНаУдаление;
|Партия = Справочник.Партии.ТекущийЭлемент;
|ГТД = Справочник.Партии.ГТД;
|Страна = Справочник.Партии.СтранаПроисхождения;
|Группировка ГТД без групп;
|Группировка Партия без групп;
|Условие (ПустоеЗначение(ГТД) + ПустоеЗначение(Страна) = 0);
|";
Здесь мы группируем данные сначала по полю ГТД, а затем по полю Партия. В результате движок запросов сформирует дерево, где верхним уровнем будут уникальные номера ГТД, а подчиненным уровнем — все партии, относящиеся к данному номеру.
Разберем, как правильно обойти такой результат в коде модуля, чтобы получить ровно по одной партии на каждый ГТД:
Запрос = СоздатьОбъект("Запрос");
Если Запрос.Выполнить(ТекстЗапроса) = 1 Тогда
Пока Запрос.Группировка(1) = 1 Цикл
ТекГТД = Запрос.ГТД;
ТекПартия = "";
// Берем только первую попавшуюся партию из подчиненной группировки
Если Запрос.Группировка(2) = 1 Тогда
ТекПартия = Запрос.Партия;
КонецЕсли;
// Обработка полученной уникальной пары ТекГТД и ТекПартия
КонецЦикла;
КонецЕсли;
При таком обходе мы извлекаем из второй группировки только одну первую запись и сразу переходим к следующему ГТД, что полностью исключает дублирование номеров деклараций.
Если требуется обработать миллионы записей без выгрузки лишних данных в оперативную память клиента, в среде 7.7 используют внешнюю компоненту 1С++. Она позволяет отправлять прямые SQL-запросы к базе данных и задействовать всю мощь СУБД:
SELECT.NOT EXISTS.В платформе 8.x язык запросов предоставляет более гибкие инструменты агрегации и соединения таблиц. Рассмотрим основные сценарии.
Если для каждого уникального ГТД требуется выбрать одну партию (например, с минимальной или максимальной ссылкой), применим группировку по реквизиту ГТД с использованием агрегатной функции МИНИМУМ:
ВЫБРАТЬ
Партии.ГТД КАК ГТД,
МИНИМУМ(Партии.Ссылка) КАК Партия
ИЗ
Справочник.Партии КАК Партии
ГДЕ
Партии.ГТД <> ЗНАЧЕНИЕ(Справочник.ГТД.ПустаяСсылка)
СГРУППИРОВАТЬ ПО
Партии.ГТД
Этот запрос возвращает строго уникальный список ГТД и единственную партию для каждого из них без необходимости процедурного отбора при обходе выборки.
Если необходимо получить полный список элементов справочника ГТД (включая те, по которым еще не было партий) и сопоставить им не более одной партии, используем левое соединение со сгруппированным подзапросом:
ВЫБРАТЬ
СпрГТД.Ссылка КАК ГТД,
ПартииСрез.Партия КАК Партия
ИЗ
Справочник.ГТД КАК СпрГТД
ЛЕВОЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ
Партии.ГТД КАК ГТД,
МИНИМУМ(Партии.Ссылка) КАК Партия
ИЗ
Справочник.Партии КАК Партии
СГРУППИРОВАТЬ ПО
Партии.ГТД
) КАК ПартииСрез
ПО СпрГТД.Ссылка = ПартииСрез.ГТД
Когда требуется отобрать только те ГТД, которые ни разу не встречались в партиях, не следует выгружать список использованных значений в массив и передавать его параметром через В (&Массив), так как на больших объемах это приведет к падению производительности.
Оптимальным решением является соединение с проверкой на NULL:
ВЫБРАТЬ
СпрГТД.Ссылка КАК ГТД
ИЗ
Справочник.ГТД КАК СпрГТД
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Партии КАК Партии
ПО СпрГТД.Ссылка = Партии.ГТД
ГДЕ
Партии.Ссылка ЕСТЬ NULL
Таким образом, несмотря на невозможность динамического чтения промежуточных результатов внутри выполняющегося запроса, правильная комбинация группировок, агрегатных функций и соединений позволяет решать любые задачи по отбору уникальных данных на стороне СУБД максимально эффективно.