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

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

В практике разработки на платформе «1С:Предприятие 8» часто встречается задача поиска наиболее подходящей записи в регистре сведений по цепочке приоритетов. Например, когда нам нужно найти запись по трем заполненным реквизитам, а если ее нет — по двум, а в крайнем случае — только по одному общему реквизиту. Подобная логика «смягчения поиска» при наивной реализации в лоб (последовательное выполнение нескольких запросов) приводит к катастрофическому падению производительности. Время выполнения кода может достигать неприемлемых 5–6 секунд и более.

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

В чем заключается проблема исходного алгоритма?

Рассмотрим логику работы исходного кода. Перед разработчиком стоит следующая задача: есть три реквизита поиска — НомерПроживания, КомнатаПроживания и КойкаПроживания. Нам необходимо получить данные из регистра сведений ЗагруженностьНФ по следующим правилам:

  1. Сначала ищем самую общую запись, где заполнен только НомерПроживания, а комната и койка пусты.
  2. Если таких записей нет, ищем более детальную запись, где совпадают НомерПроживания и КомнатаПроживания, а койка остается пустой.
  3. Если и таких записей нет, выполняем поиск по точному совпадению всех трех полей: НомерПроживания, КомнатаПроживания и КойкаПроживания.

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


Решение 1. Объединение запросов с расчетом приоритета (оптимальный запрос)

Вместо того чтобы дергать базу данных несколько раз, мы можем получить все возможные варианты за один единственный запрос. Для этого мы укажем в условиях отбора ГДЕ все допустимые комбинации параметров (используя оператор В), а в самом запросе рассчитаем поле Приоритет с помощью конструкции ВЫБОР КОГДА.

Рассмотрим структуру такого запроса. Мы присвоим приоритет 1 наиболее приоритетному для нас результату (когда заполнен только номер), приоритет 2 — среднему (номер + комната), и приоритет 3 — самому детальному (все три параметра). Затем мы упорядочим результат по приоритету и прочитаем только ту группу записей, которая вернулась с наименьшим числовым значением приоритета.

Посмотрим на пример реализации такого запроса:


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|	Т.Регистратор КАК Регистратор,
|	Т.ДатаЗаселения КАК ДатаНачала,
|	Т.ДатаВыезда КАК ДатаОкончания,
|	ВЫБОР
|		КОГДА Т.КомнатаПроживания = ЗНАЧЕНИЕ(Справочник.Комнаты.ПустаяСсылка)
|				И Т.КойкаПроживания = ЗНАЧЕНИЕ(Справочник.Койки.ПустаяСсылка)
|			ТОГДА 1
|		КОГДА Т.КомнатаПроживания = &КомнатаПроживания
|				И Т.КойкаПроживания = ЗНАЧЕНИЕ(Справочник.Койки.ПустаяСсылка)
|			ТОГДА 2
|		КОГДА Т.КомнатаПроживания = &КомнатаПроживания
|				И Т.КойкаПроживания = &КойкаПроживания
|			ТОГДА 3
|		ИНАЧЕ 0
|	КОНЕЦ КАК Приоритет
|ИЗ
|	РегистрСведений.ЗагруженностьНФ КАК Т
|ГДЕ
|	Т.НомерПроживания = &НомерПроживания
|	И Т.КомнатаПроживания В (ЗНАЧЕНИЕ(Справочник.Комнаты.ПустаяСсылка), &КомнатаПроживания)
|	И Т.КойкаПроживания В (ЗНАЧЕНИЕ(Справочник.Койки.ПустаяСсылка), &КойкаПроживания)
|
|УПОРЯДОЧИТЬ ПО
|	Приоритет,
|	ДатаНачала,
|	ДатаОкончания
|ИТОГИ ПО
|	Приоритет";

Запрос.УстановитьПараметр("НомерПроживания", ПараметрыПоиска.НомерПроживания);
Запрос.УстановитьПараметр("КомнатаПроживания", ПараметрыПоиска.КомнатаПроживания);
Запрос.УстановитьПараметр("КойкаПроживания", ПараметрыПоиска.КойкаПроживания);

РезультатЗапроса = Запрос.Выполнить();

