Как оптимизировать сложный запрос 1С с `ОБЪЕДИНИТЬ ВСЕ` и ускорить обработку его результата?

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

Часто в работе программиста 1С возникают задачи, требующие сбора данных из нескольких источников или разных полей одной таблицы. Это приводит к созданию громоздких запросов с множеством объединений и вложенных конструкций. Такие запросы могут выполняться медленно, а последующая обработка их результатов в цикле еще больше снижает производительность. Проанализируем типичный пример такого запроса и разберем по шагам, как его можно значительно ускорить.

Исходная задача — собрать данные о дополнительных расходах по импортным поступлениям, включая таможенные пошлины, НДС, сборы из документа `ГТДИмпорт` и прочие расходы из документа `ПоступлениеДопРасходовИмпорт`. Затем эти суммы нужно скорректировать пропорционально количеству товара из документа-основания.

Рассмотрим исходный код, который требует оптимизации.


// Исходный запрос
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|	ПоступлениеДопРасходов.Содержание КАК Содержание,
|	ПоступлениеДопРасходов.Номенклатура КАК Номенклатура,
|	СУММА(ПоступлениеДопРасходов.Сумма) КАК Сумма,
|	МАКСИМУМ(ЗаказПоставщикуОтгруженный.Количество) КАК Количество,
|	ПоступлениеДопРасходов.Валюта КАК Валюта,
|	ПоступлениеДопРасходов.Курс КАК Курс,
|	ПоступлениеДопРасходов.Контрагент КАК Контрагент,
|	ПоступлениеДопРасходов.ДоговорВзаиморасчетов КАК Договор
|ИЗ
|	(ВЫБРАТЬ
|		""ГТД: Таможенная пошлина"" КАК Содержание,
|		ГТДИмпортТовары.Номенклатура КАК Номенклатура,
|		// ... прочие поля
|		ГТДИмпортТовары.СуммаПошлины КАК Сумма
|	ИЗ
|		Документ.ГТДИмпорт.Товары КАК ГТДИмportТовары
|	ГДЕ
|		ГТДИмпортТовары.Ссылка.ДокументОснование = &Основание
|		И ГТДИмпортТовары.Ссылка.Проведен
|		И ГТДИмпортТовары.Номенклатура В(&МассивНоменклатуры)
|	
|	ОБЪЕДИНИТЬ ВСЕ
|	
|	ВЫБРАТЬ
|		""ГТД: НДС"",
|		ГТДИмпортТовары.Номенклатура,
|		// ... прочие поля
|		ГТДИмпортТовары.СуммаНДС
|	ИЗ
|		Документ.ГТДИмпорт.Товары КАК ГТДИмportТовары
|	ГДЕ
|		// ... те же условия
|	
|	ОБЪЕДИНИТЬ ВСЕ
|	
|	ВЫБРАТЬ
|		""ГТД: Таможенный сбор"",
|		ГТДИмпортТовары.Номенклатура,
|		// ... прочие поля
|		ГТДИмпортТовары.СуммаТаможня
|	ИЗ
|		Документ.ГТДИмпорт.Товары КАК ГТДИмportТовары
|	ГДЕ
|		// ... те же условия
|	
|	ОБЪЕДИНИТЬ ВСЕ
|	
|	ВЫБРАТЬ
|		""ДР: "" + ПоступлениеДопРасходовИмпортТовары.Ссылка.Содержание,
|		ПоступлениеДопРасходовИмпортТовары.Номенклатура,
|		// ... прочие поля
|	ИЗ
|		Документ.ПоступлениеДопРасходовИмпорт.Товары КАК ПоступлениеДопРасходовИмпортТовары
|	ГДЕ
|		// ... похожие условия
|		) КАК ПоступлениеДопРасходов
|		ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
|			ЗаказПоставщикуОтгруженныйТовары.Номенклатура КАК Номенклатура,
|			СУММА(ЗаказПоставщикуОтгруженныйТовары.Количество) КАК Количество
|		ИЗ
|			Документ.ЗаказПоставщикуОтгруженный.Товары КАК ЗаказПоставщикуОтгруженныйТовары
|		ГДЕ
|			ЗаказПоставщикуОтгруженныйТовары.Ссылка = &Основание
|		
|		СГРУППИРОВАТЬ ПО
|			ЗаказПоставщикуОтгруженныйТовары.Номенклатура) КАК ЗаказПоставщикуОтгруженный
|		ПО ПоступлениеДопРасходов.Номенклатура = ЗаказПоставщикуОтгруженный.Номенклатура
|
|СГРУППИРОВАТЬ ПО
|	// ... поля группировки";

