В практике разработки на платформе 1С часто возникает задача: получить самый первый документ (регистратор) для каждого объекта (например, контрагента). Казалось бы, задача тривиальная — достаточно использовать МИНИМУМ(Период). Однако дьявол кроется в деталях. Если в рамках одной секунды по одному контрагенту было проведено несколько документов, простое соединение по минимальному периоду приведет к «раздуванию» выборки (дублям), так как система не поймет, какой именно документ из нескольких в этой секунде является первым.
Ситуация осложняется тем, что поле МоментВремени, которое гарантирует уникальную последовательность документов, нельзя использовать в агрегатных функциях МИНИМУМ или МАКСИМУМ. В этой статье мы подробно разберем несколько проверенных способов решения этой задачи в рамках языка запросов и программной обработки.
Проанализируем наиболее изящный способ реализации задачи чисто средствами языка запросов. Поскольку мы не можем применить МИНИМУМ к моменту времени, мы можем искусственно пронумеровать записи, предварительно отсортировав их в нужном порядке. Рассмотрим по шагам, как это сделать.
На первом этапе мы выбираем все необходимые данные и помещаем их во временную таблицу, обязательно используя УПОРЯДОЧИТЬ ПО. Важно помнить, что в 1С АВТОНОМЕРЗАПИСИ() работает корректно только при сохранении результата во временную таблицу.
ВЫБРАТЬ ПЕРВЫЕ 1000000
Рег.Контрагент КАК Контрагент,
Рег.Регистратор КАК Регистратор,
Рег.Регистратор.МоментВремени КАК МоментВремени,
АВТОНОМЕРЗАПИСИ() КАК НомерСтроки
ПОМЕСТИТЬ ВТ_ВсеЗаписи
ИЗ
РегистрНакопления.Взаиморасчеты КАК Рег
ГДЕ
Рег.Период >= &ДатаНачала
УПОРЯДОЧИТЬ ПО
Контрагент,
МоментВремени;
Обратите внимание на конструкцию ВЫБРАТЬ ПЕРВЫЕ 1000000. Это техническая хитрость: без ограничения количества записей 1С может проигнорировать УПОРЯДОЧИТЬ ПО при создании временной таблицы в некоторых версиях СУБД. Далее мы находим минимальный номер строки для каждого контрагента:
ВЫБРАТЬ
ВТ.Контрагент,
МИНИМУМ(ВТ.НомерСтроки) КАК МинНомер
ПОМЕСТИТЬ ВТ_ПервыеНомера
ИЗ
ВТ_ВсеЗаписи КАК ВТ
СГРУППИРОВАТЬ ПО
ВТ.Контрагент;
Финальным шагом мы соединяем наши таблицы, чтобы получить ссылку на конкретный регистратор:
ВЫБРАТЬ
ВТ.Контрагент,
ВТ.Регистратор
ИЗ
ВТ_ПервыеНомера КАК Номера
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ВсеЗаписи КАК ВТ ПО Номера.МинНомер = ВТ.НомерСтроки;
Разберем классический SQL-подход, который позволяет обойтись без автонумерации. Мы выбираем запись из регистра и пытаемся найти такую же запись по тому же контрагенту, у которой МоментВремени меньше. Если такая запись не найдена (результат соединения ЕСТЬ NULL), значит, текущая запись — самая первая.
Хотя напрямую МоментВремени в соединениях использовать иногда проблематично из-за особенностей платформы, мы можем использовать его в блоке ГДЕ или в условии связи, если тип поля — ссылка на документ, поддерживающий этот метод.
ВЫБРАТЬ РАЗЛИЧНЫЕ
Тек.Контрагент,
Тек.Регистратор
ИЗ
РегистрНакопления.ИмяРегистра КАК Тек
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ИмяРегистра КАК Пред
ПО Пред.Контрагент = Тек.Контрагент
И Пред.Регистратор.МоментВремени < Тек.Регистратор.МоментВремени
ГДЕ
Пред.Контрагент ЕСТЬ NULL;
Важное замечание: Этот метод очень требователен к ресурсам. На больших таблицах (миллионы записей) такое соединение «каждый с каждым» по неравенству может крайне негативно сказаться на производительности системы.
Выясним причину, по которой метод с МИНИМУМ(Регистратор) считается спорным, но часто рабочим. В 1С ссылки (UUID) не всегда генерируются в строгом хронологическом порядке. Однако в пределах одной секунды использование МИНИМУМ(Ссылка) — это отличный способ детерминированно «схлопнуть» записи.
Рассмотрим надежный алгоритм:
МИНИМУМ(Период) по каждому контрагенту во временную таблицу ВТ_МинПериоды.ВТ_МинПериоды по контрагенту и периоду. В этом запросе выбираем МИНИМУМ(Регистратор) (или МИНИМУМ(Ссылка)) по каждому контрагенту. Это позволит нам выбрать ровно один документ, даже если в первую секунду их было три.Посмотрим на ситуацию со стороны производительности. Если данных в регистре очень много, а нам нужно получить первых контрагентов для отчета, иногда выгоднее перенести часть логики из SQL в код 1С. Проанализируем пример такого подхода:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Рег.Контрагент КАК Контрагент,
| Рег.Регистратор КАК Регистратор
|ИЗ
| РегистрНакопления.Взаиморасчеты КАК Рег
|УПОРЯДОЧИТЬ ПО
| Контрагент,
| Рег.МоментВремени";
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
ТаблицаПервыхДокументов = Новый ТаблицаЗначений;
ТаблицаПервыхДокументов.Колонки.Добавить("Контрагент");
ТаблицаПервыхДокументов.Колонки.Добавить("Регистратор");
ТекущийКонтрагент = Неопределено;
Пока Выборка.Следующий() Цикл
Если Выборка.Контрагент <> ТекущийКонтрагент Тогда
// Это первая запись для нового контрагента в отсортированной выборке
НоваяСтрока = ТаблицаПервыхДокументов.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
ТекущийКонтрагент = Выборка.Контрагент;
КонецЕсли;
// Остальные записи этого же контрагента просто пропускаем
КонецЦикла;
Этот способ часто работает быстрее сложных вложенных запросов, так как СУБД выполняет линейное чтение индекса, а логика отсечения лишних строк на сервере приложений 1С работает мгновенно.
Подводя итоги, можно выделить следующие правила выбора метода:
Помните, что МоментВремени — это составной тип на уровне базы данных (дата + ссылка), поэтому правильная сортировка по нему всегда обеспечит верную последовательность документов в соответствии с их позицией на временной оси 1С.