Почему медленно работает запрос в 1С и как провести его оптимизацию?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Рассмотрим распространенную ситуацию, с которой сталкивается практически каждый разработчик 1С: вы написали отчет или обработку, логика верна, данные выбираются корректно, но время выполнения оставляет желать лучшего. Запрос выполняется минуты, а то и дольше, блокируя работу пользователей. В этой статье мы разберем реальный пример оптимизации, проанализируем ошибки, которые приводят к падению производительности, и научимся писать "быстрый" код.

Мы будем действовать последовательно: сначала посмотрим на проблемный участок, затем выясним техническую причину замедления на уровне платформы и SQL, а после — напишем правильный вариант. Наша задача — не просто исправить ошибку, а понять принцип работы механизма запросов в 1С.

Анализ использования Виртуальных таблиц

Одна из самых частых причин медленной работы — неправильная фильтрация данных при работе с регистрами (накопления, сведений, бухгалтерии). Посмотрим на типичный пример, который часто пишут начинающие программисты.

Допустим, нам нужно получить остатки товаров на определенном складе. Часто можно встретить такой подход:


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

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

В чем проблема?

Механизм виртуальных таблиц в 1С устроен так, что если вы используете конструкцию ГДЕ к результату виртуальной таблицы, СУБД (SQL Server или PostgreSQL) сначала построит полную таблицу остатков по всем складам, которые есть в базе, и только потом, из этого огромного массива, отберет нужный вам склад.

Давайте исправим это. Виртуальные таблицы имеют свои параметры, которые позволяют отфильтровать данные на этапе их получения. Это кардинально меняет план выполнения запроса: система сразу рассчитает остатки только по одному складу.

Правильный вариант кода выглядит так:


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

Обратите внимание: мы перенесли условие внутрь скобок параметров виртуальной таблицы .Остатки(...). Первый параметр там — это "Период" (мы его пропустили, оставив запятую, чтобы получить актуальные остатки), а второй — "Условие".

Запомните правило: всегда накладывайте отборы в параметрах виртуальных таблиц, а не в секции ГДЕ, если логика задачи это позволяет.

Проблема "Разыменования" полей (Использование точки)

Вторая популярная причина замедления — обращение к полям ссылочного типа через точку. В среде разработчиков это называется "неявным соединением" или "разыменованием".

Рассмотрим пример запроса, где мы хотим получить данные о продажах и вывести регион партнера:


Запрос.Текст = 
    "ВЫБРАТЬ
    |	Продажи.Партнер,
    |	Продажи.Партнер.БизнесРегион.Наименование КАК Регион,
    |	Продажи.СуммаОборот
    |ИЗ
    |	РегистрНакопления.ВыручкаИСебестоимостьПродаж.Обороты КАК Продажи";

В строке Продажи.Партнер.БизнесРегион.Наименование мы видим двойное использование точки. Для нас это выглядит как простое обращение к свойству. Но давайте посмотрим, во что платформа 1С превращает этот код на языке SQL.

Каждая точка — это команда LEFT JOIN (Левое Соединение) с целой таблицей справочника. В данном случае:

  1. Сначала система присоединит таблицу справочника "Партнеры" к таблице регистра.
  2. Затем она присоединит таблицу справочника "БизнесРегионы" к таблице "Партнеры".

Если в выборке участвуют тысячи записей, а вы обращаетесь через точку к нескольким разным полям (например, Партнер.Код, Партнер.Наименование, Партнер.Менеджер.ФизЛицо.ДатаРождения), запрос превращается в монстра с десятками соединений, которые "убивают" оптимизатор СУБД.

Как поступить правильно?

Если нам нужны данные из связанных таблиц, лучше описать соединение явно. Это дает нам контроль над процессом и позволяет оптимизатору SQL построить более эффективный план запроса.


Запрос.Текст = 
    "ВЫБРАТЬ
    |	Продажи.Партнер,
    |	СпрРегионы.Наименование КАК Регион,
    |	Продажи.СуммаОборот
    |ИЗ
    |	РегистрНакопления.ВыручкаИСебестоимостьПродаж.Обороты КАК Продажи
    |		ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Партнеры КАК СпрПартнеры
    |		ПО Продажи.Партнер = СпрПартнеры.Ссылка
    |		ЛЕВОЕ СОЕДИНЕНИЕ Справочник.БизнесРегионы КАК СпрРегионы
    |		ПО СпрПартнеры.БизнесРегион = СпрРегионы.Ссылка";

Хотя код стал длиннее, он работает предсказуемо. Однако, если вам нужно получить только один реквизит (например, просто Код партнера) и вложенность небольшая (одна точка), использование разыменования допустимо. Но избегайте цепочек из 2-3 точек и более.

Использование логического ИЛИ в условиях

Разберем еще один нюанс, который часто упускают из виду. Это использование оператора ИЛИ в секции ГДЕ. Рассмотрим ситуацию, когда нам нужно найти документы, где контрагентом выступает либо "Компания А", либо "Компания Б".


Запрос.Текст = 
    "ВЫБРАТЬ
    |	Реализация.Ссылка
    |ИЗ
    |	Документ.РеализацияТоваровУслуг КАК Реализация
    |ГДЕ
    |	Реализация.Контрагент = &Контрагент1
    |	ИЛИ Реализация.Контрагент = &Контрагент2";

