Почему зависает запрос к срезу последних на SQL Server при передаче списка параметров?

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

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

Проанализируем типовую ситуацию. Имеется регистр сведений ЦеныАТТ (с периодичностью по позиции регистратора) и запрос следующего вида:


ВЫБРАТЬ
    ЦеныПродажные.Склад КАК Склад,
    ЦеныПродажные.Номенклатура КАК Номенклатура,
    ЦеныПродажные.ХарактеристикаНоменклатуры КАК ХарактеристикаНоменклатуры,
    ЦеныПродажные.Цена КАК Цена
ИЗ
    РегистрСведений.ЦеныАТТ.СрезПоследних(
            &Дата,
            Склад В (&СписокСкладов)
                И Номенклатура В (&СписокНоменклатуры)) КАК ЦеныПродажные

Когда в параметры &СписокСкладов и &СписокНоменклатуры передаются массивы размером более 200 элементов, запрос дважды выполняется быстро, а на третий вызов уходит в бесконечное чтение.

В чем техническая причина такого поведения СУБД?

Чтобы понять, что происходит внутри системы, разберем взаимодействие платформы 1С и SQL Server:

  1. Неявные временные таблицы платформы 1С. Когда в условие В (&Параметр) передается коллекция значений, содержащая более определенного порога элементов (в зависимости от релиза платформы — от 100 до 256 значений), 1С перестает подставлять их простым перечислением литералов в условие IN (...). Вместо этого платформа неявно создает в системной базе tempdb временную таблицу, наполняет ее переданными значениями и соединяет с основной таблицей регистра. Для такой автоматически созданной таблицы платформа не создает индексов и не строит статистику распределения данных.
  2. Механизм отложенной параметризации (Deferred Prepare) и Parameter Sniffing. При первых двух вызовах СУБД выполняет запрос как ad-hoc с компиляцией плана «на лету» под фактические значения переданных параметров. На третий запуск драйвер СУБД жестко компилирует и кэширует параметризованный план запроса. В этот момент оптимизатор SQL Server пытается построить обобщенный план. Из-за отсутствия статистики по неявным временным таблицам оптимизатор кардинально ошибается в оценке количества строк (Cardinality Estimation), предполагая, что во временных таблицах находится всего 1 или 100 строк.
  3. Ошибочный выбор соединения через вложенные циклы (Nested Loops). Полагая, что таблица отбора крошечная, оптимизатор выбирает алгоритм Nested Loops (вложенные циклы) вместо Hash Match или Merge Join. СУБД начинает построчно сканировать физическую таблицу регистра для каждой записи из временной таблицы.
  4. Дубликаты в исходных массивах. Если в параметр передается массив складов, выгруженный напрямую из строк документа (например, 250 строк, где повторяются всего 2 склада), неявная временная таблица содержит все 250 неиндексированных строк. В сочетании с 250 строками номенклатуры СУБД перемножает их в декартово произведение и выполняет десятки тысяч вложенных поисков по некластерным индексам регистра, что приводит к многочасовому зависанию.
  5. Периодичность по позиции регистратора. В таких регистрах кластерный индекс построен по полям Период + Регистратор + НомерСтроки. Измерения входят только во вторичные индексы. Виртуальная таблица СрезПоследних разворачивается в сложный SQL-запрос с поиском максимального регистратора и обратным соединением за ресурсами. При ошибочном плане это окончательно обрушивает производительность.

Решение 1. Дедупликация параметров перед передачей в запрос

Самое простое и быстрое исправление на уровне встроенного языка 1С — устранение избыточных данных из параметров. Если в списке складов физически используется всего два склада, в параметр запроса должны попасть только эти два уникальных значения, а не вся колонка документа на 250 строк.

Посмотрим на пример подготовки массивов:


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

СписокСкладов = ТаблицаСкладов.ВыгрузитьКолонку("Склад");

// Если размер массива не превышает 100 элементов,
// платформа не создает временную таблицу в tempdb,
// а передает список литералами в текст SQL.
Запрос.УстановитьПараметр("СписокСкладов", СписокСкладов);

Если размер коллекции не превышает порог создания временных таблиц, оптимизатор СУБД строит простой и быстрый план через поиск по индексу (Index Seek).

Решение 2. Использование явной индексированной временной таблицы (рекомендуемый стандарт)

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

Разберем по шагам текст оптимизированного пакетного запроса:


ВЫБРАТЬ РАЗЛИЧНЫЕ
    ТЧ.Склад КАК Склад,
    ТЧ.Номенклатура КАК Номенклатура
ПОМЕСТИТЬ ВТ_Отбор
ИЗ
    &ТаблицаТоваров КАК ТЧ
ИНДЕКСИРОВАТЬ ПО
    Склад,
    Номенклатура
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Цены.Склад КАК Склад,
    Цены.Номенклатура КАК Номенклатура,
    Цены.ХарактеристикаНоменклатуры КАК ХарактеристикаНоменклатуры,
    Цены.Цена КАК Цена
