Как в 1С оптимизировать получение данных, избавившись от запроса в цикле?

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

В разработке на платформе 1С часто возникает задача получить связанные данные из разных таблиц. Одна из распространенных ошибок, особенно у начинающих программистов, — это выполнение запросов внутри цикла. Такой подход создает избыточную нагрузку на сервер и значительно замедляет работу системы. Рассмотрим на конкретном примере, как правильно оптимизировать такой код, объединив несколько запросов в один.

Анализ исходной проблемы: "Запрос в цикле"

Проанализируем исходную задачу. Требуется для каждого склада получить учетные данные (логин и пароль) из регистра сведений, найти все оплаченные по этому складу заказы клиентов и отправить данные по ним через API. Оплаченным считается заказ, на основании которого создан проведенный документ ЧекККМ.

Первоначальный код решает эту задачу "в лоб":

  1. Первый запрос получает уникальный список складов и их учетных данных.
  2. Далее, в цикле по результату этого запроса, для каждого склада выполняется второй запрос, который ищет оплаченные заказы.

Посмотрим на этот код:


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

Главный недостаток: если в базе 100 складов, будет выполнен 101 запрос к базе данных. Это крайне неэффективно. Правильный путь — получить все необходимые данные за одно обращение к СУБД.

Решение: Объединение запросов с помощью СОЕДИНЕНИЙ и группировка через ИТОГИ

Чтобы уйти от цикла, необходимо объединить логику двух запросов в один. Мы можем за один раз выбрать все оплаченные заказы и сразу же присоединить к ним данные для API (логин и пароль) из регистра сведений. Связующим звеном будет поле Склад и КаналПродаж.

Итоговый запрос будет выглядеть так:


ВЫБРАТЬ РАЗЛИЧНЫЕ
	ЗаказКлиента.Ссылка КАК Ссылка,
	ЗаказКлиента.Склад КАК Склад,
	ЧекККМТовары.Ссылка КАК ЧекККМСсылка,
	API.Логин КАК Логин,
	API.Пароль КАК Пароль,
	ЗаказКлиента.НомерПоДаннымКлиента КАК НомерПоДаннымКлиента
ИЗ
	Документ.ЗаказКлиента КАК ЗаказКлиента
		// 1. Находим оплаченные заказы, соединяя с табличной частью ЧекаККМ
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ЧекККМ.Товары КАК ЧекККМТовары
		ПО (ЗаказКлиента.Ссылка = ЧекККМТовары.ЗаказКлиента) // В данной конфигурации связь в ТЧ
			И (ЧекККМТовары.Ссылка.Проведен)
		// 2. Присоединяем данные для API по складу и каналу продаж
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.API КАК API
		ПО (ЗаказКлиента.Склад = API.Склад)
			И (ЗаказКлиента.КаналПродаж = API.КаналПродаж)
ГДЕ
	ЗаказКлиента.ДополнительныеРеквизиты.Свойство = &Свойство
	И ЗаказКлиента.ДополнительныеРеквизиты.Значение = &Значение
	И ЗаказКлиента.Проведен
	И ЗаказКлиента.КаналПродаж = &КаналПродаж

УПОРЯДОЧИТЬ ПО
	ЗаказКлиента.Дата

ИТОГИ
	МИНИМУМ(Логин),
	МИНИМУМ(Пароль),
	КОЛИЧЕСТВО(РАЗЛИЧНЫЕ НомерПоДаннымКлиента)
ПО
	Склад

Разберем ключевые моменты этого запроса:

Альтернативные подходы и дополнительные улучшения

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

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

Для сложных, многоступенчатых запросов бывает полезно разбить логику на несколько шагов с помощью временных таблиц. Это улучшает читаемость и может помочь оптимизатору СУБД построить более эффективный план выполнения. Сначала можно отобрать оплаченные заказы в ВТ, а затем соединить ее с регистром API.

Посмотрим на пример такого запроса:


