Каждый разработчик 1С рано или поздно сталкивается со сценарием, когда небольшой запрос к временной таблице, созданной на основе переданной в параметры ТаблицыЗначений, начинает аномально тормозить. Казалось бы, во временной таблице содержится всего 500–900 строк, однако время выполнения запроса возрастает до катастрофических 10–20 секунд. В условиях высокой интенсивности работы пользователей (например, при выписке сотен ветеринарных свидетельств Меркурия или расходных накладных в день) такие задержки полностью парализуют бизнес-процессы.
В этой статье мы подробно разберем, почему возникает эта проблема, что происходит в этот момент «под капотом» СУБД и платформы 1С:Предприятие, а также рассмотрим несколько эффективных способов оптимизации производительности — от простых обходных путей до глубоких архитектурных решений.
---
Проанализируем архитектурную специфику обычных форм (Обычного приложения) 1С. В отличие от Управляемого приложения, где разработчик жестко ограничен контекстом выполнения кода, в Обычном приложении код модуля документа по умолчанию выполняется на клиенте.
Когда мы пишем запрос и передаем в него таблицу значений Остатки в качестве параметра, платформа вынуждена выполнить следующие действия:
Остатки на стороне клиента во внутренний XML-формат передачи данных.tempdb MS SQL Server) и заполнить ее строками с помощью SQL-команд INSERT.Давайте разберем по шагам, как решить эту проблему с помощью переноса вычислений на сервер.
Решение: Перенесите логику выполнения запроса и получения исходных данных в общий модуль с флагами Сервер и Вызов сервера. Если таблица значений будет формироваться непосредственно на сервере (например, путем прямого запроса к СУБД или к веб-сервису Меркурия) и там же передаваться в запрос, мы полностью исключим дорогостоящую операцию сериализации и сетевой передачи «клиент-сервер-клиент».
---
Это одна из самых частых «невидимых» причин медленной работы временных таблиц. Рассмотрим, как СУБД реагирует на нетипизированную таблицу значений.
Если колонки исходной таблицы значений Остатки создаются динамически в коде без явного указания типов (например, простым методом ТЗ.Колонки.Добавить("Номенклатура")), платформа 1С не знает точного типа данных этих колонок. При помещении такой таблицы в запрос СУБД вынуждена создавать колонки во временной таблице с универсальным и крайне ресурсоемким типом sql_variant или длинными текстовыми типами вроде nvarchar(MAX).
Запись даже 500 строк в таблицу с типами sql_variant происходит в СУБД построчно и крайне неоптимально. СУБД тратит много процессорного времени и дискового ввода-вывода на выделение страниц памяти под «тяжелые» типы данных. Именно на этапе наполнения временной таблицы СУБД «зависает» на те самые 10–15 секунд, в то время как сама выборка данных (оператор ВЫБРАТЬ) происходит мгновенно.
Более того, на нетипизированных полях невозможно построить эффективные индексы. Если вы попытаетесь применить конструкцию ИНДЕКСИРОВАТЬ ПО к нетипизированной временной таблице, СУБД либо проигнорирует индексы, либо это приведет к еще большему замедлению процесса записи (время выполнения может вырасти с 15 до 20 секунд).
Решение: Принудительно задавайте жесткие типы для всех колонок таблицы значений перед передачей ее в запрос. Посмотрим на пример правильного создания структуры таблицы:
// Создаем описание типов для колонок таблицы значений
МассивТиповНоменклатура = Новый Массив;
МассивТиповНоменклатура.Добавить(Тип("СправочникСсылка.Номенклатура"));
ОписаниеТиповНоменклатура = Новый ОписаниеТипов(МассивТиповНоменклатура);
МассивТиповЧисло = Новый Массив;
МассивТиповЧисло.Добавить(Тип("Число"));
КвалификаторыЧисла = Новый КвалификаторыЧисла(15, 3);
ОписаниеТиповЧисло = Новый ОписаниеТипов(МассивТиповЧисло, , , КвалификаторыЧисла);
МассивТиповСтрока = Новый Массив;
МассивТиповСтрока.Добавить(Тип("Строка"));
КвалификаторыСтроки = Новый КвалификаторыСтроки(36); // Ограничиваем длину под GUID
ОписаниеТиповСтрока = Новый ОписаниеТипов(МассивТиповСтрока, , , КвалификаторыСтроки);
// Инициализируем таблицу значений с жесткой типизацией
Остатки = Новый ТаблицаЗначений;
Остатки.Колонки.Добавить("Номенклатура", ОписаниеТиповНоменклатура);
Остатки.Колонки.Добавить("Количество", ОписаниеТиповЧисло);
Остатки.Колонки.Добавить("ГУИДПроизводителя", ОписаниеТиповСтрока);
Остатки.Колонки.Добавить("ДатаВыработки", Новый ОписаниеТипов("Дата"));
Строгое указание типов позволит СУБД создать временную таблицу с оптимальными типами полей (например, uniqueidentifier, numeric(15,3), datetime), что ускорит ее заполнение в сотни раз.
---
Временные таблицы в СУБД MS SQL Server создаются в системной базе данных tempdb. Если даже простейший запрос вида ВЫБРАТЬ * ПОМЕСТИТЬ Остатки ИЗ &Остатки выполняется неоправданно долго, проблема может крыться в настройках самой СУБД на сервере.
Проанализируем типичные системные проблемы с tempdb:
tempdb расположена на медленных магнитных дисках (HDD) или делит одну дисковую полку с основной транзакционной базой данных, операции записи во временные таблицы будут упираться в дисковую очередь.tempdb задан очень маленьким (например, 8 МБ), а шаг авторасширения установлен в процентах или небольшим фиксированным объемом (например, 1 МБ), то СУБД при каждом создании временной таблицы будет вынуждена физически приостанавливать работу запроса для расширения файлов на диске.tempdb создан всего один файл данных, это создает бутылочное горлышко.Решение для системного администратора:
tempdb на самые быстрые твердотельные накопители (NVMe SSD).tempdb с запасом (например, от 2 до 8 ГБ в зависимости от нагрузки), чтобы СУБД не тратила ресурсы на динамическое расширение файлов во время работы пользователей. Шаг авторасширения задайте фиксированным (например, по 256 МБ или 512 МБ).tempdb на несколько файлов данных одинакового размера. Количество файлов должно быть равно количеству логических ядер процессора (но не более 8 файлов, во избежание избыточных накладных расходов).---
Обратим внимание на текст запроса из первого сообщения темы. В нем содержится целый ряд конструкций, которые не позволяют оптимизатору СУБД построить эффективный план выполнения запроса:
ВЫРАЗИТЬ(СкладскиеЗаписи.ГУИДПроизводителя КАК СТРОКА(36)) = &ГУИДПроизводителя в секции ГДЕ: Любое преобразование типов на лету внутри условий соединения или фильтрации заставляет СУБД полностью отказаться от использования индексов (даже если они построены) и выполнять полный перебор всей таблицы (Table Scan). Преобразовывать ГУИД к строке нужно было еще на этапе подготовки таблицы значений в коде 1С, а не внутри SQL-запроса.ПОДОБНО "%устаревшая%": Поиск подстроки с ведущим символом процента (%) делает невозможным использование индексов. СУБД вынуждена выполнять посимвольное сканирование всех строк таблицы.ИЛИ с параметрами: Условия вида &ДатаВыработки = ДАТАВРЕМЯ(1, 1, 1) ИЛИ ... приводят к так называемой проблеме «параметрического тупика». Оптимизатор СУБД строит универсальный (и поэтому неэффективный) план запроса, который одинаково плохо работает при любых значениях параметров.Решение: Динамически формируйте текст запроса в коде 1С. Если параметр (например, &ДатаВыработки или &КодПартии) не заполнен, вообще не добавляйте соответствующее условие в секцию ГДЕ. Очистите условия запроса от функций преобразования типов ВЫРАЗИТЬ.
---
Рассмотрим прагматичное и быстрое решение проблемы, которое применил автор темы (Сообщение 2). Если в текущий момент у вас нет возможности перенастраивать СУБД или переписывать архитектуру вызовов, можно существенно сократить объем передаваемых в запрос данных.
Вместо того чтобы передавать в запрос полную таблицу со всеми остатками (которая может содержать сотни неактуальных для текущего документа строк), мы можем предварительно отфильтровать таблицу значений на клиенте программными средствами 1С, оставив только строки по нужной номенклатурной позиции.
Разберем пример реализации этого подхода в коде 1С:
// Создаем структуру отбора по конкретной номенклатуре
Отбор = Новый Структура;
Отбор.Вставить("Номенклатура", НоменклатураДокумента);
// Копируем только подходящие строки во вспомогательную таблицу значений
ОстаткиПоНоменклатуре = ТаблицаОстатковМеркурий.Скопировать(Отбор);
// Передаем в запрос только отфильтрованную таблицу
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| О.ГУИД КАК ГУИД,
| О.ДатаВыработки КАК ДатаВыработки,
| О.ДатаПросрочки КАК ДатаПросрочки,
| О.Количество КАК Количество,
| О.Номенклатура КАК Номенклатура,
| О.ГУИДПроизводителя КАК ГУИДПроизводителя
|ПОМЕСТИТЬ ВтОстатки
|ИЗ
| &Остатки КАК О
|;
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВтОстатки.ГУИД,
| ВтОстатки.ДатаВыработки,
| ВтОстатки.ДатаПросрочки,
| ВтОстатки.Количество
|ИЗ
| ВтОстатки КАК ВтОстатки";
Запрос.УстановитьПараметр("Остатки", ОстаткиПоНоменклатуре);
РезультатЗапроса = Запрос.Выполнить();
Почему это сработало у автора темы: Вместо 900 строк в запрос передавалось всего 5–10 строк, относящихся к конкретной номенклатуре. Объем передаваемых и обрабатываемых СУБД данных сократился в сотни раз, благодаря чему время выполнения запроса упало с 15 секунд до незаметных долей секунды.
---
Для того чтобы гарантировать стабильную и быструю работу запросов с временными таблицами на основе таблиц значений, придерживайтесь следующего чеклиста:
Чеклист разработчика 1С:
ВЫРАЗИТЬ и текстовых сравнений ПОДОБНО "%..." в условиях запроса к временным таблицам.tempdb на скоростных SSD-накопителях, разделите ее на несколько файлов по числу процессорных ядер и настройте фиксированный шаг авторасширения.