Типичная задача при интеграции, экспорте данных в XML или JSON — выгрузить список документов за указанный период вместе со строками их табличных частей. Начинающие разработчики нередко совершают классическую ошибку: сначала выбирают список документов, а затем в цикле обхода выполняют отдельный запрос к табличной части каждого найденного документа.
Такой подход, называемый запросом в цикле, критически снижает производительность системы: если документов тысячи, платформа выполняет тысячи обращений к СУБД, создавая огромную сетевую и вычислительную нагрузку. Разберем, как получить все данные за одно обращение к базе данных и грамотно обойти результат выборки.
Самый эффективный и каноничный способ платформы 1С:Предприятие — обращение к данным с группировкой по ссылке через секцию ИТОГИ. В этом случае мы получаем двухуровневую структуру: на верхнем уровне находятся документы (шапка), а на вложенном уровне — строки соответствующей табличной части.
Посмотрим на пример построения запроса и обхода выборки для формирования XML-файла:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Реализация.Ссылка КАК ДокументСсылка,
| Реализация.Номер КАК Номер,
| Реализация.Дата КАК Дата,
| РеализацияТовары.Номенклатура.Код КАК КодТовара,
| РеализацияТовары.Количество КАК Количество,
| РеализацияТовары.Цена КАК Цена,
| РеализацияТовары.Сумма КАК Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
| ЛЕВОЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТовары
| ПО (РеализацияТовары.Ссылка = Реализация.Ссылка)
|ГДЕ
| Реализация.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
|ИТОГИ
| МАКСИМУМ(Номер),
| МАКСИМУМ(Дата)
|ПО
| ДокументСсылка";
Запрос.УстановитьПараметр("ДатаНачала", ДатаНачала);
Запрос.УстановитьПараметр("ДатаОкончания", ДатаОкончания);
РезультатЗапроса = Запрос.Выполнить();
// Начинаем формирование XML
ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.ОткрытьФайл(ИмяФайлаВыгрузки);
ЗаписьXML.ЗаписатьНачалоЭлемента("Docs");
// Получаем выборку верхнего уровня (документы)
ВыборкаДокументы = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаДокументы.Следующий() Цикл
ЗаписьXML.ЗаписатьНачалоЭлемента("Doc");
ЗаписьXML.ЗаписатьАтрибут("num", Строка(ВыборкаДокументы.Номер));
ЗаписьXML.ЗаписатьАтрибут("date", XMLСтрока(ВыборкаДокументы.Дата));
ЗаписьXML.ЗаписатьНачалоЭлемента("Table");
// Получаем подчиненную выборку детальных записей (строки ТЧ текущего документа)
ВыборкаСтроки = ВыборкаДокументы.Выбрать();
Пока ВыборкаСтроки.Следующий() Цикл
// Проверяем наличие строки (на случай, если ТЧ документа пуста)
Если ЗначениеЗаполнено(ВыборкаСтроки.КодТовара) Тогда
ЗаписьXML.ЗаписатьНачалоЭлемента("SKU");
ЗаписьXML.ЗаписатьЭлемент("code", Строка(ВыборкаСтроки.КодТовара));
ЗаписьXML.ЗаписатьЭлемент("quantity", XMLСтрока(ВыборкаСтроки.Количество));
ЗаписьXML.ЗаписатьЭлемент("price", XMLСтрока(ВыборкаСтроки.Цена));
ЗаписьXML.ЗаписатьЭлемент("sum", XMLСтрока(ВыборкаСтроки.Сумма));
ЗаписьXML.ЗаписатьКонецЭлемента(); // SKU
КонецЕсли;
КонецЦикла;
ЗаписьXML.ЗаписатьКонецЭлемента(); // Table
ЗаписьXML.ЗаписатьКонецЭлемента(); // Doc
КонецЦикла;
ЗаписьXML.ЗаписатьКонецЭлемента(); // Docs
ЗаписьXML.Закрыть();
Разберем по шагам ключевые особенности этого подхода:
ЛЕВОЕ СОЕДИНЕНИЕ: Базовой таблицей выступает таблица документов, а табличная часть присоединяется слева. Если делать запрос напрямую к таблице табличной части (Документ.РеализацияТоваровУслуг.Товары), то документы с пустой табличной частью не попадут в выборку вовсе.ИТОГИ ... ПО: Позволяет разделить данные на уровни иерархии. Для полей шапки документа (таких как Номер, Дата) в блоке итогов указывается агрегатная функция МАКСИМУМ, благодаря чему они становятся доступны прямо в выборке верхнего уровня ВыборкаДокументы.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам): Позволяет последовательно считывать данные без загрузки всего массива в оперативную память, что крайне важно при обработке сотен тысяч записей.Второй вариант — выборка детальных записей без секции ИТОГИ, но с обязательной сортировкой по ссылке документа. В процессе обхода плоского результата отслеживается момент, когда значение ссылки меняется, что означает переход к следующему документу.
Посмотрим на пример реализации данного алгоритма:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Реализация.Ссылка КАК ДокументСсылка,
| Реализация.Номер КАК Номер,
| Реализация.Дата КАК Дата,
| РеализацияТовары.Номенклатура.Код КАК КодТовара,
| РеализацияТовары.Количество КАК Количество
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
| ЛЕВОЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТовары
| ПО (РеализацияТовары.Ссылка = Реализация.Ссылка)
|ГДЕ
| Реализация.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
|УПОРЯДОЧИТЬ ПО
| ДокументСсылка,
| РеализацияТовары.НомерСтроки";
Запрос.УстановитьПараметр("ДатаНачала", ДатаНачала);
Запрос.УстановитьПараметр("ДатаОкончания", ДатаОкончания);
Выборка = Запрос.Выполнить().Выбрать();
ТекущийДокумент = Неопределено;
Пока Выборка.Следующий() Цикл
Если ТекущийДокумент <> Выборка.ДокументСсылка Тогда
Если ТекущийДокумент <> Неопределено Тогда
// Закрываем теги предыдущего документа
ЗаписьXML.ЗаписатьКонецЭлемента(); // Table
ЗаписьXML.ЗаписатьКонецЭлемента(); // Doc
КонецЕсли;
// Открываем теги нового документа
ТекущийДокумент = Выборка.ДокументСсылка;
ЗаписьXML.ЗаписатьНачалоЭлемента("Doc");
ЗаписьXML.ЗаписатьАтрибут("num", Строка(Выборка.Номер));
ЗаписьXML.ЗаписатьАтрибут("date", XMLСтрока(Выборка.Дата));
ЗаписьXML.ЗаписатьНачалоЭлемента("Table");
КонецЕсли;
// Выгружаем строку табличной части
Если ЗначениеЗаполнено(Выборка.КодТовара) Тогда
ЗаписьXML.ЗаписатьНачалоЭлемента("SKU");
ЗаписьXML.ЗаписатьЭлемент("code", Строка(Выборка.КодТовара));
ЗаписьXML.ЗаписатьЭлемент("quantity", XMLСтрока(Выборка.Количество));
ЗаписьXML.ЗаписатьКонецЭлемента(); // SKU
КонецЕсли;
КонецЦикла;
// Закрываем последний открытый документ
Если ТекущийДокумент <> Неопределено Тогда
ЗаписьXML.ЗаписатьКонецЭлемента(); // Table
ЗаписьXML.ЗаписатьКонецЭлемента(); // Doc
КонецЕсли;
Если структура шапки или табличной части содержит слишком много полей, плоское соединение может передавать избыточный объем повторяющихся данных шапки по сети от СУБД. В такой ситуации отлично подходит пакетный запрос через ВыполнитьПакет():
ВТ_Документы.ВТ_Документы и основной таблицы документов.ВТ_Документы.В результате работы метода ВыполнитьПакет() мы получаем массив результатов запросов, данные которых можно сопоставить в коде, избегая дублирования полей шапки в сетевом трафике.
Проанализируем ключевые моменты, которые необходимо учитывать при написании запросов к документам и табличным частям:
ТабличнаяЧасть.Ссылка.Склад или ТабличнаяЧасть.Номенклатура.Код заставляют платформу генерировать дополнительные LEFT JOIN к таблицам ссылок. Если полей много, соединяйте таблицы явно.Выгрузить() на больших объемах: Выгрузка в ТаблицаЗначений или ДеревоЗначений требует единовременного выделения памяти под всю выборку. Использование Выбрать() с последовательным методом Следующий() работает в потоковом режиме и потребляет минимум оперативной памяти.ВЫБРАТЬ не препятствует расчету итогов. Для вычисления суммовых показателей используйте СУММА(Поле) в секции ИТОГИ, а для строковых или ссылочных реквизитов шапки — агрегат МАКСИМУМ(Поле).