// ... установка параметров

// Исходный код обработки
Пока Выборка.Следующий() Цикл
    // ...
    нс = ТЗ_Товары.Найти(Выборка.Номенклатура, "Номенклатура"); // Медленный поиск
    КолТовар = нс.Количество;
    
    Если КолТовар = Выборка.Количество Тогда
        НоваяСтрокаТЧ.Сумма = Выборка.Сумма;
    Иначе
        НоваяСтрокаТЧ.Сумма = Окр(Выборка.Сумма / Выборка.Количество * КолТовар, 2, 1);
    КонецЕсли;

    // Медленные вызовы в цикле
    НоваяСтрокаТЧ.СуммаУпр	= _обПересчет(..., Константы.ВалютаУправленческогоУчетаКомпании.Получить(), ..., ТекущаяДата()));
КонецЦикла;

Проанализируем основные узкие места этого кода и предложим варианты улучшения.

Этап 1: Оптимизация текста запроса

Главная проблема исходного запроса — многократное обращение к одной и той же таблице Документ.ГТДИмпорт.Товары с одинаковыми условиями отбора. Система вынуждена трижды сканировать таблицу, что неэффективно. Кроме того, использование вложенных подзапросов в ИЗ и СОЕДИНЕНИЕ затрудняет чтение и может мешать оптимизатору СУБД строить оптимальный план выполнения.

Решение: Использование временных таблиц (ВТ).

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

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

