Как получить первую реализацию по каждому контрагенту в запросе 1С

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке отчетов и обработок в 1С:Предприятии часто возникает классическая задача: выбрать только первое событие или первый документ по каждому объекту (например, первую реализацию по каждому контрагенту за период, первый заказ или срез на самую раннюю дату). Сложность заключается в том, что в языке запросов 1С отсутствуют оконные функции (такие как ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)), поэтому простую выборку первой строки в группе нельзя выполнить одной строкой кода.

Кроме того, распространенная ошибка — попытаться написать в одном запросе МИНИМУМ(Дата) и МИНИМУМ(Ссылка) с группировкой по контрагенту. В общем случае минимальная ссылка и минимальная дата могут принадлежать двум совершенно разным документам, что приведет к искажению данных.

Разберем основные надежные способы решения этой задачи: на уровне запроса с помощью временных таблиц, через группировки и обход выборки, а также с помощью программной фильтрации.

Способ 1. Двухэтапная группировка во временных таблицах (в запросе)

Рассмотрим наиболее надежный и универсальный метод получения первого документа в чистом запросе без постобработки в коде. Чтобы гарантированно сопоставить минимальную дату с конкретным документом и исключить дублирование при наличии нескольких документов за одну секунду, запрос строится в несколько этапов:

  1. Получение минимальной даты: выбираем контрагентов и их минимальные даты документов за указанный период, помещая результат во временную таблицу.
  2. Определение однозначного документа: соединяем полученную таблицу с документами по контрагенту и минимальной дате, выбирая минимальную ссылку (на случай, если в одну и ту же секунду создано два документа).
  3. Получение детальных данных: соединяем полученный список документов с таблицей документов, контактной информацией и справочником контрагентов.

Посмотрим на пример построения такого пакетного запроса:


ВЫБРАТЬ
    Реализация.Контрагент КАК Контрагент,
    МИНИМУМ(Реализация.Дата) КАК МинДата
ПОМЕСТИТЬ ВТ_МинДаты
ИЗ
    Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
    Реализация.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
    И Реализация.Проведен = ИСТИНА
СГРУППИРОВАТЬ ПО
    Реализация.Контрагент
ИНДЕКСИРОВАТЬ ПО
    Контрагент,
    МинДата
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ_МинДаты.Контрагент КАК Контрагент,
    МИНИМУМ(Реализация.Ссылка) КАК СсылкаДокумент
ПОМЕСТИТЬ ВТ_ПервыеДокументы
ИЗ
    ВТ_МинДаты КАК ВТ_МинДаты
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Реализация
        ПО ВТ_МинДаты.Контрагент = Реализация.Контрагент
        И ВТ_МинДаты.МинДата = Реализация.Дата
СГРУППИРОВАТЬ ПО
    ВТ_МинДаты.Контрагент
ИНДЕКСИРОВАТЬ ПО
    СсылкаДокумент
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ПервыеДокументы.Контрагент КАК СсылкаКонтрагент,
    Контрагенты.ИНН КАК УНП,
    Контрагенты.Наименование КАК КраткоеНаименование,
    Контрагенты.НаименованиеПолное КАК ПолноеНаименование,
    КонтактнаяИнформация.Представление КАК ЮрАдрес,
    Реализация.Ссылка КАК СсылкаДокумент,
    Реализация.Дата КАК Дата
ИЗ
    ВТ_ПервыеДокументы КАК ПервыеДокументы
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Реализация
        ПО ПервыеДокументы.СсылкаДокумент = Реализация.Ссылка
    ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
        ПО ПервыеДокументы.Контрагент = Контрагенты.Ссылка
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КонтактнаяИнформация КАК КонтактнаяИнформация
        ПО ПервыеДокументы.Контрагент = КонтактнаяИнформация.Объект
        И КонтактнаяИнформация.Тип = ЗНАЧЕНИЕ(Перечисление.ТипыКонтактнойИнформации.Адрес)
        И КонтактнаяИнформация.Вид = ЗНАЧЕНИЕ(Справочник.ВидыКонтактнойИнформации.ЮрАдресКонтрагента)
УПОРЯДОЧИТЬ ПО
    Контрагенты.Наименование

Обратите внимание: разыменование полей через точку (например, Реализация.Контрагент.ИНН) приводит к скрытым неявным соединениям к таблице справочника. Явное левое соединение со справочником контрагентов и регистром контактной информации по готовому списку отфильтрованных ссылок работает значительно быстрее.

