Почему запрос в 1С выполняется минутами и что в нём переписать, чтобы он работал за секунды

· 10 мин

Содержание

Чтобы оптимизировать запрос 1С, сначала найдите, на каком месте он теряет время. Для этого нужен замер производительности и план выполнения в СУБД. Потом обычно хватает трёх правок: условия перенести в параметры виртуальных таблиц, вложенные запросы в соединениях заменить временными таблицами с ИНДЕКСИРОВАТЬ ПО, убрать разыменование составных полей через точку.

Распространённое заблуждение: «запрос медленный, значит, нужен сервер мощнее». На практике запрос, который читает весь регистр за пять лет, будет тормозить и на 64 ядрах. Ускорение железом даёт проценты, а исправленный текст запроса — разы, иногда в десятки раз. Всё ниже относится к платформе 8.3, файловому и клиент-серверному варианту, если не сказано иное.

С чего начать: найти медленный запрос и прочитать план выполнения

Не оптимизируйте вслепую. Сначала определите, какой именно запрос виноват и сколько он выполняется.

Шаг 1. Замер производительности

  1. Откройте конфигурацию в Конфигураторе и запустите отладку (F5).
  2. В Конфигураторе включите Отладка → Замер производительности.
  3. В пользовательском режиме выполните медленную операцию: сформируйте отчёт, проведите документ.
  4. Выключите замер. В окне результатов отсортируйте строки по времени.

Замер покажет строку модуля, где вызывается Запрос.Выполнить(), и сколько секунд ушло на неё. Если 90% времени съедает один вызов — работаем с ним. Если время размазано по тысячам коротких вызовов — у вас запрос в цикле (об этом ниже).

Шаг 2. План выполнения через технологический журнал

В клиент-серверном варианте 1С транслирует текст запроса в SQL. План показывает, как СУБД его выполняет. Включите технологический журнал файлом logcfg.xml в каталоге conf платформы на сервере 1С:

<config xmlns="http://v8.1c.ru/v8/tech-log">
  <log location="D:\techlog" history="2">
    <event>
      <eq property="name" value="DBMSSQL"/>
      <ge property="duration" value="1000000"/>
    </event>
    <event>
      <eq property="name" value="DBPOSTGRS"/>
      <ge property="duration" value="1000000"/>
    </event>
    <property name="all"/>
  </log>
  <plansql/>
</config>

Фильтр по duration отсекает быстрые запросы: в 8.3 длительность пишется в микросекундах, так что 1000000 — одна секунда. Элемент <plansql/> добавляет к событию план выполнения.

Журнал с планами растёт на гигабайты за час на нагруженной базе и сам добавляет нагрузку. Включайте его на время диагностики, на отдельный диск, и удаляйте logcfg.xml сразу после замера.

Для PostgreSQL план удобно смотреть и средствами самой СУБД: модуль auto_explain с порогом по длительности пишет планы медленных запросов в лог сервера.

Шаг 3. Что искать в плане

Что видите в планеЧто это значитКуда смотреть в запросе 1С
Index SeekПоиск по индексу, быстроВсё в порядке
Clustered Index Scan, Table Scan, Seq Scan по большой таблицеЧтение всей таблицыУсловие не попало в индекс, функция над полем, ИЛИ
Nested Loops с миллионами строк на входеСУБД ошиблась с оценкой числа строкСоединение с вложенным запросом или виртуальной таблицей
Table Spool, Sort с большим объёмомПромежуточные результаты на дискеТяжёлая группировка, РАЗЛИЧНЫЕ по большому набору

💡 Сравните в плане оценочное и фактическое число строк. Если СУБД ждала 1 строку, а получила 200 000 — это главный кандидат на переписывание. Такую картину обычно дают вложенный запрос в соединении или соединение с виртуальной таблицей.

Условия отбора и соединения: где запрос теряет скорость

Правило одно: чем раньше отсечены лишние строки, тем быстрее запрос. СУБД может воспользоваться индексом, только если условие написано «чисто».

