Оптимизация запросов в 1С за 7 шагов: индексы и СКД

Почему оптимизация запросов в 1С — это не роскошь, а необходимость
Медленные запросы в 1С — одна из главных причин, по которым пользователи жалуются на «тормозящую» систему. Когда отчёт формируется пять минут, а проведение документа вызывает таймаут, бизнес теряет деньги и нервные клетки. При этом большинство проблем с производительностью решается без дорогостоящего апгрейда железа — достаточно грамотно переписать запросы.
В этой статье мы разберём 7 конкретных шагов, которые позволят вам систематически улучшить производительность запросов в 1С:Предприятие 8. Каждый шаг сопровождается рабочим кодом, объяснением механизма и типичными ошибками, которых следует избегать. Подход применим как к конфигурациям на платформе 8.3, так и к доработкам в задачах по 1С:ERP и других тяжёлых решениях.
Важно: перед любой оптимизацией измерьте текущее время выполнения запросов с помощью инструментов замера производительности. Без базовых метрик невозможно оценить эффект изменений.
Шаг 1. Профилирование — найдите «узкое место» до начала оптимизации
Оптимизировать вслепую — всё равно что лечить пациента без диагноза. Первый шаг — выявить конкретные запросы, которые тормозят систему. В 1С для этого есть несколько инструментов.
Замер производительности встроенными средствами
Используйте объект ЗамерВремени и технологический журнал. Для быстрой диагностики в коде можно использовать следующий подход:
// Замер времени выполнения запроса
Процедура ЗамерВременниЗапроса()
// Фиксируем время начала
ВремяНачала = ТекущаяДатаСекунда();
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Продажи.Номенклатура,
| СУММА(Продажи.КоличествоОборот) КАК КоличествоОборот
|ИЗ
| РегистрНакопления.Продажи.Обороты(
| &НачалоПериода,
| &КонецПериода,
| День,
| ) КАК Продажи
|СГРУППИРОВАТЬ ПО
| Продажи.Номенклатура";
Запрос.УстановитьПараметр("НачалоПериода", НачалоГода(ТекущаяДата()));
Запрос.УстановитьПараметр("КонецПериода", КонецГода(ТекущаяДата()));
Результат = Запрос.Выполнить();
// Считаем время выполнения
ВремяВыполнения = ТекущаяДатаСекунда() - ВремяНачала;
Сообщить("Запрос выполнен за: " + ВремяВыполнения + " сек.");
КонецПроцедуры
Технологический журнал
Для системного анализа настройте технологический журнал с фильтром по событию DBMSSQL (или DBPOSTGRS для PostgreSQL). Это позволит увидеть реальные SQL-запросы, которые 1С отправляет на сервер БД, и найти те, что выполняются дольше заданного порога. Рекомендуемый порог для начала анализа — 1000 мс.
После профилирования у вас будет список «виновников». Именно с ними мы будем работать на следующих шагах.
Шаг 2. Индексы в 1С — правильная настройка без лишних накладных расходов
Индексы — самый мощный инструмент ускорения запросов. Но у них есть обратная сторона: каждый дополнительный индекс замедляет запись данных и увеличивает размер базы. Поэтому важно добавлять только те индексы, которые реально используются.
Индексирование реквизитов объектов метаданных
В свойствах реквизита справочника, документа или регистра есть флаг «Индексировать». Устанавливайте его для полей, которые:
- Используются в условиях отбора (секция ГДЕ запроса);
- Участвуют в соединениях таблиц (секция СОЕДИНЕНИЕ ... ПО);
- Применяются в сортировке больших выборок (УПОРЯДОЧИТЬ ПО).
Составные индексы в регистрах
Для регистров накопления и сведений особенно важен порядок полей в составном индексе. Правило «левого префикса»: индекс по полям (А, Б, В) ускорит запросы с условием по А, по А+Б, по А+Б+В, но не ускорит запрос только по Б или В.
// Пример запроса, который выиграет от составного индекса
// по полям (Номенклатура, Склад, Период)
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.КоличествоОстаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(
| &МоментВремени,
| Номенклатура = &Номенклатура
| И Склад = &Склад
| ) КАК Остатки";
Индексирование временных таблиц
Если вы используете временные таблицы в пакетных запросах, обязательно индексируйте их поля, по которым происходит соединение. Это делается директивой ИНДЕКСИРОВАТЬ ПО:
// Создание временной таблицы с индексом для ускорения соединений
Запрос.Текст =
"ВЫБРАТЬ
| Документы.Ссылка КАК ДокументСсылка,
| Документы.Контрагент
|ПОМЕСТИТЬ ВТ_Документы
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Документы
|ГДЕ
| Документы.Дата МЕЖДУ &НачалоПериода И &КонецПериода
| И Документы.Проведен = ИСТИНА
|ИНДЕКСИРОВАТЬ ПО
| ДокументСсылка,
| Контрагент
|;
|
|// Второй запрос пакета использует индексированную временную таблицу
|ВЫБРАТЬ
| ВТ_Документы.Контрагент,
| ТаблицаТоваров.Номенклатура,
| СУММА(ТаблицаТоваров.Количество) КАК ИтогоКоличество
|ИЗ
| ВТ_Документы КАК ВТ_Документы
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК ТаблицаТоваров
| ПО ВТ_Документы.ДокументСсылка = ТаблицаТоваров.Ссылка
|СГРУППИРОВАТЬ ПО
| ВТ_Документы.Контрагент,
| ТаблицаТоваров.Номенклатура";
Шаг 3. Управление блокировками — от тупиков к параллельной работе
Блокировки — второй по значимости источник проблем с производительностью. В многопользовательской среде неправильно настроенные блокировки приводят к взаимным ожиданиям и, в худшем случае, к дедлокам. Особенно актуально для 1С:Бухгалтерия, где одновременно работают десятки пользователей.
Управляемые блокировки vs. автоматические
В современных конфигурациях рекомендуется использовать управляемый режим блокировок. Он даёт разработчику контроль над тем, когда и на что накладываются блокировки, в отличие от автоматического режима, который блокирует данные «на всякий случай».
// Пример использования управляемых блокировок
Процедура ПровестиДокументСБлокировкой(ДокументСсылка)
НачатьТранзакцию();
Попытка
// Устанавливаем блокировку только на нужные данные
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить(
"РегистрНакопления.ТоварыНаСкладах");
ЭлементБлокировки.УстановитьЗначение(
"Номенклатура",
ДокументСсылка.Номенклатура);
ЭлементБлокировки.УстановитьЗначение(
"Склад",
ДокументСсылка.СкладОтправитель);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
// Выполняем операции с данными
ДокументОбъект = ДокументСсылка.ПолучитьОбъект();
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
Чтение данных без блокировки
Для запросов, которые только читают данные (например, для построения отчётов), используйте подсказку РАЗРЕШЕННЫЕ и избегайте чтения в транзакции без необходимости. Это кардинально снижает конкуренцию за ресурсы:
// Запрос для отчёта — читаем без лишних блокировок
Запрос.Текст =
"ВЫБРАТЬ РАЗРЕШЕННЫЕ
| Контрагенты.Наименование,
| Взаиморасчеты.СуммаОстаток
|ИЗ
| РегистрНакопления.ВзаиморасчетыСКонтрагентами.Остатки КАК Взаиморасчеты
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
| ПО Взаиморасчеты.Контрагент = Контрагенты.Ссылка
|ГДЕ
| Взаиморасчеты.СуммаОстаток <> 0";
Шаг 4. Переписываем запросы — типичные антипаттерны и их замена
Даже опытные разработчики допускают ошибки в написании запросов, которые незаметны на малых объёмах данных, но катастрофически сказываются на производительности при росте базы.
Антипаттерн 1: Функции в условии ГДЕ
Применение функций к полям в секции ГДЕ делает использование индекса невозможным. СУБД вынуждена перебрать все строки таблицы.
// ПЛОХО: функция ГОД() не позволяет использовать индекс по полю Дата
// ГДЕ ГОД(Документы.Дата) = 2024
// ХОРОШО: используем диапазон дат — индекс работает
Запрос.Текст =
"ВЫБРАТЬ
| Документы.Ссылка,
| Документы.Дата,
| Документы.Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Документы
|ГДЕ
| Документы.Дата >= &НачалоПериода
| И Документы.Дата <= &КонецПериода
| И Документы.Проведен = ИСТИНА";
Антипаттерн 2: Запросы в цикле
Это, пожалуй, самая распространённая и разрушительная ошибка. Каждое обращение к базе данных имеет накладные расходы. Тысяча запросов в цикле — это тысячекратные накладные расходы.
// ПЛОХО: запрос внутри цикла
// Для каждого документа — отдельный запрос к базе!
Для Каждого СтрокаТаблицы Из ТаблицаДокументов Цикл
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ ... ГДЕ Документ.Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Ссылка", СтрокаТаблицы.Ссылка);
// Это катастрофа производительности!
КонецЦикла;
// ХОРОШО: один запрос для всех документов с использованием списка значений
СписокСсылок = Новый СписокЗначений;
Для Каждого СтрокаТаблицы Из ТаблицаДокументов Цикл
СписокСсылок.Добавить(СтрокаТаблицы.Ссылка);
КонецЦикла;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Документы.Ссылка,
| Документы.Сумма,
| Документы.Контрагент
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Документы
|ГДЕ
| Документы.Ссылка В (&СписокСсылок)";
Запрос.УстановитьПараметр("СписокСсылок", СписокСсылок);
Результат = Запрос.Выполнить();
Антипаттерн 3: Избыточные соединения через ОБЪЕДИНИТЬ
Оператор ОБЪЕДИНИТЬ ВСЕ работает быстрее, чем ОБЪЕДИНИТЬ, потому что не выполняет дедупликацию строк. Используйте ОБЪЕДИНИТЬ только тогда, когда дубликаты действительно нужно убрать.
Шаг 5. Виртуальные таблицы и параметры периода — используйте правильно
Виртуальные таблицы (Остатки, Обороты, ОстаткиИОбороты) — мощный инструмент 1С, но их неправильное использование приводит к огромным нагрузкам на СУБД.
Передавайте условия внутрь виртуальной таблицы
Критически важное правило: условия отбора по измерениям регистра нужно передавать в параметры виртуальной таблицы, а не в секцию ГДЕ внешнего запроса. Во втором случае система сначала развернёт все данные таблицы, а потом отфильтрует — это катастрофически медленно.
// ПЛОХО: условие снаружи — сначала читаем ВСЕ остатки, потом фильтруем
// ЭТО ОЧЕНЬ МЕДЛЕННО НА БОЛЬШИХ БАЗАХ!
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.Номенклатура,
| Остатки.КоличествоОстаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
|ГДЕ
| Остатки.Склад = &Склад";
// ХОРОШО: условие внутри параметров виртуальной таблицы
// СУБД сразу читает только нужные данные
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.Номенклатура,
| Остатки.КоличествоОстаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(
| &МоментВремени,
| Склад = &Склад
| ) КАК Остатки";
Запрос.УстановитьПараметр("МоментВремени", МоментВремени);
Запрос.УстановитьПараметр("Склад", ВыбранныйСклад);
Выбор между Остатки и ОстаткиИОбороты
Если вам нужны только остатки — используйте .Остатки(). Таблица .ОстаткиИОбороты() делает значительно больше работы и нужна только тогда, когда вам одновременно нужны и остатки, и обороты за период. Не берите лишнего.
Шаг 6. Оптимизация СКД — настройка схемы компоновки данных для скорости
Система компоновки данных — удобный инструмент для построения отчётов, но без должной настройки СКД-отчёты могут работать медленнее, чем аналогичные запросы, написанные вручную. Разберём ключевые точки оптимизации.
Ограничение выбираемых полей
По умолчанию СКД может выбирать из базы данных больше полей, чем нужно для конкретного варианта отчёта. Используйте настройки доступных полей и явно ограничивайте набор выбираемых данных в зависимости от варианта отчёта.
Правильное использование параметров СКД
// Программная настройка СКД с оптимальными параметрами
Процедура СформироватьОтчетСКД(НачалоПериода, КонецПериода, Склад)
// Получаем схему компоновки
Схема = РеквизитФормыВЗначение("СхемаКомпоновкиДанных");
// Создаём настройки компоновки
Настройки = Схема.НастройкиПоУмолчанию;
// Устанавливаем параметры отбора
ДляПараметра = Настройки.ПараметрыДанных.НайтиЗначениеПараметра(
Новый ПараметрКомпоновкиДанных("НачалоПериода"));
ДляПараметра.Значение = НачалоПериода;
ДляПараметра.Использование = Истина;
ДляПараметра = Настройки.ПараметрыДанных.НайтиЗначениеПараметра(
Новый ПараметрКомпоновкиДанных("КонецПериода"));
ДляПараметра.Значение = КонецПериода;
ДляПараметра.Использование = Истина;
// Добавляем отбор по складу для уменьшения объёма данных
ЭлементОтбора = Настройки.Отбор.Элементы.Добавить(
Тип("ЭлементОтбораКомпоновкиДанных"));
ЭлементОтбора.ЛевоеЗначение = Новый ПолеКомпоновкиДанных("Склад");
ЭлементОтбора.ВидСравнения = ВидСравненияКомпоновкиДанных.Равно;
ЭлементОтбора.ПравоеЗначение = Склад;
ЭлементОтбора.Использование = Истина;
// Компонуем и выводим результат
Компоновщик = Новый КомпоновщикМакетаКомпоновкиДанных;
Макет = Компоновщик.Выполнить(Схема, Настройки, , , Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцийЗначений"));
Процессор = Новый ПроцессорКомпоновкиДанных;
Процессор.Инициализировать(Макет, , , Истина);
КонецПроцедуры
Наборы данных в СКД: объединение vs. соединение
В СКД можно связывать несколько наборов данных. Связь типа «Объединение» выполняется на уровне СУБД (быстро), а связь типа «Соединение» — на уровне платформы 1С (медленно, особенно на больших объёмах). Всегда предпочитайте один хорошо написанный запрос нескольким наборам данных с соединением на уровне СКД.
Для сложных аналитических отчётов, которые нужны в электронном документообороте или при интеграциях, особенно важно контролировать количество обращений к базе данных из СКД.
Найдите специалиста для решения этой задачи на koderion.ru