Способ 2. Обход выборки по группировкам через ИТОГИ

Если данные необходимо вывести программно в таблицу значений или элемент формы, мы можем использовать секцию ИТОГИ в запросе. Это позволяет сгруппировать данные по контрагенту, а из вложенных детальных записей считывать только первую строку.

Разберем по шагам, как строится такой алгоритм:

  1. В запросе обязательно накладываем сортировку по дате реализации (УПОРЯДОЧИТЬ ПО Реализация.Дата ВОЗР), чтобы самая ранняя запись шла первой.
  2. Рассчитываем итоги по полю контрагента (ИТОГИ ПО СсылкаКонтрагент).
  3. Обходим результат запроса с параметром ОбходРезультатаЗапроса.ПоГруппировкам, а для каждого контрагента получаем детальные записи и берем ровно один первый элемент.

Посмотрим на пример реализации в коде:


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|   Реализация.Контрагент КАК СсылкаКонтрагент,
|   Реализация.Ссылка КАК СсылкаДокумент,
|   Реализация.Дата КАК Дата,
|   Реализация.Контрагент.ИНН КАК УНП,
|   Реализация.Контрагент.Наименование КАК КраткоеНаименование
|ИЗ
|   Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
|   Реализация.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
|УПОРЯДОЧИТЬ ПО
|   СсылкаКонтрагент,
|   Дата ВОЗР
|ИТОГИ ПО
|   СсылкаКонтрагент";

Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(ДатаНачала));
Запрос.УстановитьПараметр("ДатаОкончания", КонецДня(ДатаОкончания));

РезультатЗапроса = Запрос.Выполнить();
ВыборкаГруппировки = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

Пока ВыборкаГруппировки.Следующий() Цикл
    ВыборкаДетали = ВыборкаГруппировки.Выбрать();
    Если ВыборкаДетали.Следующий() Тогда
        // Обрабатываем только первую запись по текущему контрагенту
        НоваяСтрока = ТаблицаНаФорме.Добавить();
        ЗаполнитьЗначенияСвойств(НоваяСтрока, ВыборкаДетали);
    КонецЕсли;
КонецЦикла;

Способ 3. Программный обход выборки с контролем уникальности

Проанализируем еще один быстрый вариант постобработки: плоский запрос с сортировкой и фильтрацией при помощи объекта Соответствие или метода СледующийПоЗначениюПоля.

Если используется метод Выборка.СледующийПоЗначениюПоля("Контрагент"), критически важно добавить в запрос сортировку УПОРЯДОЧИТЬ ПО Контрагент, Дата ВОЗР. Метод СледующийПоЗначениюПоля пропускает дубли только в рамках непрерывной цепочки строк с одинаковым значением поля. Без сортировки СУБД вернет данные в произвольном порядке, и один и тот же контрагент будет обработан повторно.

Альтернативный универсальный вариант — контроль прочитанных контрагентов через объект Соответствие, где поиск по ключу выполняется за константное время $O(1)$:


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|   Реализация.Контрагент КАК Контрагент,
|   Реализация.Ссылка КАК СсылкаДокумент,
|   Реализация.Дата КАК Дата
|ИЗ
|   Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
|   Реализация.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
|УПОРЯДОЧИТЬ ПО
|   Дата ВОЗР";

Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(ДатаНачала));
Запрос.УстановитьПараметр("ДатаОкончания", КонецДня(ДатаОкончания));

Выборка = Запрос.Выполнить().Выбрать();
ОбработанныеКонтрагенты = Новый Соответствие;

Пока Выборка.Следующий() Цикл
    Если ОбработанныеКонтрагенты[Выборка.Контрагент] <> Неопределено Тогда
        Продолжить; // Пропускаем повторные вхождения
    КонецЕсли;
    
    ОбработанныеКонтрагенты.Вставить(Выборка.Контрагент, Истина);
    
    НоваяСтрока = ТаблицаНаФорме.Добавить();
    ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
КонецЦикла;

Резюме: для сложных отчетов (в СКД или для дальнейших соединений в БД) рекомендуется использовать Способ 1 с временными таблицами. Для программного заполнения форм и небольших выборок отлично подходят Способ 2 и Способ 3.

← На главную