Условия, которые ломают индекс

  • 🚫 Функция над полем. ГДЕ ГОД(Док.Дата) = 2025 заставляет вычислить год для каждой строки. Пишите ГДЕ Док.Дата МЕЖДУ &Начало И &Конец.
  • 🚫 ИЛИ по разным полям. ГДЕ Т.Склад = &Склад ИЛИ Т.Организация = &Орг часто превращается в полное сканирование. Разбейте на два запроса через ОБЪЕДИНИТЬ ВСЕ, каждый со своим условием. Если строка может попасть в обе части, используйте ОБЪЕДИНИТЬ или исключите пересечение условием.
  • 🚫 ПОДОБНО "%текст%". Ведущий % не даёт искать по индексу. Для поиска по подстроке в больших справочниках подключите полнотекстовый поиск.
  • 🚫 В (ВЫБРАТЬ …) с тяжёлым подзапросом. Сначала поместите результат подзапроса во временную таблицу.

Соединения

  1. Соединяйте по полям-ссылкам и измерениям, а не по строковым реквизитам вроде ИНН или наименования.
  2. Не пишите в условии соединения вложенный запрос: ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ … СГРУППИРОВАТЬ ПО …) КАК Т. СУБД не знает, сколько строк он вернёт, и выбирает неудачный план.
  3. Условие отбора по правой таблице ЛЕВОГО СОЕДИНЕНИЯ, вынесенное в ГДЕ, превращает соединение во внутреннее. Если нужна именно фильтрация — пишите ВНУТРЕННЕЕ СОЕДИНЕНИЕ, так понятнее и вам, и оптимизатору.

Разыменование через точку

Каждая точка в Док.Контрагент.ГоловнойКонтрагент.ИНН — это неявное соединение с таблицей. Для обычной ссылки это одно соединение. Для составного типа — соединение со всеми таблицами, которые входят в тип.

Классика: Движения.Регистратор.Дата. Регистратором регистра бывают десятки видов документов, и запрос соединится с каждым. Решения:

// Дата уже есть в регистре
Движения.Период

// Нужен реквизит конкретного документа
ВЫРАЗИТЬ(Движения.Регистратор КАК Документ.РеализацияТоваровУслуг).Номер

ВЫРАЗИТЬ оставляет одно соединение. Строки других типов вернут NULL, так что при необходимости добавьте отбор Движения.Регистратор ССЫЛКА Документ.РеализацияТоваровУслуг.

Виртуальные таблицы регистров: условие в параметрах, а не в ГДЕ

Это приём, без которого оптимизация запросов 1С не обходится. Виртуальная таблица Остатки, Обороты, ОстаткиИОбороты, СрезПоследних сначала собирает данные из таблиц итогов и движений, а уже потом применяется ГДЕ.

Медленно — сначала остатки по всем складам, потом фильтр:

ВЫБРАТЬ
    Остатки.Номенклатура,
    Остатки.КоличествоОстаток
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки(&Дата) КАК Остатки
ГДЕ
    Остатки.Склад = &Склад

Быстро — фильтр внутри сбора остатков:

ВЫБРАТЬ
    Остатки.Номенклатура,
    Остатки.КоличествоОстаток
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки(&Дата, Склад = &Склад) КАК Остатки

Имя регистра здесь для примера — подставьте свой. Правила для параметров:

  • ✅ В условие параметра пишите только измерения регистра.
  • ✅ Если нужен отбор по списку товаров из предыдущего шага, используйте Номенклатура В (ВЫБРАТЬ ВТ.Номенклатура ИЗ ВТ_Товары КАК ВТ) внутри параметра.
  • ✅ Для Обороты задавайте только нужную периодичность: Период, Регистратор и Запись резко увеличивают объём.
  • 🚫 Не соединяйте виртуальную таблицу с другой таблицей без отбора в параметрах — выйдут остатки по всей базе.

💡 Если регистр огромный, а итоги давно не пересчитывались, полезно проверить настройки итогов: Все функции → Стандартные → Управление итогами. Для регистров с разделением итогов и установленным периодом рассчитанных итогов остатки на дату считаются быстрее.

Временные таблицы вместо вложенных запросов

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

