Обход документов в 1С 8.3: от Выборки до фоновой обработки

· 9 мин

Содержание

Перебрать все документы в 1С 8.3 можно тремя способами: объектом Выборка, запросом по ссылкам и обходом коллекции Метаданные.Документы. Какой выбрать — зависит от того, что вы делаете: читаете пару реквизитов, вытягиваете объект целиком или массово перезаписываете.

Ошибка почти всегда одна: берут Выборку там, где хватило бы запроса, грузят весь объект ради одного поля и заворачивают обработку в одну гигантскую транзакцию. На тысяче документов это незаметно, на полумиллионе — база встаёт. Ниже разберём каждый метод с рабочим кодом, сравним по скорости и покажем, как обрабатывать большие объёмы без простоя пользователей.

Весь код выполняется в серверном контексте: на клиенте Выбрать() и ПолучитьОбъект() недоступны.

Построчное чтение объектом Выборка

Выборка — самый простой перебор. Менеджер документа возвращает объект, который читается по одной записи:

Выборка = Документы.РеализацияТоваровУслуг.Выбрать();
Пока Выборка.Следующий() Цикл
    Сообщить(Выборка.Номер + " от " + Формат(Выборка.Дата, "ДЛФ=D"));
КонецЦикла;

Метод Выбрать() принимает интервал дат, отбор и порядок:

Отбор = Новый Структура("Организация", ТекущаяОрганизация);
Выборка = Документы.РеализацияТоваровУслуг.Выбрать(
    НачалоГода, КонецГода, Отбор, "Дата Возр");

Поля вроде Выборка.Номер читаются сразу, а вот табличные части и весь документ по требованию:

Пока Выборка.Следующий() Цикл
    Объект = Выборка.ПолучитьОбъект();
    Для Каждого Строка Из Объект.Товары Цикл
        // работа с табличной частью
    КонецЦикла;
КонецЦикла;

Выборка не грузит всю таблицу в память — она читает записи порциями по мере вызова Следующий(). Поэтому по памяти она безопаснее, чем Запрос.Выполнить().Выгрузить(), но каждый ПолучитьОбъект() — это отдельное чтение объекта с сервера со всеми табличными частями.

Когда уместно: нужны почти все реквизиты документа или его табличные части, а объём умеренный. Для «пробежать и прочитать два поля у сотен тысяч документов» это медленно.

Важный момент про табличные части: их тоже можно читать запросом, не поднимая объект. Каждая таблица документа доступна как отдельная таблица запроса — Документ.РеализацияТоваровУслуг.Товары. Если нужны строки товаров по многим документам, запрос к этой таблице на порядок быстрее, чем ПолучитьОбъект() в цикле:

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

Строки.Ссылка здесь — ссылка на документ-владелец, через точку доступны его реквизиты. Поднимать объект нужно только тогда, когда вы собираетесь строки менять и записывать.

Быстрый перебор запросом и обработка порциями

Запрос читает только те поля, которые вы назвали. Это главное преимущество по скорости: платформа не поднимает объект целиком.

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|    Док.Ссылка КАК Ссылка,
|    Док.Дата КАК Дата,
|    Док.СуммаДокумента КАК Сумма
|ИЗ
|    Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
|    Док.Проведен
|УПОРЯДОЧИТЬ ПО
|    Док.Ссылка";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    // Выборка.Сумма уже прочитана — объект не поднимался
КонецЦикла;

Если документы надо не только прочитать, но и изменить, берите из запроса Ссылка и поднимайте объект только под запись. Не грузите всю базу ссылок в одну таблицу — на больших объёмах это съест память сервера. Правильный приём — порционное чтение «от последней ссылки»:

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

Пока Истина Цикл
    Запрос.УстановитьПараметр("ПоследняяСсылка", ПоследняяСсылка);
    Таблица = Запрос.Выполнить().Выгрузить();
    Если Таблица.Количество() = 0 Тогда
        Прервать;
    КонецЕсли;
    Для Каждого Строка Из Таблица Цикл
        Объект = Строка.Ссылка.ПолучитьОбъект();
        // изменение и запись объекта
    КонецЦикла;
    ПоследняяСсылка = Таблица[Таблица.Количество() - 1].Ссылка;
КонецЦикла;

Такой «курсор по ссылке» устойчив: даже если документы добавляются или помечаются на удаление во время обработки, вы не пропустите и не задвоите строки. Порцию в 500–1000 документов сервер держит легко.

