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