Массовая обработка документов в 1С 8 из кода
Содержание
Чтобы перебрать все документы в 1С 8, используют запрос: он выбирает ссылки из таблицы Документ.<Имя>, а дальше результат обходят циклом Пока Выборка.Следующий() Цикл. Для чтения полей этого достаточно. Если документы нужно менять, внутри цикла получают объект через Выборка.Ссылка.ПолучитьОбъект() и записывают.
Разберём четыре способа перебора — от быстрого чтения запросом до параллельной обработки в фоновых заданиях, — а заодно приёмы, без которых большой журнал не осилить: запись без перепроведения, порционную выборку по ссылке и транзакции. Все примеры — для платформы 8.3 (интерфейс «Такси»), код на встроенном языке 1С.
Массовый перебор меняет данные во всей базе. Прежде чем запускать обработку на рабочей базе — сделайте резервную копию и прогоните код на копии. Отмена через ОбменДанными.Загрузка не пересчитывает движения, а восстановить придётся из бэкапа.
Запрос или прямой перебор: что выбирать
Есть два источника данных для цикла:
- 🔹 Запрос (
Новый Запрос) — читает поля прямо из СУБД. Самый быстрый способ получить ссылки и реквизиты шапки. Не грузит объект целиком в память. - 🔹 Метод
Документы.<Имя>.Выбрать()— возвращает выборку документов и позволяет сразу обращаться к объекту. Удобно для небольших объёмов, но на каждый документ идёт обращение к базе — на миллионах записей это медленно.
Практическое правило простое. Для чтения и отбора берите запрос. Объект (ПолучитьОбъект) запрашивайте только у тех документов, которые действительно будете менять. Получение объекта — дорогая операция: платформа читает весь документ с табличными частями.
Ещё до цикла определитесь с фильтром. Перебирать вообще все документы базы нужно редко. Обычно отбирают по виду, по периоду, по проведённости или по значению реквизита — и это сразу отсекает лишнее на стороне СУБД, а не в коде.
Чтение всех документов запросом
Базовый шаблон — выбрать ссылки и нужные реквизиты одного вида документа:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Док.Ссылка КАК Ссылка,
| Док.Номер КАК Номер,
| Док.Дата КАК Дата
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
| Док.Проведен
| И Док.Дата МЕЖДУ &Начало И &Конец
|УПОРЯДОЧИТЬ ПО
| Док.Дата";
Запрос.УстановитьПараметр("Начало", НачалоГода(ТекущаяДата()));
Запрос.УстановитьПараметр("Конец", КонецДня(ТекущаяДата()));
Результат = Запрос.Выполнить();
Несколько вещей, которые экономят время и память:
- В секции
ВЫБРАТЬперечисляйте только нужные поля.ВЫБРАТЬ *в 1С нет, и это к лучшему — лишние реквизиты тянут данные зря. - Условия ставьте в
ГДЕ, а не отсеивайте в цикле. Индекс поДатаиПроведенотработает на стороне СУБД. - Если нужны документы разных видов — не пишите цикл по каждому виду. Используйте таблицу
Документ(без указания вида) или объедините виды черезОБЪЕДИНИТЬ ВСЕ. ТаблицаДокументсодержит только общие поля (Ссылка, Дата, Номер, Проведен).
Запрос сам по себе ничего не читает из базы, пока вы не вызвали Выполнить(). А выборку по результату можно обходить сколько угодно раз — результат уже в памяти (если это не выборка курсором из СУБД).
Как обойти результат циклом
Результат запроса превращают в выборку методом Выбрать() и обходят циклом Пока … Цикл:
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(СтрШаблон("%1 от %2",
Выборка.Номер,
Формат(Выборка.Дата, "ДЛФ=D")));
КонецЦикла;
Метод Следующий() возвращает Ложь, когда записи кончились, — это и есть условие выхода. Обращайтесь к полям по именам, которые задали в секции ВЫБРАТЬ (Выборка.Номер, Выборка.Ссылка).
Три частые ошибки в самом цикле:
- ❌ Склейка числа и строки через
+. ЕслиНомер— число,Выборка.Номер + " от "вызовет ошибку приведения типов. ИспользуйтеСтрШаблонилиФормат(Число, "ЧГ=0"). - ❌ Запрос внутри цикла. Отдельный
Запрос.Выполнить()на каждой итерации превращает перебор в тысячи обращений к СУБД. Выносите выборку наружу. - ❌ Обход по группировкам как по строкам. Если в запросе есть
ИТОГИ,Выбрать()без аргумента вернёт детальные записи, аВыбрать(ОбходРезультатаЗапроса.ПоГруппировкам)— уровни итогов. Это разные обходы.
Для небольших журналов можно собрать все ссылки в массив (Выборка.Выгрузить() → колонка Ссылка) и работать с ним. Но для больших объёмов массив ссылок в памяти — лишний груз; обходите выборку напрямую.
Изменение документов: объект и запись без перепроведения
Чтобы изменить документ , одного запроса мало — запрос данные только читает. Внутри цикла получают объект и записывают его:
Пока Выборка.Следующий() Цикл
Объект = Выборка.Ссылка.ПолучитьОбъект();
Объект.Комментарий = "Проверено " + Формат(ТекущаяДата(), "ДЛФ=D");
Объект.ОбменДанными.Загрузка = Истина;
Объект.Записать();
КонецЦикла;
Ключевая строка здесь — ОбменДанными.Загрузка = Истина. Она говорит платформе: пиши документ как есть, не запуская обработчики ПередЗаписью, ПриЗаписи и проведение. Движения по регистрам при этом НЕ пересчитываются. Приём нужен, когда вы правите реквизит шапки (комментарий, ответственного, доп. признак) и не хотите трогать проводки.
Если же нужно именно перепровести документы — записывайте с режимом проведения:
Объект.Записать(РежимЗаписиДокумента.Проведение);
Тогда платформа заново сформирует движения. Это намного тяжелее: на каждый документ отрабатывает вся логика проведения. Массовое перепроведение планируйте на нерабочее время.
ОбменДанными.Загрузка = Истина обходит и контроль заполнения, и проверку проведения. Применяйте его осознанно: записанный так документ может разойтись с движениями, если вы поменяли то, что на них влияет.
Важная оговорка про объект МенеджерЗаписи: он относится к регистрам (сведений, накопления), а не к документам. Если вам нужно перебрать и поправить движения — работайте с набором записей регистра по регистратору (РегистрНакопления.<Имя>.СоздатьНаборЗаписей()), а не с документом. Путать эти объекты — типичная причина ошибок в массовых обработках.
Порционная обработка больших журналов
На сотнях тысяч документов цикл «прочитали всё → в одной транзакции записали всё» упирается в память и в блокировки: длинная транзакция держит записи и мешает пользователям. Решение — обрабатывать порциями по ссылке.
Приём с «курсором по ссылке»: выбираем первую тысячу, запоминаем последнюю обработанную ссылку, следующую порцию берём строго «больше» неё:
ПоследняяСсылка = Документы.РеализацияТоваровУслуг.ПустаяСсылка();
Пока Истина Цикл
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1000
| Док.Ссылка КАК Ссылка
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
| Док.Ссылка > &ПоследняяСсылка
|УПОРЯДОЧИТЬ ПО
| Док.Ссылка";
Запрос.УстановитьПараметр("ПоследняяСсылка", ПоследняяСсылка);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Количество() = 0 Тогда
Прервать;
КонецЕсли;
НачатьТранзакцию();
Попытка
Пока Выборка.Следующий() Цикл
Объект = Выборка.Ссылка.ПолучитьОбъект();
// … правки …
Объект.ОбменДанными.Загрузка = Истина;
Объект.Записать();
ПоследняяСсылка = Выборка.Ссылка;
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
КонецЦикла;
Что здесь важно:
- 🔹
Док.Ссылка > &ПоследняяСсылкас сортировкой по ссылке — надёжнее, чем смещениеВЫБРАТЬ … ПЕРВЫЕ Nсо сдвигом: не зависит от того, что записи между порциями меняются. - 🔹 Каждая порция — отдельная короткая транзакция. Блокировки держатся секунды, а не весь прогон.
- 🔹
Попытка … ИсключениесОтменитьТранзакцию()обязательны: без отмены упавшая транзакция оставит соединение в неработоспособном состоянии.
Размер порции 500–2000 — рабочий диапазон. Мельче — больше накладных расходов на транзакции, крупнее — дольше держатся блокировки. Простой счёт: журнал на 500 000 документов при порции 1000 — это 500 коротких транзакций вместо одной многочасовой, и между ними база свободна для пользователей.
Отдельный вопрос — блокировки. В управляемом режиме блокировок порция читает и пишет только свои документы, и конкурирующие сеансы ждут минимум. Если же обработка меняет регистры, по которым считаются остатки, закладывайте запуск на часы минимальной активности: одновременная правка одних и тех же итогов пользователем и обработкой почти наверняка приведёт к ожиданиям на блокировках.
Когда одной порции нужны данные из нескольких таблиц, помещайте промежуточные выборки во временные таблицы через МенеджерВременныхТаблиц: собрали ссылки порции в ПОМЕСТИТЬ ВТ_Порция, а следующие запросы джойнятся к ней. Это снимает нагрузку с СУБД по сравнению с десятком независимых запросов.
Параллельный перебор в фоновых заданиях
Если обработка идёт часы, её запускают в фоне, чтобы не держать сеанс пользователя и при желании распараллелить. Базовый запуск:
Параметры = Новый Массив;
Параметры.Добавить(НачалоПериода);
Параметры.Добавить(КонецПериода);
ФоновыеЗадания.Выполнить(
"ОбработкаДокументовСервер.ОбработатьПериод",
Параметры,
"ОбработкаДокументов",
"Перебор документов за период");
Несколько правил:
- Метод (
ОбработатьПериод) — это экспортная процедура общего модуля с установленным флагом «Сервер». Клиентский код в фоне недоступен. - Третий параметр — ключ уникальности. Пока задание с таким ключом выполняется, второе с тем же ключом не запустится — защита от двойного запуска.
- Для распараллеливания делят весь диапазон на части (например, по месяцам или по остатку от деления номера на число потоков) и запускают несколько заданий. Разумное число потоков — по количеству рабочих процессов кластера, а не «чем больше, тем лучше»: лишние потоки упрутся в диск и в блокировки друг друга.
Если нужен индикатор прогресса для пользователя, не изобретайте свой — в типовых конфигурациях на БСП для этого есть подсистема «Длительные операции». Она запускает тот же фоновый метод и показывает прогресс в форме.
Разбивая перебор на потоки, следите, чтобы они не пересекались по одним и тем же документам: два задания, пишущие один объект, поймают блокировку или ошибку записи. Делите данные по непересекающимся диапазонам.
Какой способ быстрее: сравнение
Скорость зависит от того, читаете вы данные или меняете. Ориентиры:
| Способ | Что делает | Когда применять |
|---|---|---|
| Запрос + цикл по выборке | Читает поля из СУБД | Чтение, отбор, отчёты — самый быстрый |
Документы.<Имя>.Выбрать() | Выборка объектов | Малые объёмы, когда сразу нужен объект |
ПолучитьОбъект() + запись | Чтение и запись документа | Изменение реквизитов, перепроведение |
| Порции по ссылке + транзакции | Запись большими объёмами | Сотни тысяч документов, рабочая база |
| Фоновые задания | Параллельный прогон | Часовые обработки, не блокируя сеанс |
Главный источник тормозов — не выбор цикла, а лишние обращения к базе: получение объекта там, где хватило бы запроса, запрос внутри цикла, перепроведение вместо записи с ОбменДанными.Загрузка. Сначала уберите их, и только потом думайте про потоки.
Разница на практике ощутимая. Чтение 100 000 документов запросом — это секунды. Получение объекта у каждого из них (ПолучитьОбъект) — уже минуты, потому что платформа читает шапку и все табличные части. А перепроведение тех же документов — это запуск полной логики проведения на каждом, и время растёт ещё на порядок. Поэтому порядок оптимизации один: не читать лишнего, не писать лишнего, и только потом — параллелить то, что осталось.
Частые ошибки при переборе документов
- ❌ Меняют реквизит прямо в выборке запроса.
Выборка.Комментарий = …ничего не запишет — выборка только для чтения. Менять можно лишь полученный объект. - ❌ Одна гигантская транзакция на весь прогон. Держит блокировки часами и валится по памяти. Дробите на порции.
- ❌ Перепроводят всё подряд. Если движения не менялись, перепроведение — пустая трата времени. Пишите с
ОбменДанными.Загрузка = Истина. - ❌ Забывают
ОтменитьТранзакцию()в блокеИсключение. После сбоя соединение остаётся в «подвешенной» транзакции, и дальнейшие запросы падают. - ❌ Распараллеливают на пересекающихся данных. Потоки встают в очередь на блокировках и дают меньше выигрыша, чем один поток.
Выберите способ под свою задачу — ниже короткая подсказка.
Пока Выборка.Следующий(). Объект не получайте — это лишняя нагрузка.Выборка.Ссылка.ПолучитьОбъект(), правка, ОбменДанными.Загрузка = Истина, Записать().Объект.Записать(РежимЗаписиДокумента.Проведение), порциями и в нерабочее время.Частые вопросы
Как перебрать документы всех видов сразу, а не одного?
Документ без указания вида — в ней общие поля Ссылка, Дата, Номер, Проведен. Если нужны реквизиты конкретных видов, объедините запросы по видам через ОБЪЕДИНИТЬ ВСЕ.Чем `ПолучитьОбъект()` отличается от чтения через запрос?
ПолучитьОбъект() читает весь документ с табличными частями в память и даёт его менять. Для чтения берите запрос, объект — только под запись.