Почему запрос в консоли 1С выполняется мгновенно, а в коде висит 20 секунд: анализ причин и методы решения

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

В практике разработки на платформе 1С:Предприятие часто встречается парадоксальная ситуация: при отладке в консоли запросов результат возвращается мгновенно (доли секунды), но этот же запрос в работающем механизме или отчете заставляет систему "задумываться" на десятки секунд. Особенно сложно диагностировать проблему, когда на тестовой базе поведение нормальное, а на рабочей (продуктивной) наблюдаются критические задержки при идентичных данных. В данной статье мы подробно разберем реальный кейс такой проблемы, связанный с документами логистики, проанализируем причины "лжи" консоли запросов и рассмотрим методы оптимизации, позволяющие стабилизировать время выполнения.

Суть проблемы: симптомы и диагностика

Рассмотрим ситуацию, с которой столкнулся разработчик. Исходные данные таковы:

  1. На рабочей базе запрос выполняется около 15–20 секунд.
  2. В консоли запросов на той же рабочей базе время выполнения показывается как 0.08 секунды, но фактическое ожидание результата (отрисовка) занимает те же 15–20 секунд.
  3. На тестовой базе (копия рабочей) время выполнения и в консоли, и фактически составляет 0.08 секунды.
  4. Запрос выполняется в привилегированном режиме, проблема прав доступа исключена.

Такое поведение часто указывает не на сложность алгоритма выборки как такового, а на проблемы с получением данных (Fetch), передачей их на клиент или сбросом промежуточных данных на диск (tempdb) на стороне СУБД.

Анализ исходного проблемного запроса

Давайте взглянем на исходный текст запроса, который вызывает задержки. Обратите внимание на структуру соединений и группировок.


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

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ ПЕРВЫЕ 3
    ВТ_ЗаданиеНаПеревозкуСтроки.Адрес КАК Адрес,
    МАКСИМУМ(ВТ_ЗаданиеНаПеревозкуСтроки.Дата) КАК Дата
ПОМЕСТИТЬ ВТ_ПоследниеАдреса
ИЗ
    ВТ_ЗаданиеНаПеревозкуСтроки КАК ВТ_ЗаданиеНаПеревозкуСтроки
ГДЕ
    ВТ_ЗаданиеНаПеревозкуСтроки.Адрес <> ""

СГРУППИРОВАТЬ ПО
    ВТ_ЗаданиеНаПеревозкуСтроки.Адрес

УПОРЯДОЧИТЬ ПО
    МАКСИМУМ(ВТ_ЗаданиеНаПеревозкуСтроки.Дата) УБЫВ
;

В этом коде есть несколько потенциально узких мест, которые могут привести к описанным симптомам.

1. Группировка по строкам неограниченной длины

В пакете ВТ_ПоследниеАдреса происходит группировка по полю Адрес. В конфигурациях 1С поле адреса часто имеет тип Строка неограниченной длины или достаточно большой длины (например, 500-1000 символов). SQL Server (и другие СУБД) испытывают трудности при группировке и сортировке по полям типа text, ntext или varchar(max). Почему это вызывает тормоза: Оптимизатор запросов СУБД не может точно предсказать, сколько оперативной памяти (Memory Grant) потребуется для операции сортировки таких данных. Часто выделяется недостаточно памяти, и когда реальные данные превышают этот лимит, СУБД начинает использовать механизм Spill to tempdb — сбрасывать промежуточные таблицы на жесткий диск. Дисковые операции в тысячи раз медленнее операций в оперативной памяти. Именно это объясняет "зависание" на 20 секунд: процессор не нагружен (поэтому "время выполнения" маленькое), но система ждет ввода-вывода с диска.

2. Отсутствие явной типизации

В исходном запросе поля выбираются "как есть". Если Адрес или ДополнительнаяИнформация — это строки неограниченной длины, то их физическое извлечение из базы данных (особенно если они хранятся как LOB-структуры) занимает значительное время, которое консоль запросов может не учитывать в таймере "Exec time", показывая только время построения курсора.

Различия между тестовой и рабочей базой

