В практике разработки на платформе 1С часто возникает задача, которую невозможно решить простым использованием конструкции ПЕРВЫЕ. Например, когда нам нужно получить не просто 4 последних заказа в целом по системе, а по 4 последних заказа для каждого контрагента. В типовых конфигурациях, таких как УПП или комплексная автоматизация, где объем данных в регистре «Продажи» может исчисляться миллионами записей, выбор неоптимального метода может привести к фатальному падению производительности.
В этой статье мы подробно разберем несколько проверенных способов решения данной задачи: от классических запросов с соединениями до современных оконных функций и механизмов СКД. Рассмотрим плюсы и минусы каждого подхода, чтобы вы могли выбрать наиболее подходящий для вашей базы данных.
Это классический способ, который часто применялся до появления оконных функций. Суть метода заключается в том, чтобы соединить таблицу саму с собой по условию «Дата больше, чем в текущей строке». Проанализируем логику: если для текущей записи мы найдем меньше 4-х записей с более поздней датой, значит, текущая запись входит в четверку последних.
Рассмотрим пример кода для реализации этого алгоритма:
ВЫБРАТЬ
Заказы1.Ссылка КАК Ссылка,
Заказы1.Дата КАК Дата,
Заказы1.Контрагент КАК Контрагент
ПОМЕСТИТЬ ВТ_ВсеЗаказы
ИЗ
Документ.ЗаказПокупателя КАК Заказы1;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
т1.Контрагент,
т1.Ссылка,
т1.Дата
ИЗ
ВТ_ВсеЗаказы КАК т1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ВсеЗаказы КАК т2
ПО т1.Контрагент = т2.Контрагент
И т1.Дата < т2.Дата
ГРУППИРОВАТЬ ПО
т1.Контрагент,
т1.Ссылка,
т1.Дата
ИМЕЮЩИЕ
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ т2.Ссылка) < 4
УПОРЯДОЧИТЬ ПО
т1.Контрагент,
т1.Дата УБЫВ
Важный нюанс: Этот метод довольно ресурсоемок. При большом количестве заказов по одному контрагенту соединение «многие ко многим» внутри группы создаст огромную промежуточную таблицу в памяти SQL-сервера. Используйте его только в том случае, если количество документов по каждому контрагенту невелико, либо предварительно ограничивайте выборку периодом в первой временной таблице.
Этот способ часто вызывает споры среди разработчиков. С одной стороны, 1С не рекомендует использовать конструкцию В (ВЫБРАТЬ ПЕРВЫЕ...) внутри цикла по основной таблице, с другой — современные оптимизаторы СУБД (особенно MS SQL Server) отлично справляются с такими запросами, превращая их в эффективные операции Apply.
Разберем, как выглядит такой запрос:
ВЫБРАТЬ РАЗЛИЧНЫЕ
ПродажиОбороты.Контрагент КАК Контрагент
ПОМЕСТИТЬ ВТ_СписокКонтрагентов
ИЗ
РегистрНакопления.Продажи.Обороты(&ДатаНач, &ДатаКон, , ) КАК ПродажиОбороты;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ.Контрагент,
ПоследниеЗаказы.Заказ
ИЗ
ВТ_СписокКонтрагентов КАК ВТ
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Продажи КАК ПоследниеЗаказы
ПО ПоследниеЗаказы.Регистратор В
(ВЫБРАТЬ ПЕРВЫЕ 4
Прод.Регистратор
ИЗ
РегистрНакопления.Продажи КАК Прод
ГДЕ
Прод.Контрагент = ВТ.Контрагент
УПОРЯДОЧИТЬ ПО
Прод.Период УБЫВ)
Преимущество этого метода в его лаконичности. Однако стоит учитывать, что в некоторых версиях платформы 1С и СУБД коррелированный запрос может выполняться медленно, если по полю Контрагент в физической таблице регистра отсутствует эффективный индекс.
Если ваша информационная база работает на современной платформе (8.3.20 и выше), забудьте о сложных соединениях. Теперь в языке запросов 1С поддерживаются оконные функции, которые идеально подходят для задачи «N лучших в группе».
Проанализируем пример с использованием функции ROW_NUMBER():
ВЫБРАТЬ
Продажи.Контрагент,
Продажи.Регистратор,
Продажи.Период,
ПОРЯДОК_НОМЕР() ОБРАБОТКА ПО (Продажи.Контрагент УПОРЯДОЧИТЬ ПО Продажи.Период УБЫВ) КАК НомерСтроки
ПОМЕСТИТЬ ВТ_НумерованныеПродажи
ИЗ
РегистрНакопления.Продажи КАК Продажи;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
т.Контрагент,
т.Регистратор КАК Заказ,
т.Период
ИЗ
ВТ_НумерованныеПродажи КАК т
ГДЕ
т.НомерСтроки <= 4
Почему это лучший вариант? Оконная функция выполняется СУБД за один проход по индексу. Это самый быстрый и современный способ, который не нагружает сервер лишними соединениями и временными таблицами.
Для высоконагруженных систем на старых платформах часто применяют метод «выбивания» максимальных значений через временные таблицы. Сначала находим самый последний заказ (максимум по дате), затем во вторую таблицу ищем заказ, дата которого меньше максимальной, и так далее.
Рассмотрим по шагам выборку первых двух заказов (по аналогии можно расширить до четырех):
ВТ1, куда помещаем МАКСИМУМ(Дата) по каждому контрагенту.ВТ2, соединяя основную таблицу с ВТ1 и выбирая МАКСИМУМ(Дата), которая строго меньше даты из ВТ1.ОБЪЕДИНИТЬ ВСЕ.Несмотря на громоздкость кода, этот метод работает очень стабильно, так как использует простые индексы и оператор MAX, который в SQL отрабатывает мгновенно.
Если задача стоит в рамках формирования отчета, мы можем переложить всю сложность на механизмы СКД. Для этого выясним два способа настройки:
ПЕРВЫЕ 4. В связях наборов передаем Контрагент как параметр. СКД сама выполнит отдельный быстрый запрос для каждого контрагента, что часто оказывается быстрее одного огромного сложного запроса.Независимо от выбранного метода, обратите внимание на следующие моменты:
Контрагент в документах или регистрах должно быть проиндексировано. В регистрах накопления 1С автоматически создает индексы по измерениям, но порядок измерений имеет значение.Продажи.Обороты помните, что обращение к полю Регистратор заставляет систему обращаться к основной таблице движений. Если вам нужны только даты и ссылки на документы, иногда быстрее работать с физической таблицей регистра или таблицей самого документа ЗаказПокупателя.Подведем итог: если платформа позволяет — используйте ПОРЯДОК_НОМЕР(). Если база огромная и старая — используйте коррелированные подзапросы или метод последовательных максимумов. Для отчетов идеальным выбором станет СКД с ограничением количества записей в группировке.