Нередко разработчики 1С сталкиваются с парадоксальной ситуацией: при сужении периода отбора запрос начинает выполняться значительно дольше, чем при выборке данных за целый день. Например, выборка за 8 часов может выполняться 40 секунд, а аналогичная выборка за 24 часа — всего 1 секунду. Проанализируем природу этого поведения и разберем по шагам, как правильно оптимизировать подобные запросы.
Основная причина такого поведения кроется в механизме оценки количества строк (Cardinality Estimation) и формировании плана выполнения на стороне СУБД. Оптимизатор СУБД выбирает алгоритмы соединений на основе ожидаемого объема данных:
Посмотрим на исходный запрос и выявим узкие места, которые ломают оптимизацию СУБД:
НАЧАЛОПЕРИОДА(ЗапланированноеВремяВыполненияНазначения, ДЕНЬ) = &Дата. Оборачивание поля таблицы в функцию полностью отключает поиск по диапазону индекса (Index Seek), заставляя СУБД сканировать всю таблицу и вычислять функцию для каждой записи.ЛистВрачебныхНазначений.МедицинскаяКарта.ДатаЗакрытия внутри секции соединения неявно добавляет левое соединение с таблицей объекта. Наличие разыменования внутри логических условий соединения и веток ВЫБОР усложняет предикаты для СУБД.ИЛИ и конструкция ВЫБОР в секции соединения: наличие ветвления в условии ПО не позволяет оптимизатору применить быстрое соединение слиянием или хэшированием, принудительно переводя СУБД в режим медленных вложенных циклов.НАЧАЛОПЕРИОДА(&ДатаВремяНачала, МИНУТА) создают лишние скалярные вычисления. Значения параметров необходимо приводить к нужным границам во встроенном языке до передачи в запрос.ПО, а не в параметры виртуальной таблицы среза. В результате срез последних рассчитывается по избыточному объему данных, а фильтрация применяется уже после.Разберем, как последовательно переписать запрос, чтобы добиться стабильного времени выполнения вне зависимости от переданного интервала дат.
Шаг 1. Подготовка параметров во встроенном языке
Исключим вызовы функций приведения дат из текста запроса. Подготовим границы периодов заранее:
ГраницаНачала = НачалоМинуты(ДатаВремяНачала);
ГраницаОкончания = НачалоМинуты(ДатаВремяОкончания);
ДатаНачалаДня = НачалоДня(Дата);
ДатаКонецДня = КонецДня(Дата);
Запрос.УстановитьПараметр("ГраницаНачала", ГраницаНачала);
Запрос.УстановитьПараметр("ГраницаОкончания", ГраницаОкончания);
Запрос.УстановитьПараметр("ДатаНачалаДня", ДатаНачалаДня);
Запрос.УстановитьПараметр("ДатаКонецДня", ДатаКонецДня);
Шаг 2. Разделение запроса на временные таблицы
Вынесем соединение с промежуточной таблицей назначений в отдельный шаг и обязательно проиндексируем поля связи. Это предотвратит построение некорректных планов соединений оптимизатором СУБД.
ВЫБРАТЬ
ЛистВрачебныхНазначений.УникальныйИдентификаторНазначения КАК УникальныйИдентификаторНазначения,
ЛистВрачебныхНазначений.МедицинскаяКарта КАК МедицинскаяКарта,
ЛистВрачебныхНазначений.МедицинскаяКарта.ДатаЗакрытия КАК ДатаЗакрытия
ПОМЕСТИТЬ втОтобранныеНазначения
ИЗ
РегистрСведений.ЛистВрачебныхНазначенийСтационар.СрезПоследних(&Период, ) КАК ЛистВрачебныхНазначений
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_НазначенияНаТекущуюДату КАК ВТ_НазначенияНаТекущуюДату
ПО ЛистВрачебныхНазначений.МедицинскаяКарта = ВТ_НазначенияНаТекущуюДату.МедицинскаяКарта
ИНДЕКСИРОВАТЬ ПО
УникальныйИдентификаторНазначения
;
Шаг 3. Перенос отборов в параметры виртуальной таблицы без функций
Вместо функции НАЧАЛОПЕРИОДА применим стандартное диапазонное условие, которое задействует индексы физической таблицы регистра:
ВЫБРАТЬ
ВремяВыполненияСтационар.Период КАК Период,
ВремяВыполненияСтационар.Регистратор КАК Регистратор,
ВремяВыполненияСтационар.УникальныйИдентификаторНазначения КАК УникальныйИдентификаторНазначения,
ВремяВыполненияСтационар.ЗапланированноеВремяВыполненияНазначения КАК ЗапланированноеВремяВыполненияНазначения,
ВремяВыполненияСтационар.СтатусНазначения КАК СтатусНазначения,
ВремяВыполненияСтационар.РазоваяДозаИсполнено КАК РазоваяДозаИсполнено,
ВремяВыполненияСтационар.Комментарий КАК Комментарий,
ВремяВыполненияСтационар.ВремяВыполненияНазначения КАК ВремяВыполненияНазначения,
ВремяВыполненияСтационар.Исполнитель КАК Исполнитель
ПОМЕСТИТЬ втЛистВрачебныхНазначенийВремяВыполненияСтационар
ИЗ
РегистрСведений.ЛистВрачебныхНазначенийВремяВыполненияСтационар.СрезПоследних(
&Период,
ЗапланированноеВремяВыполненияНазначения >= &ДатаНачалаДня
И ЗапланированноеВремяВыполненияНазначения <= &ДатаКонецДня
И ЗапланированноеВремяВыполненияНазначения >= &ГраницаНачала
И ЗапланированноеВремяВыполненияНазначения < &ГраницаОкончания) КАК ВремяВыполненияСтационар
ВНУТРЕННЕЕ СОЕДИНЕНИЕ втОтобранныеНазначения КАК Назначения
ПО ВремяВыполненияСтационар.УникальныйИдентификаторНазначения = Назначения.УникальныйИдентификаторНазначения
ГДЕ
(Назначения.ДатаЗакрытия = ДАТАВРЕМЯ(1, 1, 1)
ИЛИ ВремяВыполненияСтационар.ЗапланированноеВремяВыполненияНазначения <= Назначения.ДатаЗакрытия
ИЛИ ВремяВыполненияСтационар.СтатусНазначения = ЗНАЧЕНИЕ(Перечисление.СтатусыЛекарственногоНазначения.Выполнено))
;
Шаг 4. Индексация временных таблиц
Убедимся, что все исходные временные таблицы, участвующие в соединениях (например, ВТ_НазначенияНаТекущуюДату), содержат директиву ИНДЕКСИРОВАТЬ ПО для полей связи (в данном случае МедицинскаяКарта). Это гарантирует, что оптимизатор СУБД всегда будет использовать индексные операции и перестанет зависеть от перепадов селективности параметров.