ИЗ
    РегистрСведений.ЦеныАТТ.СрезПоследних(
            &Дата,
            (Склад, Номенклатура) В
                (ВЫБРАТЬ
                    ВТ.Склад,
                    ВТ.Номенклатура
                ИЗ
                    ВТ_Отбор КАК ВТ)) КАК Цены

Почему этот вариант работает стабильно:

  1. Мы явно создаем временную таблицу с ключевым словом ИНДЕКСИРОВАТЬ ПО. Платформа 1С отдает команду СУБД на создание полноценного индекса по полям Склад и Номенклатура.
  2. Конструкция РАЗЛИЧНЫЕ исключает дублирующиеся строки еще до обращения к регистру.
  3. Составное условие (Склад, Номенклатура) В (...) позволяет СУБД однозначно применить эффективный план поиска и исключает возникновение губительных вложенных циклов даже при последующих кэшированиях плана выполнения.

Решение 3. Реализация собственного среза последних

Для регистров сведений с режимом записи Позиция регистратора и большим объемом движений штатная виртуальная таблица может работать медленно даже с индексированными временными таблицами. В таких случаях высокую скорость дает ручной срез через физическую таблицу регистра.

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


// Шаг 1: Выбираем только актуальные записи регистра до нужной даты с отбором
ВЫБРАТЬ
    Рег.Склад КАК Склад,
    Рег.Номенклатура КАК Номенклатура,
    Рег.ХарактеристикаНоменклатуры КАК ХарактеристикаНоменклатуры,
    Рег.Период КАК Период,
    Рег.Регистратор КАК Регистратор,
    Рег.НомерСтроки КАК НомерСтроки
ПОМЕСТИТЬ ВТ_ДвиженияПоОтбору
ИЗ
    РегистрСведений.ЦеныАТТ КАК Рег
ГДЕ
    Рег.Период <= &Дата
    И (Рег.Склад, Рег.Номенклатура) В
        (ВЫБРАТЬ ВТ.Склад, ВТ.Номенклатура ИЗ ВТ_Отбор КАК ВТ)
ИНДЕКСИРОВАТЬ ПО
    Склад,
    Номенклатура,
    ХарактеристикаНоменклатуры
;

////////////////////////////////////////////////////////////////////////////////
// Шаг 2: Находим максимальную дату/момент времени для каждой комбинации измерений
ВЫБРАТЬ
    Движения.Склад КАК Склад,
    Движения.Номенклатура КАК Номенклатура,
    Движения.ХарактеристикаНоменклатуры КАК ХарактеристикаНоменклатуры,
    МАКСИМУМ(Движения.Период) КАК МаксПериод
ПОМЕСТИТЬ ВТ_МаксПериоды
ИЗ
    ВТ_ДвиженияПоОтбору КАК Движения
СГРУППИРОВАТЬ ПО
    Движения.Склад,
    Движения.Номенклатура,
    Движения.ХарактеристикаНоменклатуры
ИНДЕКСИРОВАТЬ ПО
    Склад,
    Номенклатура,
    ХарактеристикаНоменклатуры,
    МаксПериод
;

////////////////////////////////////////////////////////////////////////////////
// ШАГ 3: Получаем итоговые цены соединением с исходной таблицей
ВЫБРАТЬ
    Цены.Склад КАК Склад,
    Цены.Номенклатура КАК Номенклатура,
    Цены.ХарактеристикаНоменклатуры КАК ХарактеристикаНоменклатуры,
    Цены.Цена КАК Цена
ИЗ
    ВТ_МаксПериоды КАК Макс
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныАТТ КАК Цены
        ПО Макс.Склад = Цены.Склад
            И Макс.Номенклатура = Цены.Номенклатура
            И Макс.ХарактеристикаНоменклатуры = Цены.ХарактеристикаНоменклатуры
            И Макс.МаксПериод = Цены.Период

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

Оперативные действия администратора СУБД

Если зависание происходит на рабочей базе прямо сейчас, администратор может применить следующие меры:

  1. Сброс кэша планов выполнения. Чтобы не перезапускать службу SQL Server, можно очистить процедурный кэш конкретной базы данных командой: ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; Это снимет текущее зависание запроса, однако не предотвратит его повторение в будущем.
  2. Отключение механизма Parameter Sniffing. Начиная с SQL Server 2016, на уровне базы данных можно отключить привязку к параметрам: ALTER DATABASE SCOPED CONFIGURATION SET PARAMETER_SNIFFING = OFF; Это заставит оптимизатор оценивать распределение по средней плотности данных, что снижает риск построения катастрофических планов. Тем не менее фундаментальным решением остается оптимизация кода 1С через явные индексированные временные таблицы.
← На главную