Посмотрим, как будет выглядеть оптимизированный запрос.


Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|	ГТДИмпортТовары.Номенклатура КАК Номенклатура,
|	ГТДИмпортТовары.Количество КАК Количество,
|	ГТДИмпортТовары.СуммаПошлины,
|	ГТДИмпортТовары.СуммаНДС,
|	ГТДИмпортТовары.СуммаТаможня,
|	ГТДИмпортТовары.Ссылка.ДоговорВзаиморасчетов.ВалютаВзаиморасчетов КАК Валюта,
|	ГТДИмпортТовары.Ссылка.КурсВзаиморасчетов КАК Курс,
|	ГТДИмпортТовары.Ссылка.Контрагент КАК Контрагент,
|	ГТДИмпортТовары.Ссылка.ДоговорВзаиморасчетов КАК ДоговорВзаиморасчетов
|ПОМЕСТИТЬ ВТ_ДанныеГТД
|ИЗ
|	Документ.ГТДИмпорт.Товары КАК ГТДИмпортТовары
|ГДЕ
|	ГТДИмпортТовары.Ссылка.ДокументОснование = &Основание
|	И ГТДИмпортТовары.Ссылка.Проведен
|	И ГТДИмпортТовары.Номенклатура В(&МассивНоменклатуры)
|;
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	ПоступлениеДопРасходовИмпортТовары.Номенклатура КАК Номенклатура,
|	""ДР: "" + ПоступлениеДопРасходовИмпортТовары.Ссылка.Содержание КАК Содержание,
|	ПоступлениеДопРасходовИмпортТовары.Количество КАК Количество,
|	ПоступлениеДопРасходовИмпортТовары.Сумма КАК Сумма,
|	ПоступлениеДопРасходовИмпортТовары.Ссылка.ВалютаДокумента КАК Валюта,
|	ПоступлениеДопРасходовИмпортТовары.Ссылка.КурсДокумента КАК Курс,
|	ПоступлениеДопРасходовИмпортТовары.Ссылка.Контрагент КАК Контрагент,
|	ПоступлениеДопРасходовИмпортТовары.Ссылка.ДоговорВзаиморасчетов КАК ДоговорВзаиморасчетов
|ПОМЕСТИТЬ ВТ_ДанныеДопРасходов
|ИЗ
|	Документ.ПоступлениеДопРасходовИмпорт.Товары КАК ПоступлениеДопРасходовИмпортТовары
|ГДЕ
|	ПоступлениеДопРасходовИмпортТовары.Ссылка.ДокументОснование = &Основание
|	И ПоступлениеДопРасходовИмпортТовары.Ссылка.Проведен
|	И ПоступлениеДопРасходовИмпортТовары.Номенклатура В(&МассивНоменклатуры)
|;
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	ЗаказПоставщикуОтгруженныйТовары.Номенклатура КАК Номенклатура,
|	СУММА(ЗаказПоставщикуОтгруженныйТовары.Количество) КАК Количество
|ПОМЕСТИТЬ ВТ_КоличествоОснование
|ИЗ
|	Документ.ЗаказПоставщикуОтгруженный.Товары КАК ЗаказПоставщикуОтгруженныйТовары
|ГДЕ
|	ЗаказПоставщикуОтгруженныйТовары.Ссылка = &Основание
|СГРУППИРОВАТЬ ПО
|	ЗаказПоставщикуОтгруженныйТовары.Номенклатура
|;
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	ВложенныйЗапрос.Содержание,
|	ВложенныйЗапрос.Номенклатура,
|	ВложенныйЗапрос.Валюта,
|	ВложенныйЗапрос.Курс,
|	ВложенныйЗапрос.Контрагент,
|	ВложенныйЗапрос.ДоговорВзаиморасчетов КАК Договор,
|	СУММА(ВложенныйЗапрос.Сумма) КАК Сумма,
|	МАКСИМУМ(ВТ_КоличествоОснование.Количество) КАК Количество
|ИЗ
|	(ВЫБРАТЬ
|		""ГТД: Таможенная пошлина"" КАК Содержание,
|		ВТ_ДанныеГТД.Номенклатура КАК Номенклатура,
|		ВТ_ДанныеГТД.СуммаПошлины КАК Сумма,
|		ВТ_ДанныеГТД.Валюта КАК Валюта,
|		ВТ_ДанныеГТД.Курс КАК Курс,
|		ВТ_ДанныеГТД.Контрагент КАК Контрагент,
|		ВТ_ДанныеГТД.ДоговорВзаиморасчетов КАК ДоговорВзаиморасчетов
|	ИЗ
|		ВТ_ДанныеГТД
|	
|	ОБЪЕДИНИТЬ ВСЕ
|	
|	ВЫБРАТЬ
|		""ГТД: НДС"",
|		ВТ_ДанныеГТД.Номенклатура,
|		ВТ_ДанныеГТД.СуммаНДС,
|		ВТ_ДанныеГТД.Валюта,
|		ВТ_ДанныеГТД.Курс,
|		ВТ_ДанныеГТД.Контрагент,
|		ВТ_ДанныеГТД.ДоговорВзаиморасчетов
|	ИЗ
|		ВТ_ДанныеГТД
|	
|	ОБЪЕДИНИТЬ ВСЕ
|	
|	ВЫБРАТЬ
|		""ГТД: Таможенный сбор"",
|		ВТ_ДанныеГТД.Номенклатура,
|		ВТ_ДанныеГТД.СуммаТаможня,
|		ВТ_ДанныеГТД.Валюта,
|		ВТ_ДанныеГТД.Курс,
|		ВТ_ДанныеГТД.Контрагент,
|		ВТ_ДанныеГТД.ДоговорВзаиморасчетов
|	ИЗ
|		ВТ_ДанныеГТД
|	
|	ОБЪЕДИНИТЬ ВСЕ
|	
|	ВЫБРАТЬ
|		ВТ_ДанныеДопРасходов.Содержание,
|		ВТ_ДанныеДопРасходов.Номенклатура,
|		ВТ_ДанныеДопРасходов.Сумма,
|		ВТ_ДанныеДопРасходов.Валюта,
|		ВТ_ДанныеДопРасходов.Курс,
|		ВТ_ДанныеДопРасходов.Контрагент,
|		ВТ_ДанныеДопРасходов.ДоговорВзаиморасчетов
|	ИЗ
|		ВТ_ДанныеДопРасходов) КАК ВложенныйЗапрос
|		ЛЕВОЕ СОЕДИНЕНИЕ ВТ_КоличествоОснование КАК ВТ_КоличествоОснование
|		ПО ВложенныйЗапрос.Номенклатура = ВТ_КоличествоОснование.Номенклатура
|
|СГРУППИРОВАТЬ ПО
|	ВложенныйЗапрос.Содержание,
|	ВложенныйЗапрос.Номенклатура,
|	ВложенныйЗапрос.Валюта,
|	ВложенныйЗапрос.Курс,
|	ВложенныйЗапрос.Контрагент,
|	ВложенныйЗапрос.ДоговорВзаиморасчетов";

