При разработке отчетов и обработок в 1С:Предприятии часто возникает классическая задача: выбрать только первое событие или первый документ по каждому объекту (например, первую реализацию по каждому контрагенту за период, первый заказ или срез на самую раннюю дату). Сложность заключается в том, что в языке запросов 1С отсутствуют оконные функции (такие как ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)), поэтому простую выборку первой строки в группе нельзя выполнить одной строкой кода.
Кроме того, распространенная ошибка — попытаться написать в одном запросе МИНИМУМ(Дата) и МИНИМУМ(Ссылка) с группировкой по контрагенту. В общем случае минимальная ссылка и минимальная дата могут принадлежать двум совершенно разным документам, что приведет к искажению данных.
Разберем основные надежные способы решения этой задачи: на уровне запроса с помощью временных таблиц, через группировки и обход выборки, а также с помощью программной фильтрации.
Рассмотрим наиболее надежный и универсальный метод получения первого документа в чистом запросе без постобработки в коде. Чтобы гарантированно сопоставить минимальную дату с конкретным документом и исключить дублирование при наличии нескольких документов за одну секунду, запрос строится в несколько этапов:
Посмотрим на пример построения такого пакетного запроса:
ВЫБРАТЬ
Реализация.Контрагент КАК Контрагент,
МИНИМУМ(Реализация.Дата) КАК МинДата
ПОМЕСТИТЬ ВТ_МинДаты
ИЗ
Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
Реализация.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
И Реализация.Проведен = ИСТИНА
СГРУППИРОВАТЬ ПО
Реализация.Контрагент
ИНДЕКСИРОВАТЬ ПО
Контрагент,
МинДата
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_МинДаты.Контрагент КАК Контрагент,
МИНИМУМ(Реализация.Ссылка) КАК СсылкаДокумент
ПОМЕСТИТЬ ВТ_ПервыеДокументы
ИЗ
ВТ_МинДаты КАК ВТ_МинДаты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Реализация
ПО ВТ_МинДаты.Контрагент = Реализация.Контрагент
И ВТ_МинДаты.МинДата = Реализация.Дата
СГРУППИРОВАТЬ ПО
ВТ_МинДаты.Контрагент
ИНДЕКСИРОВАТЬ ПО
СсылкаДокумент
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ПервыеДокументы.Контрагент КАК СсылкаКонтрагент,
Контрагенты.ИНН КАК УНП,
Контрагенты.Наименование КАК КраткоеНаименование,
Контрагенты.НаименованиеПолное КАК ПолноеНаименование,
КонтактнаяИнформация.Представление КАК ЮрАдрес,
Реализация.Ссылка КАК СсылкаДокумент,
Реализация.Дата КАК Дата
ИЗ
ВТ_ПервыеДокументы КАК ПервыеДокументы
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Реализация
ПО ПервыеДокументы.СсылкаДокумент = Реализация.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
ПО ПервыеДокументы.Контрагент = Контрагенты.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КонтактнаяИнформация КАК КонтактнаяИнформация
ПО ПервыеДокументы.Контрагент = КонтактнаяИнформация.Объект
И КонтактнаяИнформация.Тип = ЗНАЧЕНИЕ(Перечисление.ТипыКонтактнойИнформации.Адрес)
И КонтактнаяИнформация.Вид = ЗНАЧЕНИЕ(Справочник.ВидыКонтактнойИнформации.ЮрАдресКонтрагента)
УПОРЯДОЧИТЬ ПО
Контрагенты.Наименование
Обратите внимание: разыменование полей через точку (например, Реализация.Контрагент.ИНН) приводит к скрытым неявным соединениям к таблице справочника. Явное левое соединение со справочником контрагентов и регистром контактной информации по готовому списку отфильтрованных ссылок работает значительно быстрее.
Если данные необходимо вывести программно в таблицу значений или элемент формы, мы можем использовать секцию ИТОГИ в запросе. Это позволяет сгруппировать данные по контрагенту, а из вложенных детальных записей считывать только первую строку.
Разберем по шагам, как строится такой алгоритм:
УПОРЯДОЧИТЬ ПО Реализация.Дата ВОЗР), чтобы самая ранняя запись шла первой.ИТОГИ ПО СсылкаКонтрагент).ОбходРезультатаЗапроса.ПоГруппировкам, а для каждого контрагента получаем детальные записи и берем ровно один первый элемент.Посмотрим на пример реализации в коде:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Реализация.Контрагент КАК СсылкаКонтрагент,
| Реализация.Ссылка КАК СсылкаДокумент,
| Реализация.Дата КАК Дата,
| Реализация.Контрагент.ИНН КАК УНП,
| Реализация.Контрагент.Наименование КАК КраткоеНаименование
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
| Реализация.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
|УПОРЯДОЧИТЬ ПО
| СсылкаКонтрагент,
| Дата ВОЗР
|ИТОГИ ПО
| СсылкаКонтрагент";
Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(ДатаНачала));
Запрос.УстановитьПараметр("ДатаОкончания", КонецДня(ДатаОкончания));
РезультатЗапроса = Запрос.Выполнить();
ВыборкаГруппировки = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаГруппировки.Следующий() Цикл
ВыборкаДетали = ВыборкаГруппировки.Выбрать();
Если ВыборкаДетали.Следующий() Тогда
// Обрабатываем только первую запись по текущему контрагенту
НоваяСтрока = ТаблицаНаФорме.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, ВыборкаДетали);
КонецЕсли;
КонецЦикла;
Проанализируем еще один быстрый вариант постобработки: плоский запрос с сортировкой и фильтрацией при помощи объекта Соответствие или метода СледующийПоЗначениюПоля.
Если используется метод Выборка.СледующийПоЗначениюПоля("Контрагент"), критически важно добавить в запрос сортировку УПОРЯДОЧИТЬ ПО Контрагент, Дата ВОЗР. Метод СледующийПоЗначениюПоля пропускает дубли только в рамках непрерывной цепочки строк с одинаковым значением поля. Без сортировки СУБД вернет данные в произвольном порядке, и один и тот же контрагент будет обработан повторно.
Альтернативный универсальный вариант — контроль прочитанных контрагентов через объект Соответствие, где поиск по ключу выполняется за константное время $O(1)$:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Реализация.Контрагент КАК Контрагент,
| Реализация.Ссылка КАК СсылкаДокумент,
| Реализация.Дата КАК Дата
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
| Реализация.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
|УПОРЯДОЧИТЬ ПО
| Дата ВОЗР";
Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(ДатаНачала));
Запрос.УстановитьПараметр("ДатаОкончания", КонецДня(ДатаОкончания));
Выборка = Запрос.Выполнить().Выбрать();
ОбработанныеКонтрагенты = Новый Соответствие;
Пока Выборка.Следующий() Цикл
Если ОбработанныеКонтрагенты[Выборка.Контрагент] <> Неопределено Тогда
Продолжить; // Пропускаем повторные вхождения
КонецЕсли;
ОбработанныеКонтрагенты.Вставить(Выборка.Контрагент, Истина);
НоваяСтрока = ТаблицаНаФорме.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
КонецЦикла;
Резюме: для сложных отчетов (в СКД или для дальнейших соединений в БД) рекомендуется использовать Способ 1 с временными таблицами. Для программного заполнения форм и небольших выборок отлично подходят Способ 2 и Способ 3.