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