Обычная пагинация через ВЫБРАТЬ ПЕРВЫЕ … УПОРЯДОЧИТЬ ПО Дата плоха для записи: при изменении данных смещаются границы и строки теряются. Упорядочивайте и фильтруйте строго по Ссылка.

Как пройти документы всех видов разом

Если задача — «все документы базы», а не один вид, перебирать каждый тип руками не нужно. Коллекция Метаданные.Документы знает все виды документов конфигурации:

Для Каждого ОписаниеДокумента Из Метаданные.Документы Цикл
    МенеджерДокумента = Документы[ОписаниеДокумента.Имя];
    Выборка = МенеджерДокумента.Выбрать();
    Пока Выборка.Следующий() Цикл
        // обработка документа любого вида
    КонецЦикла;
КонецЦикла;

Документы[ОписаниеДокумента.Имя] — обращение к менеджеру по строковому имени вида. Это универсальный способ: добавится новый документ в конфигурацию — цикл подхватит его сам, без правки кода.

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

Если ОписаниеДокумента.Реквизиты.Найти("Организация") <> Неопределено Тогда
    // у этого вида есть реквизит Организация
КонецЕсли;

Когда видов десятки, а объёмы большие, тот же порционный приём из прошлого раздела применяйте внутри цикла по видам — для каждого вида свой «курсор по ссылке».

Что быстрее: методы под разные задачи

Единого «самого быстрого» способа нет — скорость зависит от того, что вы читаете и меняете.

МетодЧто читаетСкоростьКогда брать
Выборка без ПолучитьОбъектреквизиты шапки по одной записисредняянужны поля шапки, объём умеренный
Выборка + ПолучитьОбъектвесь объект с табличными частяминизкаянужны табличные части или запись
Запрос по полямтолько названные полявысокаячтение отчётных данных, отборы, группировки
Запрос по ссылкам порциями + ПолучитьОбъектссылки порциями, объект под записьоптимум для массовой записиперепроведение, пересчёт, правка реквизитов

Ориентир для выбора:

Что именно нужно сделать с документами?

Два общих правила скорости: не поднимайте объект ради поля, которое отдаёт запрос, и никогда не ставьте запрос внутрь цикла перебора — выносите его за цикл или объединяйте во временную таблицу.

Отдельный случай — агрегация без записи: суммы, количества, сверка по документам. Здесь не нужен ни ПолучитьОбъект, ни порционный курсор. Группируйте прямо в запросе:

ВЫБРАТЬ
    Док.Организация КАК Организация,
    КОЛИЧЕСТВО(Док.Ссылка) КАК Количество,
    СУММА(Док.СуммаДокумента) КАК Сумма
ИЗ
    Документ.РеализацияТоваровУслуг КАК Док
ГДЕ
    Док.Проведен
СГРУППИРОВАТЬ ПО
    Док.Организация

Один запрос с группировкой всегда быстрее, чем ручной перебор со сложением в переменную: платформа считает это на стороне СУБД.

Транзакции и блокировки при массовой записи

Пока вы только читаете, транзакции не нужны. Как только начинаете писать — Записать() или Записать(РежимЗаписиДокумента.Проведение) — появляются блокировки регистров, и способ обёртки в транзакцию решает всё.

Главная ошибка — одна транзакция на весь прогон:

// так НЕ надо на больших объёмах
НачатьТранзакцию();
Пока Выборка.Следующий() Цикл
    Объект = Выборка.ПолучитьОбъект();
    Объект.Записать(РежимЗаписиДокумента.Проведение);
КонецЦикла;
ЗафиксироватьТранзакцию();

Такая транзакция держит блокировки до самого конца, разрастается до эскалации (блокируются целые таблицы) и намертво встаёт поперёк работы пользователей. Правильно — короткая транзакция на документ или на небольшую порцию:

