Как настроить отбор в динамическом списке по реквизитам табличных частей в 1С?

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

В процессе разработки интерфейсов на платформе 1С предприятие 8.3 программисты регулярно сталкиваются со стандартным ограничением платформы: динамический список не позволяет напрямую и без потери производительности накладывать отборы на реквизиты табличных частей документов. Платформа считывает табличные части как вложенные выборки, из-за чего стандартные механизмы фильтрации «из коробки» не могут работать с ними напрямую.

Если решать задачу стандартным «лобовым» методом — например, просто связать в произвольном запросе динамического списка основную таблицу и ее табличную часть через обычное ЛЕВОЕ СОЕДИНЕНИЕ, — то при росте базы данных мы неизбежно столкнемся с критическим падением производительности. Система начнет соединять огромные таблицы при каждой прокрутке списка, что приведет к зависанию интерфейса пользователей.

В этой статье мы подробно и по шагам разберем самые эффективные, надежные и архитектурно правильные способы решения этой задачи. Мы проанализируем ситуацию, выясним внутренние причины ограничений платформы и реализуем несколько вариантов обхода этой проблемы — от простых приемов денормализации данных до продвинутого проектирования на уровне СУБД.

Выясним причину: почему стандартный отбор по табличным частям тормозит?

Прежде чем перейти к практике, давайте разберем физический смысл проблемы. Механизм динамического списка работает по принципу порционного считывания данных с помощью курсора СУБД. Он выбирает ровно столько записей (например, 45 строк), сколько помещается на экране пользователя в данный момент.

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

Давайте рассмотрим четыре практических метода реализации быстрого отбора, которые исключают эти просадки.

Метод 1. Настройка произвольного запроса СКД с необязательным соединением

Этот метод является наиболее изящным и правильным с точки зрения платформы 1С. Мы заставим Систему компоновки данных (СКД), на которой построены динамические списки, выполнять соединение с табличной частью только тогда, когда пользователь действительно установил отбор.

Рассмотрим по шагам процесс настройки этого механизма:

  1. Откроем форму списка документов в конфигураторе.
  2. Перейдем в свойства реквизита формы, имеющего тип ДинамическийСписок (обычно это реквизит Список).
  3. Установим флаг Произвольный запрос и нажмем гиперссылку Настройка списка.
  4. В открывшемся окне в поле «Запрос» напишем текст левого соединения основной таблицы документа с его табличной частью:

ВЫБРАТЬ РАЗЛИЧНЫЕ
    ДокументПродажа.Ссылка КАК Ссылка,
    ДокументПродажа.ПометкаУдаления КАК ПометкаУдаления,
    ДокументПродажа.Номер КАК Номер,
    ДокументПродажа.Дата КАК Дата,
    ДокументПродажа.Проведен КАК Проведен
ИЗ
    Документ.РеализацияТоваровУслуг КАК ДокументПродажа
    {ЛЕВОЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК ТЧТовары
    ПО ДокументПродажа.Ссылка = ТЧТовары.Ссылка}

Обратите внимание на фигурные скобки {} в тексте запроса. Это необязательное описание связи компоновки данных. Но это еще не все. Нам нужно правильно сконфигурировать свойства полей компоновки.

  1. В нижней части окна редактирования запроса перейдем на вкладку Компоновка данных, а затем на вкладку Таблицы.
  2. В списке таблиц найдем нашу присоединенную табличную часть Документ.РеализацияТоваровУслуг.Товары и снимем флаг «Обязательно». Это ключевое действие!
  3. Перейдем на соседнюю вкладку Условия и добавим туда поле ТЧТовары.Номенклатура, назначив ему понятный псевдоним (например, НоменклатураОтбор).

Почему этот метод работает быстро: благодаря снятому флагу «Обязательно», платформа 1С при открытии формы сформирует легкий SQL-запрос к одной плоской таблице документов без лишних соединений. Тяжелый LEFT JOIN добавится в итоговый SQL-запрос только тогда, когда пользователь интерактивно включит отбор по полю НоменклатураОтбор в панели настроек списка.

Метод 2. Денормализация данных (подход «ЗУП» через сборное текстовое поле)

Посмотрим на пример из типовых конфигураций (таких как «Зарплата и Управление Персоналом»). Фирма «1С» часто решает проблему быстрого поиска по табличной части путем денормализации данных: ключевая информация из подчиненной таблицы при записи документа собирается в одно строковое поле шапки документа.

Давайте разберем, как реализовать этот подход самостоятельно:

  1. В метаданных документа добавим новый реквизит шапки с типом Строка (например, длиной 1000 символов или неограниченной длины). Назовем его КраткийСоставСотрудников или СтрокаПоискаНоменклатуры.
  2. В модуле объекта документа в обработчике события ПередЗаписью напишем код, который будет автоматически формировать эту строку при сохранении документа:

Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    // Выгружаем колонку из табличной части
    МассивСотрудников = Сотрудники.ВыгрузитьКолонку("Сотрудник");
    
    // Исключаем дубликаты значений для экономии места в строке
    СоответствиеУникальных = Новый Соответствие;
    Для Каждого ТекСотрудник Из МассивСотрудников Цикл
        Если ЗначениеЗаполнено(ТекСотрудник) Тогда
            СоответствиеУникальных.Вставить(ТекСотрудник, Истина);
        КонецЕсли;
    КонецЦикла;
    
    СтрокаСостава = "";
    Для Каждого КлючИЗначение Из СоответствиеУникальных Цикл
        СотрудникСсылка = КлючИЗначение.Ключ;
        СтрокаСостава = СтрокаСостава + ?(ПустаяСтрока(СтрокаСостава), "", ", ") + СотрудникСсылка.Наименование;
    КонецЦикла;
    
    // Записываем результат в наш индексируемый реквизит шапки
    КраткийСоставСотрудников = Лев(СтрокаСостава, 1000);
    
