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