При разработке алгоритмов обработки документов программисты часто сталкиваются с ситуацией, когда необходимо выбрать шапку документа и строки его табличной части. В языке запросов 1С для этого существует синтаксис вложенных таблиц. Однако у многих начинающих и даже опытных разработчиков возникает вопрос: не является ли последующий вызов метода Выбрать() во вложенном цикле классическим «запросом в цикле», который создаёт высокую нагрузку на СУБД?
Рассмотрим подробно, как платформа 1С выполняет подобные запросы на физическом уровне, происходит ли обращение к базе данных при обходе вложенной выборки и какие существуют альтернативные способы решения этой задачи.
Проанализируем типичный пример кода, вызывающий споры:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЗаказКлиента.Ссылка КАК Ссылка,
| ЗаказКлиента.Номер КАК Номер,
| ЗаказКлиента.Дата КАК Дата,
| ЗаказКлиента.Товары.(
| НомерСтроки КАК НомерСтроки,
| Номенклатура КАК Номенклатура,
| Количество КАК Количество
| ) КАК Товары
|ИЗ
| Документ.ЗаказКлиента КАК ЗаказКлиента";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить("Документ № " + Выборка.Номер + " от " + Выборка.Дата);
ВыборкаТовары = Выборка.Товары.Выбрать();
Пока ВыборкаТовары.Следующий() Цикл
Сообщить(" -- Товар: " + ВыборкаТовары.Номенклатура + ", Количество: " + ВыборкаТовары.Количество);
КонецЦикла;
КонецЦикла;
На первый взгляд кажется, что строка Выборка.Товары.Выбрать() выполняется на каждом шаге цикла и обращается к СУБД для получения строк текущего документа. Выясним, так ли это на самом деле.
Если проанализировать технологический журнал 1С или профайлер SQL Server при выполнении данного кода, мы увидим, что платформа 1С не делает запросов к базе данных внутри цикла. Весь запрос выполняется единовременно при вызове метода Запрос.Выполнить().
На стороне СУБД платформа генерирует пакет из нескольких инструкций (или единый SQL-запрос с временными таблицами):
#tt1).INNER JOIN) таблицы строк табличной части _Document34_VT343 с созданной временной таблицей #tt1 по полю ссылки на документ.Пример транслируемого SQL-кода:
INSERT INTO #tt1 WITH(TABLOCK) (_C_1RRef, _C_2, _C_3, _TTC_1)
SELECT
T1._IDRRef,
T1._Number,
T1._Date_Time,
T1._IDRRef
FROM dbo._Document34 T1;
SELECT
T3._LineNo344,
T3._Fld345RRef,
T3._Fld348,
T4.SDBL_IDENTITY AS SDBL_IDENTITY
FROM dbo._Document34_VT343 T3
INNER JOIN #tt1 T4 WITH(NOLOCK) ON T4._TTC_1 = T3._Document34_IDRRef
ORDER BY 4 ASC;
Все полученные данные шапок и табличных частей считываются из СУБД за один раз и помещаются в оперативную память (RAM) сервера приложений 1С. Объект Выборка.Товары уже содержит готовую коллекцию данных (тип РезультатЗапроса). Метод Выбрать() лишь создаёт итератор для обхода этой коллекции в оперативной памяти, не отправляя никаких сетевых пакетов к SQL-серверу.
Несмотря на то, что сама вложенная выборка не создаёт запросов в цикле, при таком подходе легко допустить другие ошибки производительности:
1. Неявные запросы через обращение "через точку"
Если внутри внутреннего цикла написать ВыборкаТовары.Номенклатура.Код или использовать получение представления ссылки, платформа сделает отдельный неявный SQL-запрос к СУБД для каждого элемента. Чтобы этого избежать, нужно сразу выбирать все необходимые свойства справочников в тексте основного запроса (например, Товары.Номенклатура.Код КАК Код).
2. Высокое потребление оперативной памяти
Поскольку платформа считывает таблицы всех выбранных документов и их табличных частей одновременно, при большой выборке (тысячи документов) оперативка сервера приложений может перегрузиться. Вложенные таблицы не подходят для выгрузки масштабных объёмов данных.
3. Отсутствие гибкости сортировки и группировки
Внутри конструкции вложенной таблицы Товары.(...) нельзя использовать секции СГРУППИРОВАТЬ ПО и УПОРЯДОЧИТЬ ПО. Порядок строк всегда соответствует физическому порядку строк табличной части (НомерСтроки).
Рассмотрим, как правильно проектировать выборки данных, если нужно обработать несколько табличных частей или большой массив документов.
Для создания древовидной структуры данных сгруппируем результаты с помощью секции ИТОГИ ПО. Запрос выполняется непосредственно к табличной части документа:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Товары.Ссылка КАК Ссылка,
| Товары.Ссылка.Номер КАК Номер,
| Товары.Ссылка.Дата КАК Дата,
| Товары.Номенклатура КАК Номенклатура,
| Товары.Количество КАК Количество
|ИЗ
| Документ.ЗаказКлиента.Товары КАК Товары
|ИТОГИ ПО
| Ссылка";
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДокументов = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаДокументов.Следующий() Цикл
Сообщить("Документ: " + ВыборкаДокументов.Номер);
ВыборкаСтрок = ВыборкаДокументов.Выбрать();
Пока ВыборкаСтрок.Следующий() Цикл
Сообщить(" -- Номенклатура: " + ВыборкаСтрок.Номенклатура);
КонецЦикла;
КонецЦикла;
Этот подход позволяет использовать агрегатные функции (СУММА, КОЛИЧЕСТВО), накладывать произвольную сортировку и эффективнее работать с памятью.
Если у документа необходимо обработать сразу несколько табличных частей (например, Товары, Услуги, Скидки), правильным решением будет использование пакета запросов с временной таблицей:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Заказ.Ссылка КАК Ссылка,
| Заказ.Номер КАК Номер,
| Заказ.Дата КАК Дата
|ПОМЕСТИТЬ ВТ_Документы
|ИЗ
| Документ.ЗаказКлиента КАК Заказ
|ГДЕ
| Заказ.Проведен = ИСТИНА
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Документы.Ссылка КАК Ссылка,
| Документы.Номер КАК Номер,
| Документы.Дата КАК Дата
|ИЗ
| ВТ_Документы КАК Документы
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Товары.Ссылка КАК Ссылка,
| Товары.Номенклатура КАК Номенклатура,
| Товары.Количество КАК Количество
|ИЗ
| Документ.ЗаказКлиента.Товары КАК Товары
|ГДЕ
| Товары.Ссылка В (ВЫБРАТЬ ВТ.Ссылка ИЗ ВТ_Документы КАК ВТ)
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Услуги.Ссылка КАК Ссылка,
| Услуги.Услуга КАК Услуга,
| Услуги.Сумма КАК Сумма
|ИЗ
| Документ.ЗаказКлиента.Услуги КАК Услуги
|ГДЕ
| Услуги.Ссылка В (ВЫБРАТЬ ВТ.Ссылка ИЗ ВТ_Документы КАК ВТ)";
МассивРезультатов = Запрос.ВыполнитьПакет();
// Таблица шапок
ТаблицаШапки = МассивРезультатов[1].Выгрузить();
// Таблица товаров
ТаблицаТовары = МассивРезультатов[2].Выгрузить();
// Таблица услуг
ТаблицаУслуги = МассивРезультатов[3].Выгрузить();
Разделение на пакет запросов гарантирует минимальную нагрузку на СУБД, оптимальное использование индексирования во временных таблицах и независимую обработку каждой табличной части в коде 1С.