Как правильно обойти результат запроса по документам и строкам в 1С

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Нередко перед начинающими разработчиками встает типичная задача: получить из базы список документов и связанные с ними товарные позиции (или строки табличных частей), а затем последовательно обработать каждую строку в разрезе своего документа. Распространенной ошибкой является выгрузка всех данных в промежуточную таблицу значений, создание ее копии, сворачивание по ссылке на документ и последующий циклический поиск строк через метод НайтиСтроки().

Разберем, почему такая схема считается антипаттерном в платформе 1С:Предприятие, какие системные ресурсы она расходует и какими штатными механизмами платформы следует пользоваться для достижения максимальной производительности.

В чем недостатки ручной выгрузки и поиска в таблице значений

Проанализируем архитектурные проблемы алгоритма со сворачиванием таблицы и последующим отбором строк:

  1. Квадратичная алгоритмическая сложность: Когда мы вызываем метод НайтиСтроки() для обычной таблицы значений без явных индексов, платформа производит полный линейный перебор всех записей. Если в таблице 10 000 строк и 500 документов, система сделает до 5 000 000 сравнений. При росте базы это приводит к зависанию обработки.
  2. Избыточное потребление памяти: Использование методов Скопировать() и Свернуть() дублирует массивы данных в оперативной памяти сервера. Вся таблица принудительно материализуется целиком, что при больших выборках может исчерпать доступную память рабочего процесса.
  3. Отказ от курсорного чтения: Выгрузка через Выгрузить() заставляет СУБД отдать абсолютно все записи сразу, лишая нас преимуществ последовательного курсорного чтения платформы.

Способ 1. Иерархический обход выборки с использованием предложения ИТОГИ

Рассмотрим стандартный и наиболее читаемый подход, рекомендуемый стандартами разработки 1С. Для того чтобы сгруппировать данные по документу на уровне СУБД, добавим в конец текста запроса секцию ИТОГИ ПО. Это позволяет платформе сформировать многоуровневую выборку.

Посмотрим на пример текста запроса и последующего обхода:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	РеализацияТоваровУслуг.Ссылка КАК Документ,
    |	РеализацияТоваровУслугТовары.Номенклатура КАК Товар,
    |	РеализацияТоваровУслугТовары.Количество КАК Количество
    |ИЗ
    |	Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
    |	ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТоваровУслугТовары
    |		ПО РеализацияТоваровУслуг.Ссылка = РеализацияТоваровУслугТовары.Ссылка
    |ИТОГИ ПО
    |	Документ";

РезультатЗапроса = Запрос.Выполнить();

// Выборка верхнего уровня — обходим только документы
ВыборкаДокументы = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

Пока ВыборкаДокументы.Следующий() Цикл
    
    // В этой точке доступна ссылка на текущий документ
    ТекущийДокумент = ВыборкаДокументы.Документ;
    
    // Подчиненная выборка — обходим детальные записи только текущего документа
    ВыборкаТовары = ВыборкаДокументы.Выбрать(ОбходРезультатаЗапроса.Прямой);
    
    Пока ВыборкаТовары.Следующий() Цикл
        // Обрабатываем конкретную строку с товаром
        ТекТовар = ВыборкаТовары.Товар;
        ТекКоличество = ВыборкаТовары.Количество;
    КонецЦикла;
    
КонецЦикла;

При таком подходе платформа не создает лишних таблиц в памяти, а СУБД отдает данные порциями. Обход получается логичным, прозрачным и защищенным от ошибок утечки памяти.

Способ 2. Линейный обход с сортировкой (самый быстрый метод)

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

Разберем реализацию этого механизма по шагам:

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

Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	РеализацияТоваровУслуг.Ссылка КАК Документ,
    |	РеализацияТоваровУслугТовары.Номенклатура КАК Товар,
    |	РеализацияТоваровУслугТовары.Количество КАК Количество
    |ИЗ
    |	Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
    |	ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТоваровУслугТовары
    |		ПО РеализацияТоваровУслуг.Ссылка = РеализацияТоваровУслугТовары.Ссылка
    |УПОРЯДОЧИТЬ ПО
    |	Документ";

Выборка = Запрос.Выполнить().Выбрать();

ТекущийДокумент = Неопределено;

Пока Выборка.Следующий() Цикл
    
    Если Выборка.Документ <> ТекущийДокумент Тогда
        ТекущийДокумент = Выборка.Документ;
        // Здесь выполняется логика при переходе к новому документу
    КонецЕсли;
    
    // Здесь обрабатывается каждая строка товара
    ТекТовар = Выборка.Товар;
    
КонецЦикла;

Этот способ работает за строго линейное время O(N), не требует расчета промежуточных итогов на сервере СУБД и расходует минимум оперативной памяти.

Способ 3. Прямая выгрузка в дерево значений

Если данные действительно необходимо представить в иерархическом виде в памяти (например, для интерактивного отображения в интерфейсе формы), не нужно сворачивать и копировать таблицы вручную. Достаточно использовать возможность метода Выгрузить() с указанием типа обхода:


// В запросе обязательно должно присутствовать предложение ИТОГИ ПО Документ
ДеревоРезультата = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);

Для Каждого СтрокаДокумента Из ДеревоРезультата.Строки Цикл
    ТекДокумент = СтрокаДокумента.Документ;
    
    Для Каждого СтрокаТовара Из СтрокаДокумента.Строки Цикл
        ТекТовар = СтрокаТовара.Товар;
    КонецЦикла;
КонецЦикла;

Платформа автоматически сформирует объект ДеревоЗначений с нужной иерархией в одну системную операцию.

Что делать, если таблица значений уже получена извне

Иногда запрос выполнить невозможно, так как коллекция строк уже сформирована в виде объекта ТаблицаЗначений. В этом случае, чтобы не допускать квадратичного замедления при вызове НайтиСтроки(), обязательно используем коллекцию Индексы:


// Создаем индекс по колонке поиска перед началом циклов
ТЗТовары.Индексы.Добавить("Документ");

Отбор = Новый Структура("Документ");
Для Каждого СтрДокумент Из ТЗСвернутая Цикл
    Отбор.Документ = СтрДокумент.Документ;
    НайденныеСтроки = ТЗТовары.НайтиСтроки(Отбор); // Поиск выполняется за O(log N)
    
    Для Каждого СтрТовар Из НайденныеСтроки Цикл
        // Обработка строк
    КонецЦикла;
КонецЦикла;

Особенности изменения данных и блокировки

Если внутри цикла обхода планируется не просто чтение, а изменение и запись документов (например, через ПолучитьОбъект() и метод Записать()), обратим внимание на следующие правила:

  1. Не модифицируйте объекты в единой бесконечной транзакции: Если документов тысячи, длительная транзакция заблокирует таблицы СУБД и вызовет ошибки ожидания у пользователей. Записывайте изменения порциями или фиксируйте транзакцию на каждый отдельный документ.
  2. Используйте управляемые блокировки: При параллельной работе пользователей и фоновых заданий перед вызовом ПолучитьОбъект() накладывайте объект БлокировкаДанных на изменяемый документ во избежание взаимных блокировок (deadlocks).
← На главную