Быстрая обработка 1С: замер, запросы, порции и фон
Содержание
Правильная обработка в 1С строится по одному принципу: данные читаются одним запросом, а не в цикле. Пишутся они порциями в коротких транзакциях, а всё, что идёт дольше минуты, уходит в фоновое задание. Сначала замер, потом оптимизация. Без замера легко потратить день на ускорение кода, который занимает 2% времени.
Подход одинаков для внешней обработки .epf, кода в расширении и самой конфигурации. Примеры написаны для платформы 8.3 и управляемых форм.
С чего начать: найти, где обработка теряет время
Медленная обработка почти никогда не тормозит «вся». Обычно виноваты 1-3 строки: запрос в цикле, обращение к реквизиту ссылки, запись объекта с тяжёлым обработчиком. Задача первого шага — найти именно их.
Замер производительности в отладчике
- Откройте конфигурацию в Конфигураторе и запустите отладку (
F5). - Включите
Отладка → Замер производительности. - Выполните обработку на типичном объёме данных.
- Выключите замер. Откроется таблица: модуль, строка, число вызовов, время и процент.
Сортируйте по проценту. Строка с 40-60% времени — первый кандидат. Смотрите и на колонку количества: строка, вызванная 120 000 раз, подсказывает, что внутри цикла сидит обращение к базе.
💡 Замер ведите на копии базы с реальным объёмом. На демо-базе из 300 документов любой код быстрый, а проблемы видны только на сотнях тысяч строк.
Технологический журнал
Если медленно работает не ваш код, а платформа или СУБД (блокировки, долгие SQL-запросы), нужен технологический журнал. Он включается файлом logcfg.xml в каталоге conf установленной платформы. Полезные события:
- 🔎
DBMSSQL/DBPOSTGRS— запросы к СУБД с длительностью; - 🔒
TLOCK,TTIMEOUT,TDEADLOCK— управляемые блокировки, таймауты и взаимоблокировки; - ⏱
CALL— серверные вызовы и их длительность.
Журнал без фильтра по длительности за час вырастает до гигабайтов. Ставьте отбор (например, события дольше 1 секунды) и ограничение срока хранения в logcfg.xml.
Простой замер в коде
Для быстрой проверки «до/после» хватает разницы времени в миллисекундах:
Начало = ТекущаяУниверсальнаяДатаВМиллисекундах();
// проверяемый фрагмент
Длительность = ТекущаяУниверсальнаяДатаВМиллисекундах() - Начало;
Сообщить(СтрШаблон("Фрагмент выполнен за %1 мс", Формат(Длительность, "ЧГ=0")));
Оптимизация запросов и выборок: читать данные одним запросом
Главная причина медленных обработок — работа с объектами там, где нужен запрос. Каждое обращение «через точку» к реквизиту ссылки (Документ.Контрагент.ИНН) — отдельное чтение из базы. В цикле по 50 000 документов это 50 000 лишних обращений.
Как выглядит медленный код
// Медленно: чтение объекта на каждой итерации
Выборка = Документы.РеализацияТоваровУслуг.Выбрать(НачалоПериода, КонецПериода);
Пока Выборка.Следующий() Цикл
ИНН = Выборка.Ссылка.Контрагент.ИНН;
ДокОбъект = Выборка.Ссылка.ПолучитьОбъект();
// ...
КонецЦикла;
Здесь два узких места: разыменование ссылки и ПолучитьОбъект(), который читает документ целиком со всеми табличными частями.
Как то же самое сделать запросом
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Док.Ссылка КАК Ссылка,
| Док.Контрагент.ИНН КАК ИНН,
| Док.СуммаДокумента КАК Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
| Док.Дата МЕЖДУ &Начало И &Конец
| И Док.Проведен";
Запрос.УстановитьПараметр("Начало", НачалоПериода);
Запрос.УстановитьПараметр("Конец", КонецПериода);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Выборка.ИНН уже получен, базу не трогаем
КонецЦикла;
Внутри запроса «точка» превращается в соединение таблиц на стороне СУБД. Это одна операция вместо тысяч.
Правила хорошего запроса
- ✅ Выбирайте только нужные поля.
ВЫБРАТЬ *тянет всё, включая хранилища значений. - ✅ Отбор по виртуальным таблицам (
Остатки,Обороты,СрезПоследних) ставьте в параметры таблицы, а не вГДЕ. Тогда отбор применяется до расчёта, а не после. - ✅ Для составных типов (
Регистратор, поля «любой ссылки») используйтеВЫРАЗИТЬ(Поле КАК Документ.ХХХ): без него платформа соединяет поле со всеми возможными таблицами. - ✅ Условия ставьте на индексированные поля: измерения регистров,
Ссылка,Дата, реквизиты с флагом индексирования. - ❌ Не пишите запрос внутри цикла. Соберите ключи в таблицу значений и передайте её одним параметром.
Выборка или выгрузка
| Способ | Когда подходит | На что смотреть |
|---|---|---|
Результат.Выбрать() | Последовательный проход по большому результату | Строки читаются по мере обхода, память не растёт |
Результат.Выгрузить() | Нужны поиск, свёртка, повторный проход | Весь результат в памяти; на миллионе строк заметно |
ВыборкаИерархия / группировки ИТОГИ | Отчётные данные с подытогами | Удобна, но запрос с ИТОГИ тяжелее |
Пакетные запросы и временные таблицы
Когда одной выборки мало (нужно отфильтровать товары, затем посчитать по ним остатки, затем цены), разбивайте работу на пакет. Каждый шаг кладите во временную таблицу.
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
| &ТаблицаТоваров КАК Т
|
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Остатки.Номенклатура КАК Номенклатура,
| Остатки.КоличествоОстаток КАК Количество
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(
| &Дата,
| Номенклатура В
| (ВЫБРАТЬ
| ВТ.Номенклатура
| ИЗ
| ВТ_Товары КАК ВТ)) КАК Остатки";
Запрос.УстановитьПараметр("ТаблицаТоваров", ТаблицаТоваров);
Запрос.УстановитьПараметр("Дата", ДатаОстатков);
Результаты = Запрос.ВыполнитьПакет();
ТаблицаОстатков = Результаты[1].Выгрузить();
Имя регистра здесь условное: подставьте регистр своей конфигурации. Таблица значений в параметре должна иметь типизированные колонки, иначе запрос не поймёт тип поля.
Что даёт пакет:
- 📦 один вызов сервера вместо нескольких;
- 🧩 сложный запрос распадается на простые шаги, которые оптимизатор СУБД планирует лучше;
- 📇
ИНДЕКСИРОВАТЬ ПОускоряет соединение с временной таблицей на десятках тысяч строк.
💡 На маленьких временных таблицах (сотни строк) индекс не нужен: время на его построение больше выигрыша.
ВыполнитьПакет() возвращает массив результатов по порядку запросов, счёт с нуля. Запросы ПОМЕСТИТЬ тоже занимают позицию в массиве, поэтому финальная выборка в примере — Результаты[1].
Запись больших объёмов: порции и короткие транзакции
Чтение ускорить проще, чем запись. При записи работают обработчики ПередЗаписью, ОбработкаПроведения, подписки на события, регистрация изменений для обменов. Это нормально, но требует дисциплины.
Транзакция на порцию, а не на всё
Одна транзакция на 100 000 объектов держит блокировки до самого конца, и остальные пользователи ждут. Транзакция на каждый объект — лишние накладные расходы. Компромисс — порции по 200-1000 объектов:
РазмерПорции = 500;
Счетчик = 0;
НачатьТранзакцию();
Попытка
Для Каждого Строка Из ДанныеДляЗаписи Цикл
Объект = Строка.Ссылка.ПолучитьОбъект();
Объект.Комментарий = Строка.НовыйКомментарий;
Объект.Записать();
Счетчик = Счетчик + 1;
Если Счетчик % РазмерПорции = 0 Тогда
ЗафиксироватьТранзакцию();
НачатьТранзакцию();
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
При ошибке откатится только текущая порция, уже зафиксированные останутся. Поэтому обработка должна уметь продолжить с места остановки: например, выбирать только ещё не изменённые объекты.
Запись регистров одним набором
Для регистров сведений и накопления пишите набор записей целиком, а не по одной записи. Наполните НаборЗаписей нужными строками, установите отбор и вызовите Записать() один раз.
Режим загрузки
Объект.ОбменДанными.Загрузка = Истина перед записью отключает проверки и часть логики обработчиков, если они написаны по стандарту и проверяют этот флаг. Запись становится в разы быстрее.
Режим загрузки пропускает проверку заполнения и бизнес-логику. Используйте его только для переноса уже проверенных данных. Включать его при проведении документов нельзя: движения не сформируются как обычно.
Фоновые задания и асинхронное выполнение
Обработка дольше 20-30 секунд не должна блокировать форму. Пользователь закрывает «зависшее» окно, сеанс обрывается, данные остаются наполовину изменёнными. Выход — запуск на сервере в фоне.
Запуск фонового задания
Метод должен быть экспортным и лежать в серверном общем модуле:
// Общий модуль "ОбработкаДанныхСервер", флаг "Сервер"
Процедура ОбработатьПериод(Начало, Конец, АдресРезультата) Экспорт
// тяжёлая работа
ПоместитьВоВременноеХранилище(Итог, АдресРезультата);
КонецПроцедуры
// Вызов на сервере из формы
Параметры = Новый Массив;
Параметры.Добавить(НачалоПериода);
Параметры.Добавить(КонецПериода);
Параметры.Добавить(АдресРезультата);
Задание = ФоновыеЗадания.Выполнить(
"ОбработкаДанныхСервер.ОбработатьПериод", Параметры, , "Пересчёт за период");
ИдентификаторЗадания = Задание.УникальныйИдентификатор;
Адрес временного хранилища создайте заранее на форме через ПоместитьВоВременноеХранилище(Неопределено, УникальныйИдентификатор). Результат забирайте ПолучитьИзВременногоХранилища() после завершения задания.
Как следить за ходом
На клиенте подключите проверку раз в 2-5 секунд через ПодключитьОбработчикОжидания. В ней на сервере найдите задание ФоновыеЗадания.НайтиПоУникальномуИдентификатору() и посмотрите Состояние: активно, завершено, завершено аварийно или отменено. При аварийном завершении текст ошибки лежит в ИнформацияОбОшибке.
В типовых конфигурациях на БСП то же самое уже сделано в общем модуле ДлительныеОперации: он запускает задание, показывает индикатор и возвращает результат. Для своих доработок в типовой проще использовать его, чем писать обвязку заново.
Параллельная обработка
Большой объём можно разделить на части (по месяцам, по организациям, по диапазону кодов) и запустить несколько фоновых заданий одновременно. На сервере с 8 ядрами 4 параллельных задания часто сокращают время в 3 раза.
- ✅ Части не должны пересекаться по данным, иначе задания будут ждать друг друга на блокировках.
- ✅ Число заданий держите меньше числа ядер: пользователям тоже нужен процессор.
- ❌ Не запускайте параллельно запись в одни и те же остатки регистра накопления.
💡 В файловой базе фоновые задания тоже работают, но выполняются в процессе клиентского приложения и параллельной выгоды почти не дают. Реальное ускорение распараллеливанием — в клиент-серверном варианте.
Асинхронность на клиенте
Начиная с платформы 8.3.18 на клиенте есть Асинх и Ждать: диалоги и вопросы пользователю пишутся последовательно, без каскада процедур-оповещений. Это упрощает код, но не ускоряет обработку данных. Тяжёлая работа всё равно должна идти на сервере.
Прямой SQL к базе 1С: когда допустимо
Прямые запросы к СУБД (MS SQL, PostgreSQL) соблазняют скоростью. Правило простое: читать можно с оговорками, писать нельзя.
Почему не писать в таблицы 1С напрямую
- 🧮 Итоги регистров накопления и бухгалтерии хранятся в отдельных таблицах. Изменение движений в обход платформы оставит итоги неверными.
- 🔁 Обмены не узнают об изменении: запись не попадёт в планы обмена.
- 🧠 Сервер 1С кэширует данные, и пользователи какое-то время видят старые значения.
- 📜 Имена таблиц (
_Document123,_Fld4567) меняются при реструктуризации, и скрипт перестаёт работать после обновления.
Когда SQL уместен
- Чтение для внешней аналитики: выгрузка в хранилище данных, BI-системы. Соответствие имён таблиц объектам конфигурации даёт функция
ПолучитьСтруктуруХраненияБазыДанных(). - Обслуживание самой СУБД: перестроение индексов, обновление статистики, контроль размера файлов. Это задачи администратора базы, и на скорость 1С они влияют сильнее любых правок кода.
- Работа с внешней базой (не 1С): через объект конфигурации «Внешние источники данных» или через
Новый COMОбъект("ADODB.Connection")на Windows-сервере.
Регламент обслуживания СУБД (индексы, статистика) запускайте в нерабочее время: перестроение индексов нагружает диски, и пользователи почувствуют замедление.
Архивация данных, свертка базы и журнал регистрации
Даже идеальный код замедляется, если база за 8 лет выросла до 300 ГБ. Обработка, которая читает историю, становится медленнее вместе с историей.
Свертка и сокращение базы
В типовых конфигурациях на БСП есть свертка информационной базы: документы до выбранной даты заменяются вводом остатков. Ищите её в разделе Администрирование → Обслуживание.
Порядок работы:
- Сделайте полную выгрузку
.dtили резервную копию СУБД. Свертка необратима. - Выполните свертку на копии и сверьте ключевые остатки (счета, склады, взаиморасчёты) на дату свертки.
- Только после сверки повторите на рабочей базе.
Альтернатива свертке — архивная копия: старую базу оставить «только для чтения», а в рабочей начать учёт с остатков.
Итоги регистров
Для регистров накопления и бухгалтерии установите период рассчитанных итогов на начало текущего месяца. Тогда платформа считает остатки от ближайшего хранимого итога, а не по всей истории. Настраивается это в обработке «Управление итогами» (в меню Все функции → Стандартные). Там же есть пересчёт итогов, если отчёты расходятся с движениями.
Журнал регистрации
Журнал растёт незаметно: на базе с 50 пользователями он за год занимает десятки гигабайт. Это бьёт по диску и по скорости открытия самого журнала.
- 🗂 Уровень записи и разделение файлов по периодам задаются в Конфигураторе:
Администрирование → Настройка журнала регистрации. - ✂️ Старые записи удаляются там же командой сокращения журнала: укажите дату, до которой записи не нужны, и при желании сохраните их в файл.
- 📁 В клиент-серверной базе журнал лежит на сервере в каталоге кластера (
srvinfo\reg_1541\<идентификатор базы>\1Cv8Log).
Журнал нужен для разбора инцидентов: кто и когда изменил документ. Перед сокращением согласуйте срок хранения с бухгалтерией и безопасностью, а архив сохраните в файл.
Частые ошибки при обработке данных
Ещё три ошибки, которые встречаются в каждой второй самописной обработке:
- ❌ Выполнение тяжёлой логики в
&НаКлиенте: каждый вызов сервера из цикла — сетевой круг. Переносите цикл целиком в&НаСервереБезКонтекста. - ❌ Передача на сервер всей формы ради одного параметра. Директива
&НаСерверегоняет данные формы туда и обратно,&НаСервереБезКонтекста— нет. - ❌ Отсутствие индикатора хода. Пользователь не видит прогресса, считает обработку зависшей и снимает сеанс посреди записи.
Частые вопросы
Внешняя обработка работает медленнее встроенной в конфигурацию?
.epf выполняется так же, как код конфигурации. Скорость определяется запросами и записью, а не способом подключения.Сколько объектов писать в одной транзакции?
Можно ли ускорить обработку, включив монопольный режим?
Почему запрос в консоли быстрый, а в обработке медленный?
Нужно ли переписывать обработку для PostgreSQL?
ИЛИ в условиях на разных СУБД выполняются по-разному, поэтому замер стоит повторить после переезда.