КонецПроцедуры

Теперь в динамическом списке нам достаточно вывести реквизит шапки КраткийСоставСотрудников. Пользователь сможет легко наложить стандартный отбор с видом сравнения Содержит.

Преимущества метода: невероятная скорость поиска и простота настройки динамического списка (запрос остается стандартным и очень быстрым).

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

Метод 3. Использование вспомогательного регистра сведений

Для крупных высоконагруженных баз данных, где требуется строгий отбор по ссылочному типу данных и важна абсолютная стабильность работы СУБД, мы применим самый надежный архитектурный паттерн — вспомогательный индексируемый регистр сведений.

Рассмотрим процесс реализации этой схемы по шагам:

  1. Создадим новый независимый или подчиненный регистратору Регистр сведений (назовем его, например, ИндексыДокументовДляПоиска).
  2. Определим следующую структуру метаданных регистра:
    • Измерение: Документ (тип: Ссылка на наш документ). В свойствах включим индексирование.
    • Измерение: Номенклатура (или Сотрудник, тип: соответствующий справочник-ссылка). В свойствах также обязательно включаем индексирование.
  3. В модуле документа (или в подписке на события ПриЗаписи) организуем автоматическое заполнение этого регистра:

Процедура ОбновитьИндексыПоискаДокумента(ДокументОбъект) Экспорт
    
    НаборЗаписей = РегистрыСведений.ИндексыДокументовДляПоиска.СоздатьНаборЗаписей();
    НаборЗаписей.Отбор.Документ.Установить(ДокументОбъект.Ссылка);
    
    // Получаем уникальные значения из табличной части, чтобы избежать дублирования строк в регистре
    ТаблицаТоваров = ДокументОбъект.Товары.Выгрузить();
    ТаблицаТоваров.Свернуть("Номенклатура");
    
    Для Каждого СтрокаТовар Из ТаблицаТоваров Цикл
        Если Не ЗначениеЗаполнено(СтрокаТовар.Номенклатура) Тогда
            Продолжить;
        КонецЕсли;
        
        НоваяЗапись = НаборЗаписей.Добавить();
        НоваяЗапись.Документ = ДокументОбъект.Ссылка;
        НоваяЗапись.Номенклатура = СтрокаТовар.Номенклатура;
    КонецЦикла;
    
    НаборЗаписей.Записать();
    
КонецПроцедуры
  1. В динамическом списке настраиваем произвольный запрос с необязательной связью (по аналогии с Методом 1), но соединяем таблицу документов не с тяжелой табличной частью, а с узким, плоским и проиндексированным регистром сведений РегистрыСведений.ИндексыДокументовДляПоиска.

Такой подход гарантирует моментальное выполнение отбора даже на терабайтных базах данных, поскольку СУБД будет работать с идеальными индексами плоских таблиц.

Метод 4. Динамический запрос и передача массива ссылок

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

Посмотрим на пример кода такой реализации при изменении поля отбора на форме:


&НаСервере
Процедура УстановитьОтборПоНоменклатуреНаСервере()
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ РАЗЛИЧНЫЕ
        |   ТЧТовары.Ссылка КАК Ссылка
        |ИЗ
        |   Документ.РеализацияТоваровУслуг.Товары КАК ТЧТовары
        |ГДЕ
        |   ТЧТовары.Номенклатура = &Номенклатура";
    
    Запрос.УстановитьПараметр("Номенклатура", ВыбраннаяНоменклатура);
    РезультатЗапроса = Запрос.Выполнить();
    
    Если РезультатЗапроса.Пустой() Тогда
        // Если ничего не найдено, передаем пустую ссылку, чтобы список очистился
        МассивДокументов = Новый Массив;
        МассивДокументов.Добавить(Документы.РеализацияТоваровУслуг.ПустаяСсылка()); 
    Иначе
        МассивДокументов = РезультатЗапроса.Выгрузить().ВыгрузитьКолонку("Ссылка");
    EndIf;
    
    // Очищаем старые отборы по ссылке и устанавливаем новый
    ГруппаОтбора = Список.Отбор.Элементы;
    ГруппаОтбора.Очистить();
    
    ЭлементОтбора = ГруппаОтбора.Добавить(Тип("ЭлементОтбораКомпоновкиДанных"));
    ЭлементОтбора.ЛевоеЗначение = Новый ПолеКомпоновкиДанных("Ссылка");
    ЭлементОтбора.ВидСравнения = ВидСравненияКомпоновкиДанных.ВСписке;
    ЭлементОтбора.ПравоеЗначение = МассивДокументов;
    ЭлементОтбора.Использование = Истина;
    
КонецПроцедуры

Важное предупреждение: этот метод на форумах часто называют «тормозным», и это чистая правда при определенных условиях. Если выбранная номенклатура (или сотрудник) содержится в 10 000 документов, массив будет содержать 10 000 элементов. Передача такого гигантского массива в параметр IN (...) SQL-запроса вызовет сильную просадку производительности СУБД. Применяйте этот метод только в небольших базах данных или при гарантированно малом количестве результатов поиска.

Подведем итоги и выберем оптимальное решение

Мы подробно проанализировали четыре основных подхода к реализации отборов по реквизитам табличных частей. Какой же метод выбрать для вашей задачи?

← На главную