Что тормозит запросы
Разбираем четыре главных греха, из-за которых запрос из миллисекунд превращается в минуты, и учимся переписывать их правильно.
Оптимизация запроса — это не «сделать код красивее», а перенести работу с перебора строк вручную на движок СУБД, который умеет обрабатывать миллионы записей одним набором операций.
В 1С есть коварная особенность: неоптимальный запрос почти всегда работает правильно. На тестовой базе из пятидесяти документов он отдаёт верный результат за доли секунды, разработчик радуется и уходит домой. А через полгода в рабочей базе накопились сотни тысяч документов — и та же обработка проведения висит по три минуты, кассир нервничает, а вы получаете задачу «почему тормозит». Поэтому узнавать вредные конструкции нужно в лицо, ещё на этапе написания кода. Ниже — четыре самых частых греха новичка и способы их искупить.
Грех первый: запрос внутри цикла
Это чемпион по замедлению. Идея кажется логичной: «пройдусь по строкам документа и для каждой строки схожу в базу за нужными данными». Проблема в том, что каждый вызов Выполнить() — это отдельное обращение к серверу СУБД: подготовка, отправка, ожидание ответа. Сто строк — сто обращений. Тысяча строк — тысяча.
// ПЛОХО: запрос внутри цикла по строкам документа
Для Каждого СтрокаТовары Из Документ.Товары Цикл
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ Цена ИЗ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Дата, Номенклатура = &Ном) КАК Цены";
Запрос.УстановитьПараметр("Дата", Документ.Дата);
Запрос.УстановитьПараметр("Ном", СтрокаТовары.Номенклатура);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
СтрокаТовары.Цена = Выборка.Цена;
КонецЕсли;
КонецЦикла;
Лечится это пакетной выборкой: получаем цены сразу для всех товаров документа одним запросом, а в цикле только раскладываем готовый результат. Список номенклатуры удобно передать в параметр целиком — движок сам подставит его в условие В (&СписокНоменклатуры).
// ХОРОШО: один запрос на весь документ
МассивНоменклатуры = Документ.Товары.ВыгрузитьКолонку("Номенклатура");
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Цены.Номенклатура КАК Номенклатура,
| Цены.Цена КАК Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
| &Дата, Номенклатура В (&СписокНоменклатуры)) КАК Цены";
Запрос.УстановитьПараметр("Дата", Документ.Дата);
Запрос.УстановитьПараметр("СписокНоменклатуры", МассивНоменклатуры);
Цены = Запрос.Выполнить().Выгрузить();
// дальше в цикле только ищем цену в готовой таблице Цены — без обращений к базе
Разница на базе с большим справочником — десятки раз. Правило простое: если видите Выполнить() внутри Цикл — почти наверняка это ошибка.
Грех второй: соединение с вложенным запросом
Иногда нужно соединить основную таблицу не с реальной таблицей, а с результатом расчёта. Начинающий пишет вложенный запрос прямо в секции соединения:
// ПЛОХО: соединение с вложенным запросом
ВЫБРАТЬ
Товары.Номенклатура,
Продажи.Выручка
ИЗ
Справочник.Номенклатура КАК Товары
ЛЕВОЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ Ном КАК Номенклатура, СУММА(Сумма) КАК Выручка
ИЗ РегистрНакопления.Продажи
СГРУППИРОВАТЬ ПО Ном
) КАК Продажи
ПО Продажи.Номенклатура = Товары.Ссылка
Беда в том, что у результата вложенного запроса нет индексов. Соединяя с ним, СУБД вынуждена для каждой строки справочника заново просматривать весь промежуточный набор. Правильный приём — вынести подзапрос во временную таблицу через пакет запросов и, если строк много, проиндексировать поле соединения.
// ХОРОШО: временная таблица + индекс поля соединения
ВЫБРАТЬ
Продажи.Ном КАК Номенклатура,
СУММА(Продажи.Сумма) КАК Выручка
ПОМЕСТИТЬ Вт_Продажи
ИЗ
РегистрНакопления.Продажи КАК Продажи
СГРУППИРОВАТЬ ПО
Продажи.Ном
ИНДЕКСИРОВАТЬ ПО
Номенклатура
;
ВЫБРАТЬ
Товары.Номенклатура КАК Номенклатура,
Продажи.Выручка КАК Выручка
ИЗ
Справочник.Номенклатура КАК Товары
ЛЕВОЕ СОЕДИНЕНИЕ Вт_Продажи КАК Продажи
ПО Продажи.Номенклатура = Товары.Ссылка
Временные таблицы для 1С — родной механизм: их создаёт менеджер временных таблиц, и по проиндексированному полю соединение снова становится быстрым.
Грех третий: ПОДОБНО по подстроке
Оператор ПОДОБНО (аналог SQL LIKE) ищет строки по шаблону. И тут решает положение символа-джокера %. Если % стоит только в конце — «поиск по началу строки» — СУБД может воспользоваться индексом. Если % стоит и в начале — «поиск по подстроке» — индекс бесполезен, и база честно перебирает все записи.
// БЫСТРО: поиск по началу — работает индекс
ГДЕ Контрагенты.Наименование ПОДОБНО &Начало // параметр = "Ромаш%"
// МЕДЛЕННО: поиск по подстроке — полный перебор
ГДЕ Контрагенты.Наименование ПОДОБНО &Кусок // параметр = "%ромаш%"
Вывод не в том, что подстроку искать нельзя — иногда без неё никак. Просто помните цену: на строке поиска в интерфейсе, которая дёргается на каждый введённый символ, «начало строки» всегда предпочтительнее. А для полноценного поиска по подстроке в 1С есть отдельный механизм — полнотекстовый поиск.
Грех четвёртый: условие в ГДЕ вместо параметров виртуальной таблицы
Виртуальные таблицы регистров (Остатки, Обороты, СрезПоследних) принимают параметры отбора прямо в скобках. И это принципиально: параметр отрабатывает до расчёта итогов, а условие в ГДЕ — после. Разница огромна.
// ПЛОХО: отбор по складу в ГДЕ — сначала считаем ВСЕ склады, потом отсекаем
ВЫБРАТЬ Остатки.Номенклатура, Остатки.КоличествоОстаток
ИЗ РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
ГДЕ Остатки.Склад = &Склад
// ХОРОШО: отбор в параметрах ВТ — движок считает итоги только по нужному складу
ВЫБРАТЬ Остатки.Номенклатура, Остатки.КоличествоОстаток
ИЗ РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки
Во втором варианте СУБД сразу отбирает движения нужного склада и только по ним считает остаток. В первом — считает остатки по всем складам компании, а потом выбрасывает лишнее. На большом регистре это отличается в разы. Этому греху в учебнике посвящён отдельный урок в разделе про виртуальные таблицы — здесь важно запомнить его как одну из главных причин тормозов.
Как это работает
Под капотом любой запрос 1С транслируется в SQL и уходит на сервер СУБД (обычно PostgreSQL или MS SQL Server). Сервер строит план выполнения — пошаговую инструкцию, как добыть данные. Всё, что мы называем «оптимизацией», сводится к одному: помочь СУБД построить план, где данные берутся по индексу (точечно), а не сканированием таблицы (перебором всех строк).
Запрос в цикле плох, потому что превращает одну операцию над множеством в тысячи мелких. Соединение с подзапросом и ПОДОБНО с ведущим % плохи, потому что лишают СУБД возможности применить индекс. Условие в ГДЕ вместо параметров ВТ плохо, потому что заставляет считать лишнее. Во всех четырёх случаях лекарство одно — дать движку работать множествами и по индексам.
Частые ошибки
- «Перепишу цикл на запрос, но оставлю
Выполнить()внутри» — самообман. Вынести нужно именно вызов к базе, а не просто причесать текст. - Получать в выборку всё подряд
ВЫБРАТЬ *и колонки «на всякий случай» — лишние поля тянут лишние соединения и данные по сети. Выбирайте ровно то, что используете. - Считать, что раз на тестовой базе быстро — значит быстро всегда. Проверяйте на объёме, близком к рабочему, или хотя бы прикидывайте, сколько строк будет в проде.
- Индексировать временную таблицу, в которой три строки. Индекс сам по себе стоит времени на построение; на крошечных наборах он вреден. Индексируйте, когда строк действительно много и по полю идёт соединение.
Итоги
- Неоптимальный запрос обычно правильный — беда всплывает только на объёме. Узнавайте вредные конструкции при написании.
- Запрос внутри цикла — главный грех. Заменяйте пакетной выборкой одним запросом на весь набор.
- Соединение с вложенным запросом лишает СУБД индексов. Выносите подзапрос во временную таблицу и индексируйте поле соединения.
ПОДОБНО«по началу строки» ("текст%") может идти по индексу, «по подстроке» ("%текст%") — всегда полный перебор.- Отбор кладите в параметры виртуальной таблицы, а не в
ГДЕ: он отработает до расчёта итогов.