Почему на тестовой базе (копии) все летает, а на рабочей висит?

  1. Статистика SQL: На рабочей базе статистика распределения данных может устареть или быть обновлена в неудачный момент. Это заставляет оптимизатор выбирать план запроса (Execution Plan), который, например, использует вложенные циклы (Nested Loops) вместо хеш-соединения (Hash Match), что критично при больших объемах данных.
  2. Кэширование планов (Parameter Sniffing): Рабочая база могла закешировать неудачный план запроса при первом его запуске с определенными параметрами. Тестовая база, будучи перезапущенной или менее нагруженной, могла сгенерировать более удачный план.
  3. "Горячий" кэш данных: На тестовой базе вы, возможно, единственный пользователь. Данные таблицы полностью загружены в оперативную память (Buffer Pool). На рабочей базе данные постоянно вытесняются активностью других пользователей, и системе приходится физически читать их с диска.

Комплексное решение проблемы

Чтобы устранить проблему и добиться стабильного времени выполнения (пусть и не 0.08, но, например, стабильные 1-2 секунды вместо 20), необходимо применить типизацию полей и правильное индексирование временных таблиц. Рассмотрим оптимизированный вариант запроса, который решает проблему.

Шаг 1. Типизация полей и первичная выборка

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


ВЫБРАТЬ
    ЗаданиеНаПеревозкуРаспоряжения.НомерСтроки КАК НомерСтроки,
    ЗаданиеНаПеревозкуРаспоряжения.Ссылка.Дата КАК Дата,
    ЗаданиеНаПеревозкуРаспоряжения.КлючСвязи КАК КлючСвязи,
    // Явное приведение типов для строк
    ВЫРАЗИТЬ(ЗаданиеНаПеревозкуРаспоряжения.ДополнительнаяИнформация КАК СТРОКА(1024)) КАК ДополнительнаяИнформация,
    ЗаданиеНаПеревозкуМаршрут.Адрес КАК Адрес,
    ВЫРАЗИТЬ(ЗаданиеНаПеревозкуМаршрут.АдресЗначенияПолей КАК СТРОКА(1024)) КАК АдресЗначенияПолей
ПОМЕСТИТЬ ВТ_ЗаданиеНаПеревозкуСтроки
ИЗ
    Документ.ЗаданиеНаПеревозку.Распоряжения КАК ЗаданиеНаПеревозкуРаспоряжения
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ЗаданиеНаПеревозку.Маршрут КАК ЗаданиеНаПеревозкуМаршрут
        ПО (ЗаданиеНаПеревозкуМаршрут.КлючСвязи = ЗаданиеНаПеревозкуРаспоряжения.КлючСвязи)
ГДЕ
    ЗаданиеНаПеревозкуРаспоряжения.Ссылка.Проведен
    // ... отборы ...

// Индексирование критически важно для дальнейших соединений
ИНДЕКСИРОВАТЬ ПО
    Адрес,
    Дата
;

Что здесь изменено:

Мы использовали ВЫРАЗИТЬ(... КАК СТРОКА(1024)). Теперь поле Адрес и ДополнительнаяИнформация имеют фиксированную максимальную длину. СУБД воспринимает их как varchar(1024), а не как text/lob. Это позволяет:

Шаг 2. Разбиение сложной логики

Вместо того чтобы пытаться выбрать "первые 3" и сгруппировать все в одном запросе, разделим задачу. Сначала подготовим данные для сортировки.


ВЫБРАТЬ
    ВТ_ЗаданиеНаПеревозкуСтроки.Адрес КАК Адрес,
    МАКСИМУМ(ВТ_ЗаданиеНаПеревозкуСтроки.Дата) КАК Дата
ПОМЕСТИТЬ ВТ_ПоследниеАдреса_неУпорядок
ИЗ
    ВТ_ЗаданиеНаПеревозкуСтроки КАК ВТ_ЗаданиеНаПеревозкуСтроки
ГДЕ
    ВТ_ЗаданиеНаПеревозкуСтроки.Адрес <> ""
СГРУППИРОВАТЬ ПО
    ВТ_ЗаданиеНаПеревозкуСтроки.Адрес
;

ВЫБРАТЬ ПЕРВЫЕ 3
    ВТ_ПоследниеАдреса_неУпорядок.Адрес КАК Адрес,
    ВТ_ПоследниеАдреса_неУпорядок.Дата КАК Дата