Дополнительное улучшение: избегайте обращений к реквизитам через точку в условиях соединения и отбора (например, ГТДИмпортТовары.Ссылка.ДоговорВзаиморасчетов.ВалютаВзаиморасчетов). Лучше использовать явное ЛЕВОЕ СОЕДИНЕНИЕ с нужными таблицами (документами, справочниками). Это дает оптимизатору запросов больше информации для построения эффективного плана.

Этап 2: Оптимизация кода обработки результата

Даже с быстрым запросом общая производительность может страдать из-за неэффективной обработки его результата. Рассмотрим проблемы в цикле обхода выборки.

Проблема 1: Медленный поиск в таблице значений.

Метод ТЗ_Товары.Найти(Выборка.Номенклатура, "Номенклатура") выполняет последовательный перебор строк таблицы `ТЗ_Товары` на каждой итерации цикла. Если в выборке и в таблице много строк, это становится очень медленной операцией.

Решение: Индексация ТаблицыЗначений.

Перед циклом необходимо добавить индекс по колонке, по которой будет производиться поиск. Это изменит алгоритм поиска на гораздо более быстрый.


ТЗ_Товары = ПоступлениеТоваровИмпорт.Товары.Выгрузить();
ТЗ_Товары.Свернуть("Номенклатура", "Количество");
// Добавляем индекс перед поиском!
ТЗ_Товары.Индексы.Добавить("Номенклатура");

Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    // Теперь этот поиск будет работать очень быстро
    нс = ТЗ_Товары.Найти(Выборка.Номенклатура, "Номенклатура");
    // ...
КонецЦикла;

Проблема 2: Вызовы функций и получение данных с сервера внутри цикла.

В строке НоваяСтрокаТЧ.СуммаУпр = _обПересчет(..., Константы.ВалютаУправленческогоУчетаКомпании.Получить(), ..., ТекущаяДата()) на каждой итерации происходят:

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

Решение: Вынести получение константных данных за пределы цикла.

Все значения, которые не изменяются от итерации к итерации, нужно получить один раз перед циклом и сохранить в переменные.


// Получаем значения ОДИН РАЗ перед циклом
ВалютаУпрУчета = Константы.ВалютаУправленческогоУчетаКомпании.Получить();
ДатаДляПересчета = ?(ЗначениеЗаполнено(ПоступлениеТоваровИмпорт.Дата), ПоступлениеТоваровИмпорт.Дата, ТекущаяДата());

Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    // ...
    НоваяСтрокаТЧ.СуммаУпр	= _обПересчет(НоваяСтрокаТЧ.Сумма, НоваяСтрокаТЧ.Валюта, НоваяСтрокаТЧ.Курс, ВалютаУпрУчета, ДатаДляПересчета);
КонецЦикла;

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

Расчет пропорциональной суммы (Окр(Выборка.Сумма / Выборка.Количество * КолТовар, 2, 1)) выполняется на стороне клиента. В некоторых случаях такие вычисления можно перенести в сам запрос, чтобы они выполнялись на сервере СУБД, что обычно эффективнее.

Решение: Перенос логики в запрос с помощью `ВЫБОР КОГДА`.

Для этого нужно добавить в итоговый запрос соединение с таблицей ТЗ_Товары (которую также можно предварительно загрузить во временную таблицу) и выполнить расчет прямо в списке полей ВЫБРАТЬ.

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

Выводы

Для оптимизации производительности кода в 1С всегда анализируйте два аспекта: сам запрос и код обработки его результата.

  1. Оптимизация запроса:
    • Используйте временные таблицы, чтобы избежать повторных обращений к физическим таблицам и упростить сложные конструкции.
    • Создавайте индексы для временных таблиц по полям, участвующим в соединениях.
    • Отдавайте предпочтение явным ЛЕВОЕ СОЕДИНЕНИЕ вместо доступа к данным через точку.
  2. Оптимизация обработки:
    • Индексируйте таблицы значений перед поиском в цикле.
    • Выносите получение констант, параметров и вызов функций с неизменным результатом за пределы цикла.
    • По возможности, переносите вычисления с клиента на сервер, выполняя их средствами языка запросов.

Применение этих простых, но мощных техник позволит значительно ускорить выполнение кода и улучшить отзывчивость системы.

← На главную