Обратите внимание на предложение ИТОГИ ПО Приоритет. Оно позволяет нам сгруппировать записи по их важности. В коде обхода выборки мы прочитаем только первую группировку, которая будет соответствовать самому высокому доступному приоритету из найденных в базе данных. Таким образом, если в базе есть записи с приоритетом 1, мы выберем только их и проигнорируем приоритеты 2 и 3.

Разберем по шагам, как правильно написать обход такой выборки во встроенном языке:


// Создаем результирующую таблицу значений
СЗ = Новый ТаблицаЗначений;
СЗ.Колонки.Добавить("Регистратор");
СЗ.Колонки.Добавить("ДатаНачала");
СЗ.Колонки.Добавить("ДатаОкончания");

ВыборкаПриоритет = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

// Берем только ПЕРВУЮ успешную группировку (минимальный найденный приоритет)
Если ВыборкаПриоритет.Следующий() Тогда
	
	ВыборкаДетальныеЗаписи = ВыборкаПриоритет.Выбрать();
	Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
		НСТР = СЗ.Добавить();
		ЗаполнитьЗначенияСвойств(НСТР, ВыборкаДетальныеЗаписи);
	КонецЦикла;
	
КонецЕсли;

Используя конструкцию Если ВыборкаПриоритет.Следующий() Тогда вместо цикла Пока, мы гарантируем, что в итоговую таблицу попадут только те записи, которые относятся к первому сработавшему уровню детализации. Если нашлись записи с приоритетом 1, алгоритм заполнит таблицу и завершит работу, не спускаясь к приоритетам 2 и 3.


Решение 2. Оптимизация на уровне временных таблиц (без итогов в 1С)

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

Проанализируем этот подход. Алгоритм в запросе будет следующим:

  1. Выбираем все подходящие записи с расчетом приоритета во временную таблицу ВТ_ВсеЗаписи.
  2. Вторым пакетом находим минимальное значение приоритета, которое реально присутствует в таблице ВТ_ВсеЗаписи, и сохраняем его во временную таблицу ВТ_ЦелевойПриоритет.
  3. Третьим пакетом выбираем из ВТ_ВсеЗаписи только те строки, у которых поле Приоритет совпадает со значением из таблицы ВТ_ЦелевойПриоритет.

Посмотрим на пример реализации такого пакетного запроса:


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|	Т.Регистратор КАК Регистратор,
|	Т.ДатаЗаселения КАК ДатаНачала,
|	Т.ДатаВыезда КАК ДатаОкончания,
|	ВЫБОР
|		КОГДА Т.КомнатаПроживания = ЗНАЧЕНИЕ(Справочник.Комнаты.ПустаяСсылка)
|				И Т.КойкаПроживания = ЗНАЧЕНИЕ(Справочник.Койки.ПустаяСсылка)
|			ТОГДА 1
|		КОГДА Т.КомнатаПроживания = &КомнатаПроживания
|				И Т.КойкаПроживания = ЗНАЧЕНИЕ(Справочник.Койки.ПустаяСсылка)
|			ТОГДА 2
|		КОГДА Т.КомнатаПроживания = &КомнатаПроживания
|				И Т.КойкаПроживания = &КойкаПроживания
|			ТОГДА 3
|		ИНАЧЕ 0
|	КОНЕЦ КАК Приоритет
|ПОМЕСТИТЬ ВТ_ВсеЗаписи
|ИЗ
|	РегистрСведений.ЗагруженностьНФ КАК Т
|ГДЕ
|	Т.НомерПроживания = &НомерПроживания
|	И Т.КомнатаПроживания В (ЗНАЧЕНИЕ(Справочник.Комнаты.ПустаяСсылка), &КомнатаПроживания)
|	И Т.КойкаПроживания В (ЗНАЧЕНИЕ(Справочник.Койки.ПустаяСсылка), &КойкаПроживания)
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	МИНИМУМ(ВТ_ВсеЗаписи.Приоритет) КАК ЦелевойПриоритет
|ПОМЕСТИТЬ ВТ_ЦелевойПриоритет
|ИЗ
|	ВТ_ВсеЗаписи КАК ВТ_ВсеЗаписи
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	ВТ_ВсеЗаписи.Регистратор КАК Регистратор,
|	ВТ_ВсеЗаписи.ДатаНачала КАК ДатаНачала,
|	ВТ_ВсеЗаписи.ДатаОкончания КАК ДатаОкончания
|ИЗ
|	ВТ_ВсеЗаписи КАК ВТ_ВсеЗаписи
|	ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ЦелевойПриоритет КАК ВТ_ЦелевойПриоритет
|		ПО ВТ_ВсеЗаписи.Приоритет = ВТ_ЦелевойПриоритет.ЦелевойПриоритет
|УПОРЯДОЧИТЬ ПО
|	ДатаНачала,
|	ДатаОкончания";