Проблема оператора ИЛИ заключается в том, что он часто блокирует использование индексов таблиц базы данных. СУБД не может эффективно использовать индекс по полю "Контрагент", когда видит альтернативное условие, и вынуждена сканировать таблицу целиком (Table Scan).

Для оптимизации таких запросов рекомендуется использовать конструкцию ОБЪЕДИНИТЬ ВСЕ. Мы разбиваем один сложный запрос на два простых, каждый из которых отлично отрабатывает по индексу, а затем склеиваем результаты.


Запрос.Текст = 
    "ВЫБРАТЬ
    |	Реализация.Ссылка
    |ИЗ
    |	Документ.РеализацияТоваровУслуг КАК Реализация
    |ГДЕ
    |	Реализация.Контрагент = &Контрагент1
    |
    |ОБЪЕДИНИТЬ ВСЕ
    |
    |ВЫБРАТЬ
    |	Реализация.Ссылка
    |ИЗ
    |	Документ.РеализацияТоваровУслуг КАК Реализация
    |ГДЕ
    |	Реализация.Контрагент = &Контрагент2";

Такой подход может показаться громоздким, но на больших объемах данных (сотни тысяч документов) разница в скорости может быть десятикратной.

Фильтрация по списку значений и Временные таблицы

Нередко возникает задача отобрать данные по большому списку номенклатуры или контрагентов, который мы подготовили в коде (например, массив из 1000 элементов).

Неопытный разработчик передаст этот список прямо в параметр:

ГДЕ Товар В (&СписокТоваров)

Если список маленький (10-20 элементов), это нормально. Но если элементов сотни или тысячи, это создаст огромную нагрузку. Правильным решением будет использование Временных таблиц (Менеджер временных таблиц).

Давайте разберем алгоритм действий:

  1. Создаем объект МенеджерВременныхТаблиц.
  2. Передаем наш список значений (массив) в запрос и помещаем его во временную таблицу.
  3. В основном запросе делаем ВНУТРЕННЕЕ СОЕДИНЕНИЕ с этой временной таблицей.

Пример реализации:


// 1. Подготовка списка
МассивТоваров = Новый Массив;
// ... заполнение массива ...

// 2. Помещение во временную таблицу
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	&Товар КАК Ссылка
    |ПОМЕСТИТЬ ВТ_Товары
    |ИЗ
    |	&ТЗТоваров КАК ТЗТоваров"; // Здесь мы передаем таблицу значений или массив как внешний источник

// Обязательно типизируем колонку, если передаем ТЗ, для массива платформа сделает это сама
Запрос.УстановитьПараметр("ТЗТоваров", МассивТоваров); 
Запрос.Выполнить();

// 3. Основной запрос с соединением
Запрос.Текст = 
    "ВЫБРАТЬ
    |	Остатки.Номенклатура,
    |	Остатки.КоличествоОстаток
    |ИЗ
    |	РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
    |		ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Товары КАК Фильтр
    |		ПО Остатки.Номенклатура = Фильтр.Ссылка";
    
Результат = Запрос.Выполнить();

Использование ВНУТРЕННЕЕ СОЕДИНЕНИЕ с временной таблицей работает как жесткий фильтр. СУБД оптимизирует этот процесс гораздо лучше, чем оператор В (...) с огромным списком параметров.

Получение данных через "Ссылку" в цикле

Хотя эта статья посвящена языку запросов, нельзя не упомянуть ошибку, которая сводит на нет любую оптимизацию запроса. Это обращение к реквизитам полученной выборки через точку внутри цикла обхода результата.

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


Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    // ГРУБАЯ ОШИБКА ПРОИЗВОДИТЕЛЬНОСТИ:
    Сообщить(Выборка.Номенклатура.Артикул); 
КонецЦикла;

В строке Выборка.Номенклатура.Артикул происходит следующее: платформа видит, что в выборке есть только ссылка, а вы просите артикул. Она делает скрытый запрос к базе данных, чтобы прочитать объект целиком (или нужные поля). Если в выборке 10 000 строк, вы сделаете 10 000 дополнительных микро-запросов к серверу базы данных.

Решение: Всегда выбирайте все необходимые поля сразу в тексте запроса.


// Правильный запрос
Запрос.Текст = "ВЫБРАТЬ Номенклатура, Номенклатура.Артикул КАК Артикул ...";
// ...
Пока Выборка.Следующий() Цикл
    Сообщить(Выборка.Артикул); // Данные уже выбраны, обращения к БД не происходит
КонецЦикла;

Подведем итоги

Для того чтобы ваши решения на платформе 1С:Предприятие работали быстро и стабильно, следуйте чек-листу при написании запросов:

  1. Используйте параметры виртуальных таблиц вместо секции ГДЕ для фильтрации регистров.
  2. Избегайте множественного разыменования полей (точек), заменяйте их явными соединениями (JOIN).
  3. Заменяйте условия с ИЛИ на ОБЪЕДИНИТЬ ВСЕ, если это возможно.
  4. При отборе по большому списку используйте Временные таблицы и соединение с ними.
  5. Выбирайте все нужные поля сразу в запросе, чтобы не обращаться к ним через точку при обходе выборки.

Соблюдение этих простых правил позволит вам избежать 90% проблем с производительностью и сделает работу пользователей комфортной.

← На главную