ВЫБРАТЬ РАЗЛИЧНЫЕ
    Реализация.Контрагент КАК Контрагент
ПОМЕСТИТЬ ВТ_Контрагенты
ИЗ
    Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
    Реализация.Дата МЕЖДУ &НачалоПериода И &КонецПериода
    И Реализация.Проведен
ИНДЕКСИРОВАТЬ ПО
    Контрагент
;

ВЫБРАТЬ
    ВТ.Контрагент,
    Контрагенты.ИНН
ИЗ
    ВТ_Контрагенты КАК ВТ
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
    ПО ВТ.Контрагент = Контрагенты.Ссылка

Когда временная таблица нужна, а когда лишняя:

СитуацияВременная таблицаПочему
Вложенный запрос в соединенииДаТочное число строк, план стабильный
Один результат используется в 2+ местах пакетаДаСчитается один раз
Большой отбор, по которому дальше соединениеДа, с ИНДЕКСИРОВАТЬ ПОИндекс по полю соединения
Таблица на 10–50 строк, которая читается один разНетСоздание обходится дороже выгоды
Простой отбор, который сразу выводитсяНетЛишняя запись в tempdb

Индексировать стоит только поля соединения и отбора, и только если в таблице больше пары сотен строк. Индекс по таблице из 30 строк ничего не ускорит.

Таблица значений как источник

Если данные уже лежат в таблице значений, не перебирайте её с запросом на каждую строку. Передайте её параметром и положите во временную таблицу:

Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
|    Т.Номенклатура КАК Номенклатура
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
|    &ТаблицаТоваров КАК Т
|ИНДЕКСИРОВАТЬ ПО
|    Номенклатура";
Запрос.УстановитьПараметр("ТаблицаТоваров", ТаблицаТоваров);
Запрос.Выполнить();

Колонки таблицы значений должны быть типизированы (тип задают при Колонки.Добавить), иначе запрос завершится ошибкой.

Индексы: ускорение запроса 1С без переписывания текста

Индексы в 1С создают в Конфигураторе, а не в СУБД вручную. Индексы, созданные напрямую в SQL, могут пропасть при реструктуризации, и поддерживать их некому.

Где задаётся индекс

  • 📌 Реквизит справочника или документа: свойство Индексировать в палитре свойств. Варианты — «Не индексировать», «Индексировать», «Индексировать с доп. упорядочиванием».
  • 📌 Измерение регистра сведений и накопления: то же свойство у измерения.
  • 📌 Стандартные поля: Ссылка, Код, Наименование, Дата, Номер, Период, Регистратор уже проиндексированы платформой.

«С доп. упорядочиванием» нужен, когда по реквизиту часто отбирают в динамическом списке с сортировкой по коду или наименованию.

Порядок измерений имеет значение

Основной индекс регистра строится по измерениям в порядке их расположения в конфигурации. Условие по второму измерению без первого использует индекс хуже. Для регистра «Склад, Номенклатура» отбор только по Номенклатура может пойти сканированием — помогает отдельный индекс по этому измерению.

Изменение свойства Индексировать в рабочей типовой конфигурации снимает её с полной поддержки и требует обновления структуры базы с монопольным доступом. На большой таблице реструктуризация занимает часы. Делайте это в нерабочее время, после резервной копии, и согласуйте с тем, кто обновляет конфигурацию.

Каждый индекс замедляет запись. Не индексируйте реквизит «на всякий случай»: только если по нему регулярно отбирают и это видно в плане.