Запрос.УстановитьПараметр("НомерПроживания", ПараметрыПоиска.НомерПроживания);
Запрос.УстановитьПараметр("КомнатаПроживания", ПараметрыПоиска.КомнатаПроживания);
Запрос.УстановитьПараметр("КойкаПроживания", ПараметрыПоиска.КойкаПроживания);

// Теперь мы можем просто выгрузить результат напрямую в Таблицу Значений!
СЗ = Запрос.Выполнить().Выгрузить();

Этот вариант является наиболее изящным и производительным с точки зрения СУБД. В СУБД уходят временные таблицы, индексы которых оптимизируют поиск, а на сервер 1С возвращается идеально отфильтрованный плоский результат. Код на встроенном языке сжимается всего до одной строки выгрузки результата!


Решение 3. Оптимизация клиент-серверных вызовов

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

Для устранения задержек при передаче данных между сервером и клиентом необходимо внедрить два правила:

  1. Использовать директиву &НаСервереБезКонтекста: При вызове контекстного метода формы &НаСервере платформа сериализует все данные формы и передает их на сервер. Если мы заменим его на внеконтекстный вызов, на сервер будут переданы только те параметры, которые мы явно указали в скобках вызова функции.
  2. Не передавать мутабельные объекты: Объект типа ТаблицаЗначений является мутабельным. Его нельзя передавать напрямую на клиент в управляемом приложении. Нам необходимо выгрузить результат запроса в обычный массив структур (немутабельный тип) и передать его.

Рассмотрим пример правильной организации серверной функции в модуле управляемой формы:


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

Такой подход полностью убирает накладные расходы. Передача простого массива структур выполняется мгновенно даже на медленных интернет-соединениях.


Решение 4. Настройка индексов регистра и использование виртуальных таблиц

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

Если вам нужны только актуальные на данный момент сведения, необходимо использовать виртуальную таблицу СрезПоследних. При этом крайне важно накладывать условия отбора непосредственно в параметрах виртуальной таблицы, а не в предложении ГДЕ:


// ПЛОХО: СУБД сначала построит срез последних по ВСЕЙ базе данных, а потом отфильтрует его по номеру
|ИЗ РегистрСведений.ЗагруженностьНФ.СрезПоследних(&Период, ) КАК Т
|ГДЕ Т.НомерПроживания = &НомерПроживания

// ХОРОШО: СУБД сразу выберет срез последних только для нужного Номера проживания
|ИЗ РегистрСведений.ЗагруженностьНФ.СрезПоследних(&Период, НомерПроживания = &НомерПроживания) КАК Т

Также важно уделить внимание структуре индексации. СУБД автоматически создает индекс по измерениям регистра сведений в порядке их следования в конфигураторе (слева направо). Чтобы эффективно отрабатывал отбор по полям НомерПроживания, КомнатаПроживания и КойкаПроживания, они должны быть расположены в структуре измерений регистра именно в таком порядке — от более общего к частному:

  1. НомерПроживания (первое измерение)
  2. КомнатаПроживания (второе измерение)
  3. КойкаПроживания (третье измерение)

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

Используя предложенные методики (объединение запросов через временные таблицы с расчетом приоритета, перевод вызовов в &НаСервереБезКонтекста и правильную работу с индексами), вы легко сократите время выполнения подобных задач с 6.5 секунд до сотых долей секунды!

← На главную