Каждый разработчик 1С рано или поздно сталкивается с ситуацией, когда запрос, работающий в штатном режиме за доли секунды, вдруг начинает «зависать» на минуту или более. Особенно критично это становится, когда такой запрос выполняется в транзакции проведения важных документов, например, «Реализации товаров и услуг». В результате работа пользователей полностью блокируется, возникают таймауты и взаимные блокировки СУБД.
В этой статье мы подробно разберем, почему возникает плавающее время выполнения запроса к виртуальным таблицам остатков регистра бухгалтерии, как на это влияют внутренние механизмы СУБД и какими способами можно навсегда устранить эту проблему.
Рассмотрим исходный запрос, который периодически вызывает сильные задержки при проведении документов:
ТекстЗапроса = "
|ВЫБРАТЬ
| ВЫРАЗИТЬ(ПланСчетовОсновнойОстатки.Субконто3 как Справочник.СтавкиНДС) КАК СтавкаНДС,
| ПланСчетовОсновнойОстатки.СуммаОстатокДт КАК СуммаЛей,
| ПланСчетовОсновнойОстатки.ВалютнаяСуммаОстатокДт КАК СуммаВал
|ИЗ
| РегистрБухгалтерии.ПланСчетовОсновной.Остатки(
| &&ДатаКон,
| Счет = &СчетКт,
| ,
| Субконто1 = &Контрагент
| И Субконто2 = &ОсновнойДоговор
| И Субконто3 В (&СписокСтавокНДС)
| И ВЫБОР
| КОГДА &Валютный
| ТОГДА Валюта = &Валюта
| ИНАЧЕ ИСТИНА
| КОНЕЦ) КАК ПланСчетовОсновнойОстатки
|ГДЕ
| ВЫБОР
| КОГДА &Валютный
| ТОГДА ПланСчетовОсновнойОстатки.ВалютнаяСуммаОстатокДт < 0
| ИНАЧЕ ПланСчетовОсновнойОстатки.СуммаОстатокДт < 0
| КОНЕЦ
|";
На первый взгляд запрос выглядит вполне стандартно. Однако в нем кроется сразу несколько критических уязвимостей, которые заставляют планировщик СУБД совершать ошибки при построении плана выполнения. Проанализируем эти уязвимости по шагам.
Главным виновником нестабильного поведения запроса является использование конструкции ВЫБОР внутри параметров виртуальной таблицы и в секции ГДЕ. Когда СУБД (будь то MS SQL Server или PostgreSQL) впервые компилирует данный запрос, она анализирует переданные значения параметров — этот процесс называется Parameter Sniffing (прослушивание параметров).
Выясним, что происходит на уровне СУБД при разных сценариях:
&Валютный = Ложь. Оптимизатор СУБД строит план выполнения, ориентированный на работу только с рублевыми суммами. Этот план кэшируется для последующего использования.&Валютный = Истина. СУБД пытается сэкономить ресурсы на компиляции и берет уже готовый план из кэша. Но этот план был построен для рублевого учета! Он не учитывает необходимость фильтрации по валюте и валютным суммам.В результате СУБД вместо быстрого поиска по индексу начинает выполнять полное сканирование таблиц (Clustered Index Scan), что увеличивает время выполнения с миллисекунд до десятков секунд или даже минут.
Конструкция ВЫБОР транслируется на язык SQL в виде операторов CASE WHEN или сложных логических условий с OR. Такие конструкции не позволяют СУБД эффективно использовать индексы, так как вычисление условий происходит динамически для каждой строки таблицы.
Для того чтобы полностью исключить негативное влияние Parameter Sniffing, необходимо избавиться от условного ветвления внутри самого SQL-запроса. Сделать это нужно на стороне встроенного языка 1С с помощью динамического формирования текста запроса:
ТекстШаблона =
"ВЫБРАТЬ
| ВЫРАЗИТЬ(ПланСчетовОсновнойОстатки.Субконто3 как Справочник.СтавкиНДС) КАК СтавкаНДС,
| ПланСчетовОсновнойОстатки.СуммаОстатокДт КАК СуммаЛей,
| ПланСчетовОсновнойОстатки.ВалютнаяСуммаОстатокДт КАК СуммаВал
|ИЗ
| РегистрБухгалтерии.ПланСчетовОсновной.Остатки(
| &ДатаКон,
| Счет = &СчетКт,
| &ВидыСубконто,
| Субконто1 = &Контрагент
| И Субконто2 = &ОсновнойДоговор
| И Субконто3 В (&СписокСтавокНДС)%УсловиеВалюты%) КАК ПланСчетовОсновнойОстатки
|ГДЕ
| %УсловиеСуммы%";
Если Валютный Тогда
УсловиеВалюты = " И Валюта = &Валюта";
УсловиеСуммы = "ПланСчетовОсновнойОстатки.ВалютнаяСуммаОстатокДт < 0";
Иначе
УсловиеВалюты = "";
УсловиеСуммы = "ПланСчетовОсновнойОстатки.СуммаОстатокДт < 0";
КонецЕсли;
ТекстЗапроса = СтрЗаменить(ТекстШаблона, "%УсловиеВалюты%", УсловиеВалюты);
ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "%УсловиеСуммы%", УсловиеСуммы);
Благодаря этому подходу платформа 1С будет передавать в СУБД два абсолютно разных текста запроса. Для каждого из них СУБД создаст и закэширует свой собственный, максимально оптимальный план выполнения, который никогда не перепутается.
Обратите внимание на третий параметр виртуальной таблицы Остатки. В исходном запросе он оставлен пустым:
РегистрБухгалтерии.ПланСчетовОсновной.Остатки(&ДатаКон, Счет = &СчетКт, , ... )
Разберем техническую суть этой ошибки. Поля Субконто1, Субконто2, Субконто3 являются универсальными. На уровне базы данных СУБД значения субконто физически хранятся в отдельной таблице значений субконто (например, таблица _AccRgED в MS SQL Server).
Когда мы не указываем конкретные виды субконто в третьем параметре, платформа 1С вынуждена генерировать избыточный SQL-запрос, выполняющий левые соединения (LEFT JOIN) со всеми возможными таблицами значений субконто, которые потенциально могут быть привязаны к выбранному счету в плане счетов. Это существенно увеличивает количество логических чтений СУБД и усложняет план запроса.
Для оптимизации запроса необходимо явно передать массив видов субконто в параметры виртуальной таблицы. Это позволит платформе построить точечный SQL-запрос с прямыми и эффективными соединениями таблиц:
МассивВидовСубконто = Новый Массив;
МассивВидовСубконто.Добавить(ПланыВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты);
МассивВидовСубконто.Добавить(ПланыВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры);
МассивВидовСубконто.Добавить(ПланыВидовХарактеристик.ВидыСубконтоХозрасчетные.СтавкиНДС);
Запрос.УстановитьПараметр("ВидыСубконто", МассивВидовСубконто);
Передача этого параметра гарантирует задействование СУБД оптимального индекса по субконто вида (Счет, ВидСубконто1, Значение1...).
Использование оператора сравнения В со списком значений в параметрах виртуальной таблицы — еще одна частая причина дестабилизации планов СУБД. Если количество элементов в передаваемом списке (массиве) постоянно меняется (например, при одном вызове передается 1 элемент, а при другом — 5), СУБД может посчитать такие запросы разными и начать заново строить планы выполнения, либо применить неэффективный кэшированный план.
Если список значений содержит более одного-двух элементов, правильным решением будет предварительная выгрузка списка во временную таблицу с ее обязательным индексированием:
// Создаем временную таблицу со списком ставок НДС
Запрос.Текст =
"ВЫБРАТЬ
| Ставки.Ссылка КАК СтавкаНДС
|ПОМЕСТИТЬ ВТ_СтавкиНДС
|ИЗ
| &СписокСтавокНДС КАК Ставки
|ИНДЕКСИРОВАТЬ ПО
| СтавкаНДС";
После этого в параметрах виртуальной таблицы остатков мы можем связать данные через условие Субконто3 В (ВЫБРАТЬ СтавкаНДС ИЗ ВТ_СтавкиНДС). Это сделает структуру SQL-запроса статичной и предсказуемой для планировщика СУБД независимо от количества элементов в списке.
Если плавающее замедление происходит строго периодически (например, 1-2 раза в месяц), это явно указывает на периоды пиковых нагрузок: закрытие отчетного периода, массовое перепроведение документов или сдача отчетности.
В эти моменты в базу данных записываются миллионы новых строк. Статистика СУБД по распределению данных в таблицах мгновенно устаревает. На основе «тухлой» (устаревшей) статистики оптимизатор СУБД начинает ошибочно полагать, что индексы неселективны, и переходит на полное сканирование таблиц.
Для предотвращения подобных ситуаций системному администратору необходимо настроить регламентные задачи на сервере СУБД:
UPDATE STATISTICS, для PostgreSQL — команда ANALYZE.REBUILD / REORGANIZE) позволяет СУБД поддерживать физическую структуру таблиц в оптимальном состоянии, что ускоряет выборку данных.Мы подробно проанализировали проблему разного времени выполнения запросов в 1С. Для обеспечения стабильной и быстрой работы системы придерживайтесь следующих правил:
ВЫБОР и ИЛИ внутри параметров виртуальных таблиц и секций ГДЕ.