Почему запрос в 1С выполняется минутами и что в нём переписать, чтобы он работал за секунды
Содержание
Чтобы оптимизировать запрос 1С, сначала найдите, на каком месте он теряет время. Для этого нужен замер производительности и план выполнения в СУБД. Потом обычно хватает трёх правок: условия перенести в параметры виртуальных таблиц, вложенные запросы в соединениях заменить временными таблицами с ИНДЕКСИРОВАТЬ ПО, убрать разыменование составных полей через точку.
Распространённое заблуждение: «запрос медленный, значит, нужен сервер мощнее». На практике запрос, который читает весь регистр за пять лет, будет тормозить и на 64 ядрах. Ускорение железом даёт проценты, а исправленный текст запроса — разы, иногда в десятки раз. Всё ниже относится к платформе 8.3, файловому и клиент-серверному варианту, если не сказано иное.
С чего начать: найти медленный запрос и прочитать план выполнения
Не оптимизируйте вслепую. Сначала определите, какой именно запрос виноват и сколько он выполняется.
Шаг 1. Замер производительности
- Откройте конфигурацию в Конфигураторе и запустите отладку (
F5). - В Конфигураторе включите
Отладка → Замер производительности. - В пользовательском режиме выполните медленную операцию: сформируйте отчёт, проведите документ.
- Выключите замер. В окне результатов отсортируйте строки по времени.
Замер покажет строку модуля, где вызывается Запрос.Выполнить(), и сколько секунд ушло на неё. Если 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заставляет вычислить год для каждой строки. ПишитеГДЕ Док.Дата МЕЖДУ &Начало И &Конец. - 🚫
ИЛИпо разным полям.ГДЕ Т.Склад = &Склад ИЛИ Т.Организация = &Оргчасто превращается в полное сканирование. Разбейте на два запроса черезОБЪЕДИНИТЬ ВСЕ, каждый со своим условием. Если строка может попасть в обе части, используйтеОБЪЕДИНИТЬили исключите пересечение условием. - 🚫
ПОДОБНО "%текст%". Ведущий%не даёт искать по индексу. Для поиска по подстроке в больших справочниках подключите полнотекстовый поиск. - 🚫
В (ВЫБРАТЬ …)с тяжёлым подзапросом. Сначала поместите результат подзапроса во временную таблицу.
Соединения
- Соединяйте по полям-ссылкам и измерениям, а не по строковым реквизитам вроде ИНН или наименования.
- Не пишите в условии соединения вложенный запрос:
ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ … СГРУППИРОВАТЬ ПО …) КАК Т. СУБД не знает, сколько строк он вернёт, и выбирает неудачный план. - Условие отбора по правой таблице
ЛЕВОГО СОЕДИНЕНИЯ, вынесенное вГДЕ, превращает соединение во внутреннее. Если нужна именно фильтрация — пишитеВНУТРЕННЕЕ СОЕДИНЕНИЕ, так понятнее и вам, и оптимизатору.
Разыменование через точку
Каждая точка в Док.Контрагент.ГоловнойКонтрагент.ИНН — это неявное соединение с таблицей. Для обычной ссылки это одно соединение. Для составного типа — соединение со всеми таблицами, которые входят в тип.
Классика: Движения.Регистратор.Дата. Регистратором регистра бывают десятки видов документов, и запрос соединится с каждым. Решения:
// Дата уже есть в регистре
Движения.Период
// Нужен реквизит конкретного документа
ВЫРАЗИТЬ(Движения.Регистратор КАК Документ.РеализацияТоваровУслуг).Номер
ВЫРАЗИТЬ оставляет одно соединение. Строки других типов вернут NULL, так что при необходимости добавьте отбор Движения.Регистратор ССЫЛКА Документ.РеализацияТоваровУслуг.
Виртуальные таблицы регистров: условие в параметрах, а не в ГДЕ
Это приём, без которого оптимизация запросов 1С не обходится. Виртуальная таблица Остатки, Обороты, ОстаткиИОбороты, СрезПоследних сначала собирает данные из таблиц итогов и движений, а уже потом применяется ГДЕ.
Медленно — сначала остатки по всем складам, потом фильтр:
ВЫБРАТЬ
Остатки.Номенклатура,
Остатки.КоличествоОстаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(&Дата) КАК Остатки
ГДЕ
Остатки.Склад = &Склад
Быстро — фильтр внутри сбора остатков:
ВЫБРАТЬ
Остатки.Номенклатура,
Остатки.КоличествоОстаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(&Дата, Склад = &Склад) КАК Остатки
Имя регистра здесь для примера — подставьте свой. Правила для параметров:
- ✅ В условие параметра пишите только измерения регистра.
- ✅ Если нужен отбор по списку товаров из предыдущего шага, используйте
Номенклатура В (ВЫБРАТЬ ВТ.Номенклатура ИЗ ВТ_Товары КАК ВТ)внутри параметра. - ✅ Для
Оборотызадавайте только нужную периодичность:Период,РегистраториЗаписьрезко увеличивают объём. - 🚫 Не соединяйте виртуальную таблицу с другой таблицей без отбора в параметрах — выйдут остатки по всей базе.
💡 Если регистр огромный, а итоги давно не пересчитывались, полезно проверить настройки итогов: Все функции → Стандартные → Управление итогами. Для регистров с разделением итогов и установленным периодом рассчитанных итогов остатки на дату считаются быстрее.
Временные таблицы вместо вложенных запросов
Временная таблица решает две задачи. СУБД знает точное число строк в ней. А ИНДЕКСИРОВАТЬ ПО даёт индекс по полю, по которому дальше будет соединение.
ВЫБРАТЬ РАЗЛИЧНЫЕ
Реализация.Контрагент КАК Контрагент
ПОМЕСТИТЬ ВТ_Контрагенты
ИЗ
Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
Реализация.Дата МЕЖДУ &НачалоПериода И &КонецПериода
И Реализация.Проведен
ИНДЕКСИРОВАТЬ ПО
Контрагент
;
ВЫБРАТЬ
ВТ.Контрагент,
Контрагенты.ИНН
ИЗ
ВТ_Контрагенты КАК ВТ
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
ПО ВТ.Контрагент = Контрагенты.Ссылка
Когда временная таблица нужна, а когда лишняя:
| Ситуация | Временная таблица | Почему |
|---|---|---|
| Вложенный запрос в соединении | Да | Точное число строк, план стабильный |
| Один результат используется в 2+ местах пакета | Да | Считается один раз |
| Большой отбор, по которому дальше соединение | Да, с ИНДЕКСИРОВАТЬ ПО | Индекс по полю соединения |
| Таблица на 10–50 строк, которая читается один раз | Нет | Создание обходится дороже выгоды |
| Простой отбор, который сразу выводится | Нет | Лишняя запись в tempdb |
Индексировать стоит только поля соединения и отбора, и только если в таблице больше пары сотен строк. Индекс по таблице из 30 строк ничего не ускорит.
Таблица значений как источник
Если данные уже лежат в таблице значений, не перебирайте её с запросом на каждую строку. Передайте её параметром и положите во временную таблицу:
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
| &ТаблицаТоваров КАК Т
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура";
Запрос.УстановитьПараметр("ТаблицаТоваров", ТаблицаТоваров);
Запрос.Выполнить();
Колонки таблицы значений должны быть типизированы (тип задают при Колонки.Добавить), иначе запрос завершится ошибкой.
Индексы: ускорение запроса 1С без переписывания текста
Индексы в 1С создают в Конфигураторе, а не в СУБД вручную. Индексы, созданные напрямую в SQL, могут пропасть при реструктуризации, и поддерживать их некому.
Где задаётся индекс
- 📌 Реквизит справочника или документа: свойство
Индексироватьв палитре свойств. Варианты — «Не индексировать», «Индексировать», «Индексировать с доп. упорядочиванием». - 📌 Измерение регистра сведений и накопления: то же свойство у измерения.
- 📌 Стандартные поля:
Ссылка,Код,Наименование,Дата,Номер,Период,Регистраторуже проиндексированы платформой.
«С доп. упорядочиванием» нужен, когда по реквизиту часто отбирают в динамическом списке с сортировкой по коду или наименованию.
Порядок измерений имеет значение
Основной индекс регистра строится по измерениям в порядке их расположения в конфигурации. Условие по второму измерению без первого использует индекс хуже. Для регистра «Склад, Номенклатура» отбор только по Номенклатура может пойти сканированием — помогает отдельный индекс по этому измерению.
Изменение свойства Индексировать в рабочей типовой конфигурации снимает её с полной поддержки и требует обновления структуры базы с монопольным доступом. На большой таблице реструктуризация занимает часы. Делайте это в нерабочее время, после резервной копии, и согласуйте с тем, кто обновляет конфигурацию.
Каждый индекс замедляет запись. Не индексируйте реквизит «на всякий случай»: только если по нему регулярно отбирают и это видно в плане.
Группировки и агрегатные функции без лишней нагрузки
- 📊 Отбор до группировки. Условие на исходные поля пишите в
ГДЕ.ИМЕЮЩИЕ— только для условий на результат агрегата, напримерИМЕЮЩИЕ СУММА(Т.Сумма) > 100000. - 📊 Не группируйте движения вручную, если есть виртуальная таблица
Обороты. Она берёт данные из итогов и почти всегда быстрее, чемСГРУППИРОВАТЬ ПОпо таблице движений. - 📊 Группируйте по ссылке, а не по реквизитам через точку.
СГРУППИРОВАТЬ ПО Т.Номенклатура.Наименованиедобавляет соединение и сортировку по строке. Группируйте поТ.Номенклатура, а представление получайте черезПРЕДСТАВЛЕНИЕ(Т.Номенклатура)в итоговой выборке. - 📊
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ …)иРАЗЛИЧНЫЕпо миллионам строк требуют сортировки. Если уникальность гарантирована структурой данных,РАЗЛИЧНЫЕне пишите. - 📊
ИТОГИв запросе считаются отдельными проходами. В отчётах на СКД итоги лучше отдать компоновщику, а в запросе оставить только детальные данные.
Пример переписывания. Было — группировка по документам с отбором после:
ВЫБРАТЬ
Т.Номенклатура,
СУММА(Т.Количество) КАК Количество
ИЗ
РегистрНакопления.Продажи КАК Т
СГРУППИРОВАТЬ ПО
Т.Номенклатура
ИМЕЮЩИЕ
МАКСИМУМ(Т.Период) >= &Начало
Стало — оборот за период из итогов:
ВЫБРАТЬ
Т.Номенклатура,
Т.КоличествоОборот КАК Количество
ИЗ
РегистрНакопления.Продажи.Обороты(&Начало, &Конец) КАК Т
Учтите, что второй вариант считает именно оборот за период — это то, что обычно и нужно в отчёте о продажах.
Антипаттерны, которые замедляют запросы 1С
Ошибки, которые встречаются в доработках чаще всего и которые не видны в одном отдельно взятом запросе.
Запрос в цикле
Для Каждого Строка Из Товары Цикл
Запрос.УстановитьПараметр("Товар", Строка.Номенклатура);
Результат = Запрос.Выполнить();
…
КонецЦикла;
1000 строк — 1000 обращений к СУБД. Каждое занимает миллисекунды, вместе — минуты. Решение: один запрос с параметром-массивом ГДЕ Т.Номенклатура В (&МассивТоваров) или временная таблица из таблицы значений, а потом соединение.
Чтение объекта ради одного реквизита
Ссылка.ПолучитьОбъект().Реквизит читает объект целиком с табличными частями. Нужен один реквизит — получите его запросом или через ОбщегоНазначения.ЗначениеРеквизитаОбъекта в типовых конфигурациях на БСП.
Остальное
- 🐢 Выборка всех полей, когда нужно два.
- 🐢 Соединение одной и той же виртуальной таблицы несколько раз в одном запросе вместо одной временной таблицы.
- 🐢 Перебор результата
Выбрать()с обращением к реквизитам ссылок через точку внутри цикла — каждое обращение отдельный запрос. - 🐢 Отбор по полю составного типа без
ССЫЛКАилиВЫРАЗИТЬ.
Когда тормозит не сам запрос
Частые вопросы
Есть ли смысл в оптимизации запросов в файловой базе?
Помогает ли ПЕРВЫЕ N ускорить запрос?
Можно ли смотреть план выполнения из конструктора запроса?
plansql или средствами самой СУБД.Что быстрее: левое соединение или вложенный запрос в списке полей?
В (…) по большой таблице лучше сначала положить во временную таблицу с индексом.Нужно ли удалять временные таблицы?
УНИЧТОЖИТЬ ВТ_Имя или закройте менеджер методом Закрыть(), когда таблицы больше не нужны.