Группировки и агрегатные функции без лишней нагрузки

  • 📊 Отбор до группировки. Условие на исходные поля пишите в ГДЕ. ИМЕЮЩИЕ — только для условий на результат агрегата, например ИМЕЮЩИЕ СУММА(Т.Сумма) > 100000.
  • 📊 Не группируйте движения вручную, если есть виртуальная таблица Обороты. Она берёт данные из итогов и почти всегда быстрее, чем СГРУППИРОВАТЬ ПО по таблице движений.
  • 📊 Группируйте по ссылке, а не по реквизитам через точку. СГРУППИРОВАТЬ ПО Т.Номенклатура.Наименование добавляет соединение и сортировку по строке. Группируйте по Т.Номенклатура, а представление получайте через ПРЕДСТАВЛЕНИЕ(Т.Номенклатура) в итоговой выборке.
  • 📊 КОЛИЧЕСТВО(РАЗЛИЧНЫЕ …) и РАЗЛИЧНЫЕ по миллионам строк требуют сортировки. Если уникальность гарантирована структурой данных, РАЗЛИЧНЫЕ не пишите.
  • 📊 ИТОГИ в запросе считаются отдельными проходами. В отчётах на СКД итоги лучше отдать компоновщику, а в запросе оставить только детальные данные.

Пример переписывания. Было — группировка по документам с отбором после:

ВЫБРАТЬ
    Т.Номенклатура,
    СУММА(Т.Количество) КАК Количество
ИЗ
    РегистрНакопления.Продажи КАК Т
СГРУППИРОВАТЬ ПО
    Т.Номенклатура
ИМЕЮЩИЕ
    МАКСИМУМ(Т.Период) >= &Начало

Стало — оборот за период из итогов:

ВЫБРАТЬ
    Т.Номенклатура,
    Т.КоличествоОборот КАК Количество
ИЗ
    РегистрНакопления.Продажи.Обороты(&Начало, &Конец) КАК Т

Учтите, что второй вариант считает именно оборот за период — это то, что обычно и нужно в отчёте о продажах.

Антипаттерны, которые замедляют запросы 1С

Ошибки, которые встречаются в доработках чаще всего и которые не видны в одном отдельно взятом запросе.

Запрос в цикле

Для Каждого Строка Из Товары Цикл
    Запрос.УстановитьПараметр("Товар", Строка.Номенклатура);
    Результат = Запрос.Выполнить();
    …
КонецЦикла;

1000 строк — 1000 обращений к СУБД. Каждое занимает миллисекунды, вместе — минуты. Решение: один запрос с параметром-массивом ГДЕ Т.Номенклатура В (&МассивТоваров) или временная таблица из таблицы значений, а потом соединение.

Чтение объекта ради одного реквизита

Ссылка.ПолучитьОбъект().Реквизит читает объект целиком с табличными частями. Нужен один реквизит — получите его запросом или через ОбщегоНазначения.ЗначениеРеквизитаОбъекта в типовых конфигурациях на БСП.

Остальное

  • 🐢 Выборка всех полей, когда нужно два.
  • 🐢 Соединение одной и той же виртуальной таблицы несколько раз в одном запросе вместо одной временной таблицы.
  • 🐢 Перебор результата Выбрать() с обращением к реквизитам ссылок через точку внутри цикла — каждое обращение отдельный запрос.
  • 🐢 Отбор по полю составного типа без ССЫЛКА или ВЫРАЗИТЬ.

Когда тормозит не сам запрос

Диагностика: выберите симптом

Частые вопросы

Есть ли смысл в оптимизации запросов в файловой базе?
Да. Правила те же: параметры виртуальных таблиц, временные таблицы, без запросов в цикле. Плана выполнения СУБД в файловом варианте нет, поэтому ориентируйтесь на замер производительности.
Помогает ли ПЕРВЫЕ N ускорить запрос?
Помогает, если нужен только верх списка и сортировка идёт по индексированному полю. При сортировке по вычисляемому полю СУБД всё равно соберёт весь набор.
Можно ли смотреть план выполнения из конструктора запроса?
Нет. Конструктор и консоль запросов показывают текст и результат. План берут из технологического журнала с plansql или средствами самой СУБД.
Что быстрее: левое соединение или вложенный запрос в списке полей?
Как правило, соединение с временной таблицей. Вложенный запрос в условии В (…) по большой таблице лучше сначала положить во временную таблицу с индексом.
Нужно ли удалять временные таблицы?
При выполнении пакета в одном запросе без менеджера временных таблиц они удаляются сами. С менеджером — удалите УНИЧТОЖИТЬ ВТ_Имя или закройте менеджер методом Закрыть(), когда таблицы больше не нужны.