В практике разработки на платформе «1С:Предприятие 8» часто встречается задача поиска наиболее подходящей записи в регистре сведений по цепочке приоритетов. Например, когда нам нужно найти запись по трем заполненным реквизитам, а если ее нет — по двум, а в крайнем случае — только по одному общему реквизиту. Подобная логика «смягчения поиска» при наивной реализации в лоб (последовательное выполнение нескольких запросов) приводит к катастрофическому падению производительности. Время выполнения кода может достигать неприемлемых 5–6 секунд и более.
В данной статье мы подробно разберем, почему возникает эта проблема, проанализируем узкие места исходного кода и реализуем правильные, высокопроизводительные архитектурные решения на уровне СУБД и клиент-серверного взаимодействия.
Рассмотрим логику работы исходного кода. Перед разработчиком стоит следующая задача: есть три реквизита поиска — НомерПроживания, КомнатаПроживания и КойкаПроживания. Нам необходимо получить данные из регистра сведений ЗагруженностьНФ по следующим правилам:
НомерПроживания, а комната и койка пусты.НомерПроживания и КомнатаПроживания, а койка остается пустой.НомерПроживания, КомнатаПроживания и КойкаПроживания.В исходном коде для реализации этой логики разработчик запускает один и тот же запрос до трех раз подряд, меняя параметры отбора в зависимости от результата предыдущего выполнения. Это порождает сразу три серьезных проблемы производительности:
ТаблицаЗначений через клиент-серверную границу вызывает длительную сериализацию данных.Вместо того чтобы дергать базу данных несколько раз, мы можем получить все возможные варианты за один единственный запрос. Для этого мы укажем в условиях отбора ГДЕ все допустимые комбинации параметров (используя оператор В), а в самом запросе рассчитаем поле Приоритет с помощью конструкции ВЫБОР КОГДА.
Рассмотрим структуру такого запроса. Мы присвоим приоритет 1 наиболее приоритетному для нас результату (когда заполнен только номер), приоритет 2 — среднему (номер + комната), и приоритет 3 — самому детальному (все три параметра). Затем мы упорядочим результат по приоритету и прочитаем только ту группу записей, которая вернулась с наименьшим числовым значением приоритета.
Посмотрим на пример реализации такого запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Т.Регистратор КАК Регистратор,
| Т.ДатаЗаселения КАК ДатаНачала,
| Т.ДатаВыезда КАК ДатаОкончания,
| ВЫБОР
| КОГДА Т.КомнатаПроживания = ЗНАЧЕНИЕ(Справочник.Комнаты.ПустаяСсылка)
| И Т.КойкаПроживания = ЗНАЧЕНИЕ(Справочник.Койки.ПустаяСсылка)
| ТОГДА 1
| КОГДА Т.КомнатаПроживания = &КомнатаПроживания
| И Т.КойкаПроживания = ЗНАЧЕНИЕ(Справочник.Койки.ПустаяСсылка)
| ТОГДА 2
| КОГДА Т.КомнатаПроживания = &КомнатаПроживания
| И Т.КойкаПроживания = &КойкаПроживания
| ТОГДА 3
| ИНАЧЕ 0
| КОНЕЦ КАК Приоритет
|ИЗ
| РегистрСведений.ЗагруженностьНФ КАК Т
|ГДЕ
| Т.НомерПроживания = &НомерПроживания
| И Т.КомнатаПроживания В (ЗНАЧЕНИЕ(Справочник.Комнаты.ПустаяСсылка), &КомнатаПроживания)
| И Т.КойкаПроживания В (ЗНАЧЕНИЕ(Справочник.Койки.ПустаяСсылка), &КойкаПроживания)
|
|УПОРЯДОЧИТЬ ПО
| Приоритет,
| ДатаНачала,
| ДатаОкончания
|ИТОГИ ПО
| Приоритет";
Запрос.УстановитьПараметр("НомерПроживания", ПараметрыПоиска.НомерПроживания);
Запрос.УстановитьПараметр("КомнатаПроживания", ПараметрыПоиска.КомнатаПроживания);
Запрос.УстановитьПараметр("КойкаПроживания", ПараметрыПоиска.КойкаПроживания);
РезультатЗапроса = Запрос.Выполнить();
Обратите внимание на предложение ИТОГИ ПО Приоритет. Оно позволяет нам сгруппировать записи по их важности. В коде обхода выборки мы прочитаем только первую группировку, которая будет соответствовать самому высокому доступному приоритету из найденных в базе данных. Таким образом, если в базе есть записи с приоритетом 1, мы выберем только их и проигнорируем приоритеты 2 и 3.
Разберем по шагам, как правильно написать обход такой выборки во встроенном языке:
// Создаем результирующую таблицу значений
СЗ = Новый ТаблицаЗначений;
СЗ.Колонки.Добавить("Регистратор");
СЗ.Колонки.Добавить("ДатаНачала");
СЗ.Колонки.Добавить("ДатаОкончания");
ВыборкаПриоритет = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
// Берем только ПЕРВУЮ успешную группировку (минимальный найденный приоритет)
Если ВыборкаПриоритет.Следующий() Тогда
ВыборкаДетальныеЗаписи = ВыборкаПриоритет.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
НСТР = СЗ.Добавить();
ЗаполнитьЗначенияСвойств(НСТР, ВыборкаДетальныеЗаписи);
КонецЦикла;
КонецЕсли;
Используя конструкцию Если ВыборкаПриоритет.Следующий() Тогда вместо цикла Пока, мы гарантируем, что в итоговую таблицу попадут только те записи, которые относятся к первому сработавшему уровню детализации. Если нашлись записи с приоритетом 1, алгоритм заполнит таблицу и завершит работу, не спускаясь к приоритетам 2 и 3.
Предыдущее решение отлично работает, но использование обхода по группировкам на стороне сервера приложений 1С все же тратит ресурсы на построение и разбор дерева итогов. Мы можем переложить эту задачу целиком на плечи СУБД, используя временные таблицы. СУБД справится с фильтрацией приоритетов гораздо быстрее.
Проанализируем этот подход. Алгоритм в запросе будет следующим:
ВТ_ВсеЗаписи.ВТ_ВсеЗаписи, и сохраняем его во временную таблицу ВТ_ЦелевойПриоритет.ВТ_ВсеЗаписи только те строки, у которых поле Приоритет совпадает со значением из таблицы ВТ_ЦелевойПриоритет.Посмотрим на пример реализации такого пакетного запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Т.Регистратор КАК Регистратор,
| Т.ДатаЗаселения КАК ДатаНачала,
| Т.ДатаВыезда КАК ДатаОкончания,
| ВЫБОР
| КОГДА Т.КомнатаПроживания = ЗНАЧЕНИЕ(Справочник.Комнаты.ПустаяСсылка)
| И Т.КойкаПроживания = ЗНАЧЕНИЕ(Справочник.Койки.ПустаяСсылка)
| ТОГДА 1
| КОГДА Т.КомнатаПроживания = &КомнатаПроживания
| И Т.КойкаПроживания = ЗНАЧЕНИЕ(Справочник.Койки.ПустаяСсылка)
| ТОГДА 2
| КОГДА Т.КомнатаПроживания = &КомнатаПроживания
| И Т.КойкаПроживания = &КойкаПроживания
| ТОГДА 3
| ИНАЧЕ 0
| КОНЕЦ КАК Приоритет
|ПОМЕСТИТЬ ВТ_ВсеЗаписи
|ИЗ
| РегистрСведений.ЗагруженностьНФ КАК Т
|ГДЕ
| Т.НомерПроживания = &НомерПроживания
| И Т.КомнатаПроживания В (ЗНАЧЕНИЕ(Справочник.Комнаты.ПустаяСсылка), &КомнатаПроживания)
| И Т.КойкаПроживания В (ЗНАЧЕНИЕ(Справочник.Койки.ПустаяСсылка), &КойкаПроживания)
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| МИНИМУМ(ВТ_ВсеЗаписи.Приоритет) КАК ЦелевойПриоритет
|ПОМЕСТИТЬ ВТ_ЦелевойПриоритет
|ИЗ
| ВТ_ВсеЗаписи КАК ВТ_ВсеЗаписи
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_ВсеЗаписи.Регистратор КАК Регистратор,
| ВТ_ВсеЗаписи.ДатаНачала КАК ДатаНачала,
| ВТ_ВсеЗаписи.ДатаОкончания КАК ДатаОкончания
|ИЗ
| ВТ_ВсеЗаписи КАК ВТ_ВсеЗаписи
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ЦелевойПриоритет КАК ВТ_ЦелевойПриоритет
| ПО ВТ_ВсеЗаписи.Приоритет = ВТ_ЦелевойПриоритет.ЦелевойПриоритет
|УПОРЯДОЧИТЬ ПО
| ДатаНачала,
| ДатаОкончания";
Запрос.УстановитьПараметр("НомерПроживания", ПараметрыПоиска.НомерПроживания);
Запрос.УстановитьПараметр("КомнатаПроживания", ПараметрыПоиска.КомнатаПроживания);
Запрос.УстановитьПараметр("КойкаПроживания", ПараметрыПоиска.КойкаПроживания);
// Теперь мы можем просто выгрузить результат напрямую в Таблицу Значений!
СЗ = Запрос.Выполнить().Выгрузить();
Этот вариант является наиболее изящным и производительным с точки зрения СУБД. В СУБД уходят временные таблицы, индексы которых оптимизируют поиск, а на сервер 1С возвращается идеально отфильтрованный плоский результат. Код на встроенном языке сжимается всего до одной строки выгрузки результата!
Даже самый быстрый запрос в мире будет работать медленно, если вызывать его неправильно с точки зрения архитектуры управляемого приложения 1С. В сообщениях на форуме упоминается, что «запрос выполняется быстро, но тупит вызов функции». Выясним причину этой проблемы.
Для устранения задержек при передаче данных между сервером и клиентом необходимо внедрить два правила:
&НаСервереБезКонтекста: При вызове контекстного метода формы &НаСервере платформа сериализует все данные формы и передает их на сервер. Если мы заменим его на внеконтекстный вызов, на сервер будут переданы только те параметры, которые мы явно указали в скобках вызова функции.ТаблицаЗначений является мутабельным. Его нельзя передавать напрямую на клиент в управляемом приложении. Нам необходимо выгрузить результат запроса в обычный массив структур (немутабельный тип) и передать его.Рассмотрим пример правильной организации серверной функции в модуле управляемой формы:
&НаСервереБезКонтекста
Функция ПолучитьДанныеЗагруженностиНаСервере(Знач НомерПроживания, Знач КомнатаПроживания, Знач КойкаПроживания)
ПараметрыПоиска = Новый Структура;
ПараметрыПоиска.Вставить("НомерПроживания", НомерПроживания);
ПараметрыПоиска.Вставить("КомнатаПроживания", КомнатаПроживания);
ПараметрыПоиска.Вставить("КойкаПроживания", КойкаПроживания);
// Вызываем наш оптимизированный запрос (Решение 2)
ТаблицаРезультата = ВыполнитьЗапросПриоритета(ПараметрыПоиска);
// Преобразуем ТаблицуЗначений в массив структур для быстрой сериализации
МассивРезультата = Новый Массив;
Для Каждого СтрокаТЗ Из ТаблицаРезультата Цикл
СтрокаСтруктура = Новый Структура("Регистратор, ДатаНачала, ДатаОкончания");
ЗаполнитьЗначенияСвойств(СтрокаСтруктура, СтрокаТЗ);
МассивРезультата.Добавить(СтрокаСтруктура);
КонецЦикла;
Возврат МассивРезультата;
КонецФункции
Такой подход полностью убирает накладные расходы. Передача простого массива структур выполняется мгновенно даже на медленных интернет-соединениях.
В обсуждении на форуме справедливо отмечалось, что регистр сведений ЗагруженностьНФ, скорее всего, является периодическим. Если регистр периодический, то обращение к его основной физической таблице — это грубая ошибка. Физическая таблица хранит всю историю изменений за многие годы, из-за чего СУБД приходится перебирать миллионы записей.
Если вам нужны только актуальные на данный момент сведения, необходимо использовать виртуальную таблицу СрезПоследних. При этом крайне важно накладывать условия отбора непосредственно в параметрах виртуальной таблицы, а не в предложении ГДЕ:
// ПЛОХО: СУБД сначала построит срез последних по ВСЕЙ базе данных, а потом отфильтрует его по номеру
|ИЗ РегистрСведений.ЗагруженностьНФ.СрезПоследних(&Период, ) КАК Т
|ГДЕ Т.НомерПроживания = &НомерПроживания
// ХОРОШО: СУБД сразу выберет срез последних только для нужного Номера проживания
|ИЗ РегистрСведений.ЗагруженностьНФ.СрезПоследних(&Период, НомерПроживания = &НомерПроживания) КАК Т
Также важно уделить внимание структуре индексации. СУБД автоматически создает индекс по измерениям регистра сведений в порядке их следования в конфигураторе (слева направо). Чтобы эффективно отрабатывал отбор по полям НомерПроживания, КомнатаПроживания и КойкаПроживания, они должны быть расположены в структуре измерений регистра именно в таком порядке — от более общего к частному:
НомерПроживания (первое измерение)КомнатаПроживания (второе измерение)КойкаПроживания (третье измерение)При таком порядке измерений СУБД сможет использовать один единственный стандартный кластерный индекс для любого из наших приоритетных отборов, и вам не придется включать флаг «Индексировать» в конфигураторе, захламляя базу данных лишними индексами и замедляя запись в регистр.
Используя предложенные методики (объединение запросов через временные таблицы с расчетом приоритета, перевод вызовов в &НаСервереБезКонтекста и правильную работу с индексами), вы легко сократите время выполнения подобных задач с 6.5 секунд до сотых долей секунды!