В процессе разработки интерфейсов на платформе 1С предприятие 8.3 программисты регулярно сталкиваются со стандартным ограничением платформы: динамический список не позволяет напрямую и без потери производительности накладывать отборы на реквизиты табличных частей документов. Платформа считывает табличные части как вложенные выборки, из-за чего стандартные механизмы фильтрации «из коробки» не могут работать с ними напрямую.
Если решать задачу стандартным «лобовым» методом — например, просто связать в произвольном запросе динамического списка основную таблицу и ее табличную часть через обычное ЛЕВОЕ СОЕДИНЕНИЕ, — то при росте базы данных мы неизбежно столкнемся с критическим падением производительности. Система начнет соединять огромные таблицы при каждой прокрутке списка, что приведет к зависанию интерфейса пользователей.
В этой статье мы подробно и по шагам разберем самые эффективные, надежные и архитектурно правильные способы решения этой задачи. Мы проанализируем ситуацию, выясним внутренние причины ограничений платформы и реализуем несколько вариантов обхода этой проблемы — от простых приемов денормализации данных до продвинутого проектирования на уровне СУБД.
Прежде чем перейти к практике, давайте разберем физический смысл проблемы. Механизм динамического списка работает по принципу порционного считывания данных с помощью курсора СУБД. Он выбирает ровно столько записей (например, 45 строк), сколько помещается на экране пользователя в данный момент.
Когда мы связываем документ с его табличной частью (где у одного документа может быть от одной до нескольких тысяч строк), результирующая выборка на уровне SQL-запроса «раздваивается». Возникает дублирование строк документов. Чтобы избавиться от дублей, разработчики вынуждены использовать операторы ВЫБРАТЬ РАЗЛИЧНЫЕ или группировку СГРУППИРОВАТЬ ПО. СУБД в этот момент теряет возможность использовать эффективные индексы для порционной выборки и начинает сканировать таблицы целиком. На больших базах данных это приводит к катастрофическому замедлению работы формы списка.
Давайте рассмотрим четыре практических метода реализации быстрого отбора, которые исключают эти просадки.
Этот метод является наиболее изящным и правильным с точки зрения платформы 1С. Мы заставим Систему компоновки данных (СКД), на которой построены динамические списки, выполнять соединение с табличной частью только тогда, когда пользователь действительно установил отбор.
Рассмотрим по шагам процесс настройки этого механизма:
ДинамическийСписок (обычно это реквизит Список).
ВЫБРАТЬ РАЗЛИЧНЫЕ
ДокументПродажа.Ссылка КАК Ссылка,
ДокументПродажа.ПометкаУдаления КАК ПометкаУдаления,
ДокументПродажа.Номер КАК Номер,
ДокументПродажа.Дата КАК Дата,
ДокументПродажа.Проведен КАК Проведен
ИЗ
Документ.РеализацияТоваровУслуг КАК ДокументПродажа
{ЛЕВОЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК ТЧТовары
ПО ДокументПродажа.Ссылка = ТЧТовары.Ссылка}
Обратите внимание на фигурные скобки {} в тексте запроса. Это необязательное описание связи компоновки данных. Но это еще не все. Нам нужно правильно сконфигурировать свойства полей компоновки.
Документ.РеализацияТоваровУслуг.Товары и снимем флаг «Обязательно». Это ключевое действие!ТЧТовары.Номенклатура, назначив ему понятный псевдоним (например, НоменклатураОтбор).Почему этот метод работает быстро: благодаря снятому флагу «Обязательно», платформа 1С при открытии формы сформирует легкий SQL-запрос к одной плоской таблице документов без лишних соединений. Тяжелый LEFT JOIN добавится в итоговый SQL-запрос только тогда, когда пользователь интерактивно включит отбор по полю НоменклатураОтбор в панели настроек списка.
Посмотрим на пример из типовых конфигураций (таких как «Зарплата и Управление Персоналом»). Фирма «1С» часто решает проблему быстрого поиска по табличной части путем денормализации данных: ключевая информация из подчиненной таблицы при записи документа собирается в одно строковое поле шапки документа.
Давайте разберем, как реализовать этот подход самостоятельно:
Строка (например, длиной 1000 символов или неограниченной длины). Назовем его КраткийСоставСотрудников или СтрокаПоискаНоменклатуры.ПередЗаписью напишем код, который будет автоматически формировать эту строку при сохранении документа:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Выгружаем колонку из табличной части
МассивСотрудников = Сотрудники.ВыгрузитьКолонку("Сотрудник");
// Исключаем дубликаты значений для экономии места в строке
СоответствиеУникальных = Новый Соответствие;
Для Каждого ТекСотрудник Из МассивСотрудников Цикл
Если ЗначениеЗаполнено(ТекСотрудник) Тогда
СоответствиеУникальных.Вставить(ТекСотрудник, Истина);
КонецЕсли;
КонецЦикла;
СтрокаСостава = "";
Для Каждого КлючИЗначение Из СоответствиеУникальных Цикл
СотрудникСсылка = КлючИЗначение.Ключ;
СтрокаСостава = СтрокаСостава + ?(ПустаяСтрока(СтрокаСостава), "", ", ") + СотрудникСсылка.Наименование;
КонецЦикла;
// Записываем результат в наш индексируемый реквизит шапки
КраткийСоставСотрудников = Лев(СтрокаСостава, 1000);
КонецПроцедуры
Теперь в динамическом списке нам достаточно вывести реквизит шапки КраткийСоставСотрудников. Пользователь сможет легко наложить стандартный отбор с видом сравнения Содержит.
Преимущества метода: невероятная скорость поиска и простота настройки динамического списка (запрос остается стандартным и очень быстрым).
Недостатки метода: избыточность хранения данных в базе, а также невозможность использовать строгий отбор по ссылке (пользователь ищет по текстовому вхождению, а не выбирает конкретный элемент справочника из формы выбора).
Для крупных высоконагруженных баз данных, где требуется строгий отбор по ссылочному типу данных и важна абсолютная стабильность работы СУБД, мы применим самый надежный архитектурный паттерн — вспомогательный индексируемый регистр сведений.
Рассмотрим процесс реализации этой схемы по шагам:
ИндексыДокументовДляПоиска).Документ (тип: Ссылка на наш документ). В свойствах включим индексирование.Номенклатура (или Сотрудник, тип: соответствующий справочник-ссылка). В свойствах также обязательно включаем индексирование.ПриЗаписи) организуем автоматическое заполнение этого регистра:
Процедура ОбновитьИндексыПоискаДокумента(ДокументОбъект) Экспорт
НаборЗаписей = РегистрыСведений.ИндексыДокументовДляПоиска.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Документ.Установить(ДокументОбъект.Ссылка);
// Получаем уникальные значения из табличной части, чтобы избежать дублирования строк в регистре
ТаблицаТоваров = ДокументОбъект.Товары.Выгрузить();
ТаблицаТоваров.Свернуть("Номенклатура");
Для Каждого СтрокаТовар Из ТаблицаТоваров Цикл
Если Не ЗначениеЗаполнено(СтрокаТовар.Номенклатура) Тогда
Продолжить;
КонецЕсли;
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Документ = ДокументОбъект.Ссылка;
НоваяЗапись.Номенклатура = СтрокаТовар.Номенклатура;
КонецЦикла;
НаборЗаписей.Записать();
КонецПроцедуры
РегистрыСведений.ИндексыДокументовДляПоиска.Такой подход гарантирует моментальное выполнение отбора даже на терабайтных базах данных, поскольку СУБД будет работать с идеальными индексами плоских таблиц.
Иногда разработчики используют более простой программный подход: сначала на сервере выполняется запрос к табличной части для получения списка документов, удовлетворяющих условию, а затем полученный массив ссылок передается в отбор динамического списка.
Посмотрим на пример кода такой реализации при изменении поля отбора на форме:
&НаСервере
Процедура УстановитьОтборПоНоменклатуреНаСервере()
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| ТЧТовары.Ссылка КАК Ссылка
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК ТЧТовары
|ГДЕ
| ТЧТовары.Номенклатура = &Номенклатура";
Запрос.УстановитьПараметр("Номенклатура", ВыбраннаяНоменклатура);
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
// Если ничего не найдено, передаем пустую ссылку, чтобы список очистился
МассивДокументов = Новый Массив;
МассивДокументов.Добавить(Документы.РеализацияТоваровУслуг.ПустаяСсылка());
Иначе
МассивДокументов = РезультатЗапроса.Выгрузить().ВыгрузитьКолонку("Ссылка");
EndIf;
// Очищаем старые отборы по ссылке и устанавливаем новый
ГруппаОтбора = Список.Отбор.Элементы;
ГруппаОтбора.Очистить();
ЭлементОтбора = ГруппаОтбора.Добавить(Тип("ЭлементОтбораКомпоновкиДанных"));
ЭлементОтбора.ЛевоеЗначение = Новый ПолеКомпоновкиДанных("Ссылка");
ЭлементОтбора.ВидСравнения = ВидСравненияКомпоновкиДанных.ВСписке;
ЭлементОтбора.ПравоеЗначение = МассивДокументов;
ЭлементОтбора.Использование = Истина;
КонецПроцедуры
Важное предупреждение: этот метод на форумах часто называют «тормозным», и это чистая правда при определенных условиях. Если выбранная номенклатура (или сотрудник) содержится в 10 000 документов, массив будет содержать 10 000 элементов. Передача такого гигантского массива в параметр IN (...) SQL-запроса вызовет сильную просадку производительности СУБД. Применяйте этот метод только в небольших базах данных или при гарантированно малом количестве результатов поиска.
Мы подробно проанализировали четыре основных подхода к реализации отборов по реквизитам табличных частей. Какой же метод выбрать для вашей задачи?