Нередко перед начинающими разработчиками встает типичная задача: получить из базы список документов и связанные с ними товарные позиции (или строки табличных частей), а затем последовательно обработать каждую строку в разрезе своего документа. Распространенной ошибкой является выгрузка всех данных в промежуточную таблицу значений, создание ее копии, сворачивание по ссылке на документ и последующий циклический поиск строк через метод НайтиСтроки().
Разберем, почему такая схема считается антипаттерном в платформе 1С:Предприятие, какие системные ресурсы она расходует и какими штатными механизмами платформы следует пользоваться для достижения максимальной производительности.
Проанализируем архитектурные проблемы алгоритма со сворачиванием таблицы и последующим отбором строк:
НайтиСтроки() для обычной таблицы значений без явных индексов, платформа производит полный линейный перебор всех записей. Если в таблице 10 000 строк и 500 документов, система сделает до 5 000 000 сравнений. При росте базы это приводит к зависанию обработки.Скопировать() и Свернуть() дублирует массивы данных в оперативной памяти сервера. Вся таблица принудительно материализуется целиком, что при больших выборках может исчерпать доступную память рабочего процесса.Выгрузить() заставляет СУБД отдать абсолютно все записи сразу, лишая нас преимуществ последовательного курсорного чтения платформы.Рассмотрим стандартный и наиболее читаемый подход, рекомендуемый стандартами разработки 1С. Для того чтобы сгруппировать данные по документу на уровне СУБД, добавим в конец текста запроса секцию ИТОГИ ПО. Это позволяет платформе сформировать многоуровневую выборку.
Посмотрим на пример текста запроса и последующего обхода:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РеализацияТоваровУслуг.Ссылка КАК Документ,
| РеализацияТоваровУслугТовары.Номенклатура КАК Товар,
| РеализацияТоваровУслугТовары.Количество КАК Количество
|ИЗ
| Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТоваровУслугТовары
| ПО РеализацияТоваровУслуг.Ссылка = РеализацияТоваровУслугТовары.Ссылка
|ИТОГИ ПО
| Документ";
РезультатЗапроса = Запрос.Выполнить();
// Выборка верхнего уровня — обходим только документы
ВыборкаДокументы = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаДокументы.Следующий() Цикл
// В этой точке доступна ссылка на текущий документ
ТекущийДокумент = ВыборкаДокументы.Документ;
// Подчиненная выборка — обходим детальные записи только текущего документа
ВыборкаТовары = ВыборкаДокументы.Выбрать(ОбходРезультатаЗапроса.Прямой);
Пока ВыборкаТовары.Следующий() Цикл
// Обрабатываем конкретную строку с товаром
ТекТовар = ВыборкаТовары.Товар;
ТекКоличество = ВыборкаТовары.Количество;
КонецЦикла;
КонецЦикла;
При таком подходе платформа не создает лишних таблиц в памяти, а СУБД отдает данные порциями. Обход получается логичным, прозрачным и защищенным от ошибок утечки памяти.
Если нам не требуется вычислять агрегатные функции (суммы, максимумы) на уровне документа, построение итогов в запросе может создать небольшую дополнительную нагрузку на СУБД. В высоконагруженных сценариях самым быстрым методом является обычная сортировка с линейным обходом и фиксацией момента смены документа.
Разберем реализацию этого механизма по шагам:
УПОРЯДОЧИТЬ ПО Документ.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РеализацияТоваровУслуг.Ссылка КАК Документ,
| РеализацияТоваровУслугТовары.Номенклатура КАК Товар,
| РеализацияТоваровУслугТовары.Количество КАК Количество
|ИЗ
| Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТоваровУслугТовары
| ПО РеализацияТоваровУслуг.Ссылка = РеализацияТоваровУслугТовары.Ссылка
|УПОРЯДОЧИТЬ ПО
| Документ";
Выборка = Запрос.Выполнить().Выбрать();
ТекущийДокумент = Неопределено;
Пока Выборка.Следующий() Цикл
Если Выборка.Документ <> ТекущийДокумент Тогда
ТекущийДокумент = Выборка.Документ;
// Здесь выполняется логика при переходе к новому документу
КонецЕсли;
// Здесь обрабатывается каждая строка товара
ТекТовар = Выборка.Товар;
КонецЦикла;
Этот способ работает за строго линейное время O(N), не требует расчета промежуточных итогов на сервере СУБД и расходует минимум оперативной памяти.
Если данные действительно необходимо представить в иерархическом виде в памяти (например, для интерактивного отображения в интерфейсе формы), не нужно сворачивать и копировать таблицы вручную. Достаточно использовать возможность метода Выгрузить() с указанием типа обхода:
// В запросе обязательно должно присутствовать предложение ИТОГИ ПО Документ
ДеревоРезультата = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
Для Каждого СтрокаДокумента Из ДеревоРезультата.Строки Цикл
ТекДокумент = СтрокаДокумента.Документ;
Для Каждого СтрокаТовара Из СтрокаДокумента.Строки Цикл
ТекТовар = СтрокаТовара.Товар;
КонецЦикла;
КонецЦикла;
Платформа автоматически сформирует объект ДеревоЗначений с нужной иерархией в одну системную операцию.
Иногда запрос выполнить невозможно, так как коллекция строк уже сформирована в виде объекта ТаблицаЗначений. В этом случае, чтобы не допускать квадратичного замедления при вызове НайтиСтроки(), обязательно используем коллекцию Индексы:
// Создаем индекс по колонке поиска перед началом циклов
ТЗТовары.Индексы.Добавить("Документ");
Отбор = Новый Структура("Документ");
Для Каждого СтрДокумент Из ТЗСвернутая Цикл
Отбор.Документ = СтрДокумент.Документ;
НайденныеСтроки = ТЗТовары.НайтиСтроки(Отбор); // Поиск выполняется за O(log N)
Для Каждого СтрТовар Из НайденныеСтроки Цикл
// Обработка строк
КонецЦикла;
КонецЦикла;
Если внутри цикла обхода планируется не просто чтение, а изменение и запись документов (например, через ПолучитьОбъект() и метод Записать()), обратим внимание на следующие правила:
ПолучитьОбъект() накладывайте объект БлокировкаДанных на изменяемый документ во избежание взаимных блокировок (deadlocks).