Оптимизация ночного регламента 1С без переписывания: 4 антипаттерна, которые съедают окно
Иван Недомолков · Опубликовано:
Типичная ситуация: ночной регламент в 1С формально укладывается в норматив, но по журналу видно, что прогон плавно растёт от ночи к ночи. Пока укладывается, его никто не трогает. А потом наступает ночь, когда он в окно не влез, и утром пользователи получают недосчитанные цены или недоформированные документы.
Тот же сюжет разыгрывается и в масштабе месяца: если у вас за окно перестало влезать не одно задание, а закрытие месяца целиком, причины там другие - перепроведение, восстановление последовательности и блокировки, - но подход к поиску тот же. Бывает и противоположная беда: прогон не растёт, потому что задание вообще ничего не делает, хотя журнал показывает успех. Этот случай разобран отдельно - выгрузка по расписанию не работает, хотя задания зелёные. А если задание не запускается вовсе и журнал по нему пуст, порядок проверки свой: регламентное задание 1С не выполняется.
У нас был ровно такой регламент: ночная переоценка товаров без движения в сети розничных магазинов на ERP. Прогон плавал от 55 до 100 минут (в среднем около 73) при нормативе в три часа. Обработка большая, около 9 400 строк рабочей логики, и переписывать её с нуля означало гарантированную регрессию и недели проверок. Мы не переписывали. Время утекало в пяти конкретных местах, и каждое лечится точечно. Разберём антипаттерны, потому что они универсальные: цифры у вас будут свои, а места те же.
Отдельно про то, как эти пять правок находились в паре с ИИ-моделями и что из лабораторного прогноза подтвердилось в проде, у нас есть статья на Инфостарте.
Сначала главное: читайте журнал по шагам, а не общее время
Общее время прогона бесполезно для диагностики. Оно говорит “долго”, но не говорит где. Первый шаг всегда один: разложить регламент по шагам и посмотреть журнал за две недели, сколько занимает каждый шаг.
У нас разбивка сразу показала перекос. Большая часть окна уходила не в саму переоценку (она как раз была ни при чём), а в инфраструктуру: срез по лишним данным, построчная запись в регистр, обращения к базе внутри циклов. Один шаг каждую ночь стабильно жёг по несколько минут на пустых накладных расходах. По общему времени это было незаметно, по разбивке очевидно.
Вывод, годный для любого регламента: пока вы не видите время по шагам, вы оптимизируете вслепую. Начинайте с журнала регламентного задания, а не с догадок о том, что “наверное, тормозит запрос”.
Антипаттерн 1: срез виртуальной таблицы без отбора по нужному измерению
Самый дорогой из найденных. Шаг читал историю расчёта и подтягивал к ней текущие цены через СрезПоследних(). В параметрах среза стоял отбор только по типу цен, без ограничения по номенклатуре. Платформа честно строила срез по всем товарам этого типа цен, а лишнее отбрасывала уже при соединении.
В цифрах плана запроса это выглядело так: чтобы вернуть 300 строк, шаг делал 585 739 логических чтений страниц. Регистр цен там свыше 107 миллионов строк и около 93 ГБ индексов, так что на холодном ночном кэше заметная часть этих чтений превращается в физические.
Лечение не в переписывании запроса, а в переносе отбора. Нужные номенклатуры материализуются во временную таблицу с индексом, и срез ограничивается ею прямо в параметрах виртуальной таблицы, а не в условии соединения после. После правки те же 300 строк возвращались за 9 090 чтений вместо 585 739, в 64 раза меньше. Результат совпал посимвольно.
Вся суть в том, куда попадает отбор. В параметрах виртуальной таблицы платформа строит срез сразу узким, по тремстам нужным номенклатурам. В условии соединения после среза платформа сначала строит срез по всем товарам, перелопачивая сто с лишним миллионов строк регистра, и только потом отбрасывает лишнее. Запрос внешне почти одинаковый, разница в месте отбора, а в чтениях это 585 739 против 9 090. Это первое место, куда стоит смотреть: СрезПоследних() или Остатки() без отбора по нужному измерению.
Антипаттерн 2: запись в регистр по одной строке в цикле
Классика, которую видно грепом по модулю. Внутри цикла на каждую строку создаётся менеджер записи и вызывается Записать():
Для Каждого Строка Из ТЗРасчёта Цикл
МЗ = РегистрыСведений.ИсторияРасчёта.СоздатьМенеджерЗаписи();
// ...
МЗ.Записать(); // отдельная операция записи на КАЖДУЮ строку
КонецЦикла;
На 8 406 строках это 103 629 мс. Каждая мелкая запись это отдельная транзакция и отдельный коммит со сбросом лога на диск, и вот их восемь с лишним тысяч. Замена: прочитать набор записей за период, точечно заменить строки по ключу в памяти и записать всё одной операцией. Стало 5 265 мс, ускорение почти в 20 раз. Состав и контрольная сумма регистра до и после совпали побитово.
Тут есть тонкость, без которой правка была бы неверной. Запись набором с отбором по периоду перезаписывает весь период, это осознанный размен, и он безопасен только потому, что регламент единственный писатель в этот регистр и работает ночью в эксклюзивное окно. А ещё одно измерение регистра хранилось в базе усечённым, обрезанное имя вместо полного, и сопоставление ключей пришлось делать по нормализованному значению. Иначе вместо перезаписи в регистре появлялись бы дубли. Пакетную запись нельзя ставить вслепую: без сверки контрольной суммы такой дубль легко проехать в прод.
Антипаттерны 3-5: обращения к базе внутри циклов
Три места помельче, но по кратности эффектные, и все про одно: обращение к базе там, где его можно вынести из цикла.
- Реквизит справочника “через точку” внутри цикла. На каждую новую ссылку платформа читает весь объект целиком в кэш объектов. Пока номенклатура повторяется, это дёшево, но на тысячах разных ссылок это тысячи чтений объектов вместо одного запроса до цикла. Вынесли в предварительную выборку: ускорение участка от 212 до 265 раз в трёх разных местах.
Массив.Найти()при накоплении уникальных значений это линейный поиск на каждой вставке, квадрат по числу элементов. Рядом с массивом завелиСоответствиедля проверки наличия по хешу: около 7 раз.- Чтение настроек из базы на каждой строке детальной таблицы. Закэшировали в переменной модуля: было 3,05 мс на вызов, стало почти ноль.
Ни одна из этих трёх правок не меняет логику. Меняется только то, сколько раз шаг ходит в базу за одним и тем же ответом.
Методика: любую правку меряем числом “было / стало”
Это не про производительность, а про безопасность, и без этого пункта всё остальное превращается в угадывание. Каждую из пяти правок мы проверяли на эквивалентность результата: фиксировали результат до, применяли правку, сверяли после контрольной суммой набора записей или посимвольным отпечатком выборки.
Отпечаток это конкатенация полей результата в фиксированном порядке в одну строку, которую сравнивают посимвольно, так видно любое расхождение вплоть до одного изменившегося символа. У среза цен отпечаток совпал знак в знак на выборке в 50 685 символов. У регистра истории совпали состав и контрольная сумма. Без такой сверки правки в прод везти нельзя: “стало быстрее” ничего не стоит, если попутно “стало неправильно”.
И обратная сторона той же дисциплины: одну гипотезу мы проверили и отклонили. Идея про индексацию временных таблиц в главном запросе звучала правильно, но замер её не подтвердил (на холодном кэше чтений не сокращала, на прогретом была даже медленнее), и правку выбросили. В отчётах об оптимизации такой пункт обычно не показывают, а зря: именно отклонённая по замеру правка отличает инженерную работу от “переписали всё, что попалось”.
Как найти то же самое у себя: чек-лист
Если у вас есть ночной регламент, который “пока укладывается”, пройдите его по четырём шагам:
- Читайте журнал регламента по шагам. Общее время прячет, где именно потери. Часто основное время висит на одном шаге, о котором никто не думал.
- Смотрите логические чтения тяжёлых шагов (план запроса, статистика ввода-вывода, технологический журнал). “На выходе триста строк, а чтений полмиллиона” это верный признак виртуальной таблицы без отбора по нужному измерению.
- Грепните модуль по антипаттернам:
.Записать()внутри цикла, реквизиты через точку в циклах,Массив.Найти(при накоплении уникальных, чтение настроек внутри цикла по строкам. Если обработок в базе не одна, а два десятка, грепать каждую руками смысла мало: Анализ кода внешних обработок проходит по ним пакетом и строит рейтинг, худшие сверху. Обращение к базе внутри цикла у него отдельное правило с весом 60, находка приходит с номером строки и фрагментом кода. - Любую правку меряйте числом “было / стало” и сверяйте результат контрольной суммой или отпечатком.
Чаще всего большую часть окна съедает один шаг, и обычно это либо срез виртуальной таблицы без нужного отбора, либо запись в цикле. С них и стоит начинать.
Итог
Регламент, который растёт и подбирается к границе окна, почти никогда не требует переписывания. В нашем случае пять точечных правок в обработке на 9 400 строк убрали риск не уложиться в ночное окно, а самый тяжёлый шаг после выкладки отработал за секунды вместо минут. Диагностику мы вели с ИИ-агентом на доступе только на чтение, но решения и ответственность оставались на инженере (про этот эксперимент с моделями у нас отдельный разбор).
Готовые обработки по теме:
- Авто-УНИЧТОЖИТЬ: оптимизатор временных таблиц - разбирает пакетный запрос, находит место последнего использования каждой временной таблицы и расставляет уничтожение. В обработке на девять с половиной тысяч строк такие пакеты идут десятками, и держать их в голове бесполезно.
- Трансформатор SQL → запрос 1С - переводит текст запроса из следа СУБД в термины конфигурации, чтобы найти виновника по имени объекта, а не по номеру таблицы.
- Чек-ап СУБД под 1С - проверка настроек сервера, с которых стоит начинать, прежде чем править код.
Если у вас похожая история и ночное окно перестаёт вмещать регламент, посмотрите нашу услугу оптимизации 1С: мы приходим с замерами по шагам и сверкой результата, а не с переписыванием на всякий случай.