Рассмотрим распространенную ситуацию, с которой сталкивается практически каждый разработчик 1С: вы написали отчет или обработку, логика верна, данные выбираются корректно, но время выполнения оставляет желать лучшего. Запрос выполняется минуты, а то и дольше, блокируя работу пользователей. В этой статье мы разберем реальный пример оптимизации, проанализируем ошибки, которые приводят к падению производительности, и научимся писать "быстрый" код.
Мы будем действовать последовательно: сначала посмотрим на проблемный участок, затем выясним техническую причину замедления на уровне платформы и SQL, а после — напишем правильный вариант. Наша задача — не просто исправить ошибку, а понять принцип работы механизма запросов в 1С.
Одна из самых частых причин медленной работы — неправильная фильтрация данных при работе с регистрами (накопления, сведений, бухгалтерии). Посмотрим на типичный пример, который часто пишут начинающие программисты.
Допустим, нам нужно получить остатки товаров на определенном складе. Часто можно встретить такой подход:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыНаСкладахОстатки.Номенклатура,
| ТоварыНаСкладахОстатки.ВНаличииОстаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки КАК ТоварыНаСкладахОстатки
|ГДЕ
| ТоварыНаСкладахОстатки.Склад = &Склад";
На первый взгляд, все логично: мы выбираем данные из виртуальной таблицы остатков и накладываем условие ГДЕ, чтобы отфильтровать нужный склад. Однако, с точки зрения производительности, этот код может работать катастрофически медленно на больших базах.
В чем проблема?
Механизм виртуальных таблиц в 1С устроен так, что если вы используете конструкцию ГДЕ к результату виртуальной таблицы, СУБД (SQL Server или PostgreSQL) сначала построит полную таблицу остатков по всем складам, которые есть в базе, и только потом, из этого огромного массива, отберет нужный вам склад.
Давайте исправим это. Виртуальные таблицы имеют свои параметры, которые позволяют отфильтровать данные на этапе их получения. Это кардинально меняет план выполнения запроса: система сразу рассчитает остатки только по одному складу.
Правильный вариант кода выглядит так:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыНаСкладахОстатки.Номенклатура,
| ТоварыНаСкладахОстатки.ВНаличииОстаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки( , Склад = &Склад) КАК ТоварыНаСкладахОстатки";
Обратите внимание: мы перенесли условие внутрь скобок параметров виртуальной таблицы .Остатки(...). Первый параметр там — это "Период" (мы его пропустили, оставив запятую, чтобы получить актуальные остатки), а второй — "Условие".
Запомните правило: всегда накладывайте отборы в параметрах виртуальных таблиц, а не в секции ГДЕ, если логика задачи это позволяет.
Вторая популярная причина замедления — обращение к полям ссылочного типа через точку. В среде разработчиков это называется "неявным соединением" или "разыменованием".
Рассмотрим пример запроса, где мы хотим получить данные о продажах и вывести регион партнера:
Запрос.Текст =
"ВЫБРАТЬ
| Продажи.Партнер,
| Продажи.Партнер.БизнесРегион.Наименование КАК Регион,
| Продажи.СуммаОборот
|ИЗ
| РегистрНакопления.ВыручкаИСебестоимостьПродаж.Обороты КАК Продажи";
В строке Продажи.Партнер.БизнесРегион.Наименование мы видим двойное использование точки. Для нас это выглядит как простое обращение к свойству. Но давайте посмотрим, во что платформа 1С превращает этот код на языке SQL.
Каждая точка — это команда LEFT JOIN (Левое Соединение) с целой таблицей справочника. В данном случае:
Если в выборке участвуют тысячи записей, а вы обращаетесь через точку к нескольким разным полям (например, Партнер.Код, Партнер.Наименование, Партнер.Менеджер.ФизЛицо.ДатаРождения), запрос превращается в монстра с десятками соединений, которые "убивают" оптимизатор СУБД.
Как поступить правильно?
Если нам нужны данные из связанных таблиц, лучше описать соединение явно. Это дает нам контроль над процессом и позволяет оптимизатору SQL построить более эффективный план запроса.
Запрос.Текст =
"ВЫБРАТЬ
| Продажи.Партнер,
| СпрРегионы.Наименование КАК Регион,
| Продажи.СуммаОборот
|ИЗ
| РегистрНакопления.ВыручкаИСебестоимостьПродаж.Обороты КАК Продажи
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Партнеры КАК СпрПартнеры
| ПО Продажи.Партнер = СпрПартнеры.Ссылка
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.БизнесРегионы КАК СпрРегионы
| ПО СпрПартнеры.БизнесРегион = СпрРегионы.Ссылка";
Хотя код стал длиннее, он работает предсказуемо. Однако, если вам нужно получить только один реквизит (например, просто Код партнера) и вложенность небольшая (одна точка), использование разыменования допустимо. Но избегайте цепочек из 2-3 точек и более.
Разберем еще один нюанс, который часто упускают из виду. Это использование оператора ИЛИ в секции ГДЕ. Рассмотрим ситуацию, когда нам нужно найти документы, где контрагентом выступает либо "Компания А", либо "Компания Б".
Запрос.Текст =
"ВЫБРАТЬ
| Реализация.Ссылка
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
| Реализация.Контрагент = &Контрагент1
| ИЛИ Реализация.Контрагент = &Контрагент2";
Проблема оператора ИЛИ заключается в том, что он часто блокирует использование индексов таблиц базы данных. СУБД не может эффективно использовать индекс по полю "Контрагент", когда видит альтернативное условие, и вынуждена сканировать таблицу целиком (Table Scan).
Для оптимизации таких запросов рекомендуется использовать конструкцию ОБЪЕДИНИТЬ ВСЕ. Мы разбиваем один сложный запрос на два простых, каждый из которых отлично отрабатывает по индексу, а затем склеиваем результаты.
Запрос.Текст =
"ВЫБРАТЬ
| Реализация.Ссылка
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
| Реализация.Контрагент = &Контрагент1
|
|ОБЪЕДИНИТЬ ВСЕ
|
|ВЫБРАТЬ
| Реализация.Ссылка
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
| Реализация.Контрагент = &Контрагент2";
Такой подход может показаться громоздким, но на больших объемах данных (сотни тысяч документов) разница в скорости может быть десятикратной.
Нередко возникает задача отобрать данные по большому списку номенклатуры или контрагентов, который мы подготовили в коде (например, массив из 1000 элементов).
Неопытный разработчик передаст этот список прямо в параметр:
ГДЕ Товар В (&СписокТоваров)
Если список маленький (10-20 элементов), это нормально. Но если элементов сотни или тысячи, это создаст огромную нагрузку. Правильным решением будет использование Временных таблиц (Менеджер временных таблиц).
Давайте разберем алгоритм действий:
МенеджерВременныхТаблиц.ВНУТРЕННЕЕ СОЕДИНЕНИЕ с этой временной таблицей.Пример реализации:
// 1. Подготовка списка
МассивТоваров = Новый Массив;
// ... заполнение массива ...
// 2. Помещение во временную таблицу
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| &Товар КАК Ссылка
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
| &ТЗТоваров КАК ТЗТоваров"; // Здесь мы передаем таблицу значений или массив как внешний источник
// Обязательно типизируем колонку, если передаем ТЗ, для массива платформа сделает это сама
Запрос.УстановитьПараметр("ТЗТоваров", МассивТоваров);
Запрос.Выполнить();
// 3. Основной запрос с соединением
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.Номенклатура,
| Остатки.КоличествоОстаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Товары КАК Фильтр
| ПО Остатки.Номенклатура = Фильтр.Ссылка";
Результат = Запрос.Выполнить();
Использование ВНУТРЕННЕЕ СОЕДИНЕНИЕ с временной таблицей работает как жесткий фильтр. СУБД оптимизирует этот процесс гораздо лучше, чем оператор В (...) с огромным списком параметров.
Хотя эта статья посвящена языку запросов, нельзя не упомянуть ошибку, которая сводит на нет любую оптимизацию запроса. Это обращение к реквизитам полученной выборки через точку внутри цикла обхода результата.
Представим, что мы выбрали запросом только ссылки на номенклатуру:
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// ГРУБАЯ ОШИБКА ПРОИЗВОДИТЕЛЬНОСТИ:
Сообщить(Выборка.Номенклатура.Артикул);
КонецЦикла;
В строке Выборка.Номенклатура.Артикул происходит следующее: платформа видит, что в выборке есть только ссылка, а вы просите артикул. Она делает скрытый запрос к базе данных, чтобы прочитать объект целиком (или нужные поля). Если в выборке 10 000 строк, вы сделаете 10 000 дополнительных микро-запросов к серверу базы данных.
Решение: Всегда выбирайте все необходимые поля сразу в тексте запроса.
// Правильный запрос
Запрос.Текст = "ВЫБРАТЬ Номенклатура, Номенклатура.Артикул КАК Артикул ...";
// ...
Пока Выборка.Следующий() Цикл
Сообщить(Выборка.Артикул); // Данные уже выбраны, обращения к БД не происходит
КонецЦикла;
Для того чтобы ваши решения на платформе 1С:Предприятие работали быстро и стабильно, следуйте чек-листу при написании запросов:
ГДЕ для фильтрации регистров.JOIN).ИЛИ на ОБЪЕДИНИТЬ ВСЕ, если это возможно.Соблюдение этих простых правил позволит вам избежать 90% проблем с производительностью и сделает работу пользователей комфортной.