ВЫБРАТЬ
	ЗаказКлиента.Ссылка КАК ЗаказКлиента,
	ЗаказКлиента.Склад КАК Склад,
	ЗаказКлиента.КаналПродаж КАК КаналПродаж,
	МАКСИМУМ(ДокументЧекККМ.Ссылка) КАК ЧекККМ
ПОМЕСТИТЬ ОплаченныеЗаказыКлиентов
ИЗ
	Документ.ЗаказКлиента.ДополнительныеРеквизиты КАК ЗаказКлиентаДополнительныеРеквизиты
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ЗаказКлиента КАК ДокументЗаказКлиента
		ПО (ЗаказКлиентаДополнительныеРеквизиты.Ссылка = ДокументЗаказКлиента.Ссылка)
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ЧекККМ.Товары КАК ЧекККМТовары
		ПО (ДокументЗаказКлиента.Ссылка = ЧекККМТовары.ЗаказКлиента)
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ЧекККМ КАК ДокументЧекККМ
		ПО (ЧекККМТовары.Ссылка = ДокументЧекККМ.Ссылка)
ГДЕ
	ЗаказКлиентаДополнительныеРеквизиты.Свойство = &Свойство
	И ЗаказКлиентаДополнительныеРеквизиты.Значение = &Значение
	И ДокументЧекККМ.Проведен
СГРУППИРОВАТЬ ПО
	ДокументЗаказКлиента.Ссылка,
	ДокументЗаказКлиента.Склад,
	ДокументЗаказКлиента.КаналПродаж
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	ОплаченныеЗаказыКлиентов.ЗаказКлиента,
	ОплаченныеЗаказыКлиентов.Склад,
	API.Логин,
	API.Пароль
ИЗ
	ОплаченныеЗаказыКлиентов КАК ОплаченныеЗаказыКлиентов
		ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.API КАК API
		ПО ОплаченныеЗаказыКлиентов.Склад = API.Склад
			И ОплаченныеЗаказыКлиентов.КаналПродаж = API.КаналПродаж

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

2. Обработка группировок на стороне 1С (без ИТОГОВ)

Вместо использования ИТОГИ в запросе, можно получить "плоскую" таблицу данных и сгруппировать ее уже в коде 1С. Этот метод дает больше гибкости и часто делает код обработки результата более наглядным.

Алгоритм действий:

  1. Из запроса убираем секцию ИТОГИ.
  2. Обязательно добавляем УПОРЯДОЧИТЬ ПО Склад. Сортировка — ключ к этому методу.
  3. В коде обходим результат и отслеживаем смену значения в поле Склад.

Посмотрим на пример кода:


// Запрос выполняется без секции ИТОГИ, но с УПОРЯДОЧИТЬ ПО Склад
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();

ТекущийСклад = Неопределено;
Токен = "";
ДанныеДляОтправки = Новый Массив;

Пока Выборка.Следующий() Цикл
    
    // Проверяем, изменился ли склад. Если да - это новая группа.
    Если ТекущийСклад <> Выборка.Склад Тогда
        
        // Если это не первая итерация, отправляем данные по предыдущему складу
        Если ТекущийСклад <> Неопределено Тогда
            ОтправитьДанныеПоAPI(Токен, ДанныеДляОтправки);
        КонецЕсли;
        
        // Начинаем обработку новой группы
        ТекущийСклад = Выборка.Склад;
        ДанныеДляОтправки.Очистить();
        
        // Получаем токен для нового склада один раз
        Токен = ПолучитьТокен(Выборка.Логин, Выборка.Пароль);
    КонецЕсли;
    
    // Накапливаем данные для текущего склада
    ДанныеДляОтправки.Добавить(Выборка.НомерПоДаннымКлиента);
    
КонецЦикла;

// ВАЖНО: не забываем отправить данные для последней группы после завершения цикла
Если ДанныеДляОтправки.Количество() > 0 Тогда
    ОтправитьДанныеПоAPI(Токен, ДанныеДляОтправки);
КонецЕсли;

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

← На главную