ПОМЕСТИТЬ ВТ_ПоследниеАдреса
ИЗ
    ВТ_ПоследниеАдреса_неУпорядок КАК ВТ_ПоследниеАдреса_неУпорядок
УПОРЯДОЧИТЬ ПО
    ВТ_ПоследниеАдреса_неУпорядок.Дата УБЫВ

ИНДЕКСИРОВАТЬ ПО
    Адрес
;

Такое разбиение помогает оптимизатору строить более простые планы. Обратите внимание на ИНДЕКСИРОВАТЬ ПО в конце создания временной таблицы. Индексы во временных таблицах — это мощнейший инструмент ускорения последующих соединений.

Шаг 3. Улучшение условий соединения

В обсуждении проблемы было замечено, что соединение только по КлючСвязи (который часто является GUID-ом, но не ссылкой) может быть неэффективным, если это поле не проиндексировано в самой базе данных. Хорошей практикой при соединении табличных частей одного документа (или связанных таблиц) является добавление условия по Ссылке, если это логически допустимо:


...
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ЗаданиеНаПеревозку.Маршрут КАК ЗаданиеНаПеревозкуМаршрут
ПО (ЗаданиеНаПеревозкуМаршрут.КлючСвязи = ЗаданиеНаПеревозкуРаспоряжения.КлючСвязи)
// Желательно добавить, если структура метаданных позволяет:
// И (ЗаданиеНаПеревозкуМаршрут.Ссылка = ЗаданиеНаПеревозкуРаспоряжения.Ссылка)
...

Однако, если логика подразумевает поиск связи по ключу в рамках разных документов (или структура данных отличается), то создание собственного индекса во временной таблице, как мы сделали выше (ИНДЕКСИРОВАТЬ ПО КлючСвязи), является обязательным.

Почему после "исправления" стало медленнее на тесте?

Автор проблемы заметил, что после применения конструкции ВЫРАЗИТЬ и добавления индексов, время выполнения стало составлять около 1.5 секунд (вместо 0.08 на тесте), но зато оно стало таким же и на рабочей базе (вместо 20 секунд). Это нормальная цена за стабильность:

  1. Накладные расходы на создание индекса: Команда ИНДЕКСИРОВАТЬ ПО требует времени на построение индекса во временной таблице (tempdb). При 0.08 сек запрос, скорее всего, проскакивал без построения структур, но был подвержен риску "плохого плана".
  2. Операция CAST: Преобразование типов для всех строк также требует ресурсов CPU.

Однако, мы получили предсказуемое время выполнения. 1.5 секунды — это приемлемый результат для пользователя, в отличие от 20-секундного зависания.

Дополнительные рекомендации по диагностике

Если типизация и индексирование не помогают, необходимо использовать более глубокие инструменты анализа. 1. Обновление статистики На уровне MS SQL Server (или PostgreSQL) статистики таблиц могут устаревать. Это приводит к тому, что оптимизатор "думает", что в таблице 100 строк, и выбирает цикл (Nested Loops), а там на самом деле 1 000 000 строк. Решение: Попросить администратора БД выполнить обновление статистики для проблемных таблиц. 2. Технологический журнал (ТЖ) Настройте ТЖ на сбор событий DBMSSQL (или DBPOSTGRS) с длительностью более 1 секунды. Найдите там реальный SQL-запрос, который отправляет платформа, и его план выполнения (planSQLText). Сравнение планов на тестовой и рабочей базе мгновенно покажет причину расхождений (например, Index Scan против Table Scan). 3. Избегайте ИЛИ в соединениях В исходном запросе (в сообщениях форума) встречались конструкции вида:


И (ЗаданиеНаПеревозкуРаспоряжения.Перевозчик = &Перевозчик
    ИЛИ &Перевозчик = НЕОПРЕДЕЛЕНО ...)

Использование ИЛИ в условиях ГДЕ часто блокирует использование индексов. По возможности такие условия лучше заменять на использование ОБЪЕДИНИТЬ ВСЕ или динамическое построение текста запроса в коде 1С, добавляя условие только если параметр заполнен.

Резюме

Для устранения "фантомных" тормозов, которые не видны в консоли, но есть в приложении:

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