Для Каждого Строка Из Таблица Цикл
    НачатьТранзакцию();
    Попытка
        Объект = Строка.Ссылка.ПолучитьОбъект();
        Объект.Записать(РежимЗаписиДокумента.Проведение);
        ЗафиксироватьТранзакцию();
    Исключение
        ОтменитьТранзакцию();
        ЗаписьЖурналаРегистрации("Обработка документов",
            УровеньЖурналаРегистрации.Ошибка, , Строка.Ссылка,
            ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
    КонецПопытки;
КонецЦикла;

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

Проведение в рабочей базе формирует движения по регистрам. Перед массовым перепроведением сделайте резервную копию и запускайте обработку в нерабочее время или монопольно — иначе конкурентные блокировки с пользователями неизбежны.

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

Порядок в блоке Исключение строгий: сначала ОтменитьТранзакцию(), и только потом логирование или любой другой код. Если вызвать запись в журнал до отмены, незакрытая транзакция может помешать и самому логированию. И не глушите исключение молча — без записи в журнал регистрации вы потом не узнаете, какие именно документы не обработались и почему.

Фоновая обработка больших объёмов

Десятки тысяч документов в клиентском сеансе обрабатывать нельзя: сеанс повиснет, а при разрыве связи работа оборвётся на середине. Выносите это в фоновое задание на сервере:

ПараметрыЗадания = Новый Массив;
ПараметрыЗадания.Добавить(НачалоПериода);
ПараметрыЗадания.Добавить(КонецПериода);

ФоновыеЗадания.Выполнить(
    "ОбработкаДокументовСервер.ПеребратьИОбработать",
    ПараметрыЗадания, , "Массовая обработка документов");

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

Для прогресса с показом пользователю в конфигурациях на БСП есть ДлительныеОперации.ВыполнитьВФоне — она запускает тот же серверный метод, но возвращает на форму идентификатор, по которому видно процент выполнения.

Когда объёмы по-настоящему велики, обработку делят на несколько параллельных заданий. Делите не по видам документов (их размеры разные — нагрузка перекосится), а по равным диапазонам ссылок: каждому заданию свой отрезок ГДЕ Ссылка > &От И Ссылка <= &До. Так работа ложится ровно, а общий прогон укладывается в ночное окно. Но не запускайте больше заданий, чем свободных рабочих процессов на сервере, — лишние встанут в очередь и выигрыша не дадут.

Отдельно про перепроведение: Записать(РежимЗаписиДокумента.Проведение) заново формирует движения по регистрам, а простая Записать() — только перезаписывает сам документ, не трогая проводки. Если задача пересчитать итоги — нужен именно режим проведения; если поправить реквизит шапки — хватит обычной записи, и это в разы легче для базы.

Самый безопасный сценарий для сотен тысяч документов — регламентное задание ночью: пользователей нет, блокировки никому не мешают, а порционные транзакции не дают разрастись нагрузке.

Частые ошибки при переборе документов

  • 🔁 Запрос внутри цикла перебора. На каждый документ — отдельное обращение к базе. Выносите запрос за цикл или собирайте данные во временную таблицу одним запросом.
  • 📦 ПолучитьОбъект() ради одного поля. Подъём всего объекта с табличными частями там, где хватило бы запроса по реквизиту. Читайте полями.
  • 🧱 Одна транзакция на весь прогон. Ведёт к эскалации блокировок и зависанию базы. Транзакция — на документ или порцию.
  • 🧭 Пагинация по дате или номеру при записи. Границы смещаются, строки теряются. Для устойчивого курсора упорядочивайте по Ссылка.
  • 🧨 Отсутствие Попытка вокруг записи. Один непроведённый документ обрушивает весь цикл. Логируйте ошибку и идите дальше.
  • 💾 Выгрузка всех ссылок в одну таблицу. На больших базах память сервера не выдержит. Читайте порциями по 500–1000.
Как перебрать только проведённые документы одного вида?
В запросе добавьте условие ГДЕ Док.Проведен. Для перебора объектом Выборка отфильтровать проведённость напрямую нельзя — там отбор идёт по реквизитам, поэтому для «только проведённые» удобнее запрос.
Почему `Выборка` с `ПолучитьОбъект` работает медленно?
Каждый вызов ПолучитьОбъект() поднимает с сервера весь документ вместе с табличными частями. Если вам нужны только поля шапки, замените на запрос по этим полям — объект поднимать не придётся, и скорость вырастет в разы.
Можно ли запускать перебор документов на клиенте?
Нет. Методы Выбрать(), ПолучитьОбъект() и выполнение запроса доступны только в серверном контексте (&НаСервере, общий серверный модуль, фоновое задание). С клиента вызывайте серверную процедуру.
Как показать прогресс при обработке десятков тысяч документов?
Запускайте обработку через ДлительныеОперации.ВыполнитьВФоне (БСП) и опрашивайте состояние задания на форме, либо считайте обработанные порции и пишите процент. Сам цикл при этом должен быть порционным и возобновляемым.
Чем обход через `Метаданные.Документы` лучше перечисления видов руками?
Он не требует правок при изменении конфигурации: новый вид документа цикл подхватит автоматически. Перечисление видов в коде придётся дополнять вручную и легко забыть.