Почему код 1С работает медленно и как это исправить

· 10 мин

Содержание

Чаще всего код на 1С тормозит не из-за «слабого сервера», а из-за одной типовой ошибки: запрос внутри цикла. Обработка проходит по тысяче строк и на каждой лезет в базу — получается тысяча обращений вместо одного. Разберём, где в коде прячутся узкие места производительности платформы 1С:Предприятие 8.3 и как переписать запросы, циклы и работу с регистрами, чтобы та же задача считалась в разы быстрее.

Речь про управляемое приложение, платформу 8.3.2x и интерфейс «Такси». Приёмы общие для БП 3.0, ЗУП 3.1, УТ 11 и собственных конфигураций — они работают на уровне языка запросов и объектной модели, а не конкретного прикладного решения.

С чего начать: находим, что именно тормозит

Оптимизировать «весь код подряд» бессмысленно. Сначала измеряем и находим узкое место — иначе легко потратить день на ускорение процедуры, которая занимает 2% времени.

Инструменты, которые дают цифры:

  • 🔍 Замер производительности в конфигураторе (Отладка → Замер производительности). Запускаете, выполняете операцию в пользовательском режиме, останавливаете. Платформа покажет, сколько раз выполнилась каждая строка кода и сколько суммарно заняла. Строки-рекордсмены по времени — ваши кандидаты на переписывание.
  • ⏱ Технологический журнал — логирует длительные запросы и обращения к СУБД с текстом SQL. Настраивается файлом logcfg.xml. Нужен, когда тормоза видны только под нагрузкой на боевом сервере.
  • 📊 Центр управления производительностью (ЦУП) и показатель APDEX — для оценки отклика ключевых операций в целом по базе, а не одной процедуры.

Правило: сначала замер, потом правка. Оптимизируйте строку, которая в замере набрала больше всего времени, а не ту, что «на глаз кажется тяжёлой».

В технологическом журнале для поиска тормозов полезны события длительных операций: DBMSSQL (запросы к СУБД с текстом SQL и временем), TLOCK/TDEADLOCK (ожидания и взаимоблокировки), CALL (длительность серверных вызовов). Отфильтруйте по длительности — и сразу видно, какие запросы и ожидания съедают время под нагрузкой.

Отдельно смотрите на число обращений к СУБД. Если простая операция порождает сотни SQL-запросов — почти наверняка внутри цикла сидит запрос или чтение через точку. Это первое, что нужно убрать.

Запросы: убираем обращения к базе из цикла

Запрос к базе данных — самая дорогая операция в прикладном коде. Один хорошо написанный запрос почти всегда быстрее, чем перебор данных в коде объекта.

Запрос в цикле — главная ошибка

Классический антипаттерн: цикл по коллекции, и на каждой итерации — новый запрос.

// Плохо: N запросов к базе
Для Каждого СтрокаТЧ Из Товары Цикл
    Запрос = Новый Запрос;
    Запрос.Текст = "ВЫБРАТЬ Цена ИЗ РегистрСведений.ЦеныНоменклатуры...";
    Запрос.УстановитьПараметр("Номенклатура", СтрокаТЧ.Номенклатура);
    Результат = Запрос.Выполнить();
КонецЦикла;

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

// Хорошо: один запрос по всему списку
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ Номенклатура, Цена ИЗ РегистрСведений.ЦеныНоменклатуры
               |ГДЕ Номенклатура В (&СписокТоваров)";
Запрос.УстановитьПараметр("СписокТоваров", Товары.ВыгрузитьКолонку("Номенклатура"));

Запрос внутри цикла — первое, что нужно искать при тормозах. Один запрос вместо тысячи ускоряет операцию на порядки.

Чтение через точку — скрытый запрос

Обращение Ссылка.Реквизит.ЕщёРеквизит в коде выглядит безобидно, но каждое такое обращение в цикле — это отдельный запрос к базе. Платформа дочитывает данные объекта лениво, в момент обращения.

Если нужны реквизиты связанного объекта — получайте их запросом заранее, через соединение или поле Номенклатура.Поставщик, а не разыменовывайте ссылку в цикле построчно.

Выбирайте только нужные поля

ВЫБРАТЬ * тянет все поля таблицы, включая те, что не нужны. Перечисляйте явно только используемые колонки — меньше данных читается и передаётся.

Соединения и условия, которые не тормозят

Несколько приёмов, которые заметно влияют на план запроса:

  • Используйте ВНУТРЕННЕЕ СОЕДИНЕНИЕ, когда записи есть в обеих таблицах, и ЛЕВОЕ — когда вторая таблица может не иметь соответствия. ПОЛНОЕ СОЕДИНЕНИЕ и соединение по вычисляемому выражению почти всегда тяжёлые — избегайте их.
  • Условие ИЛИ в ГДЕ и в условиях соединения мешает СУБД использовать индекс. Там, где это возможно, замените ИЛИ на объединение (ОБЪЕДИНИТЬ ВСЕ) или на отбор по списку значений В (&Список).
  • Не соединяйте таблицу с подзапросом. Вынесите подзапрос в отдельную временную таблицу с индексом и соединяйте уже с ней.

Сравнение типовых решений одной задачи:

ЗадачаМедленноБыстро
Данные по списку объектовЗапрос в цикле по каждомуОдин запрос с В (&Список)
Остатки по региструГруппировка основной таблицыВиртуальная таблица Остатки
Отбор по складу в остаткахФильтр во внешнем ГДЕПараметр внутри виртуальной таблицы
Сложный отчёт с подзапросамиСоединение с подзапросомПОМЕСТИТЬ + индекс + соединение

Пакетные запросы вместо нескольких отдельных

Если логике нужно несколько связанных выборок, оформляйте их одним пакетным запросом через ПОМЕСТИТЬ и разделитель ;, а не отдельными объектами Запрос. Промежуточные результаты остаются во временных таблицах на сервере и не гоняются туда-обратно, а платформа выполняет пакет за одно обращение.

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

Для итогов по регистрам накопления используйте виртуальные таблицы Остатки, Обороты, ОстаткиИОбороты, а не основную таблицу движений с ручной группировкой СУММА(...) СГРУППИРОВАТЬ ПО. Виртуальные таблицы обращаются к таблицам итогов и работают быстрее.

Параметры отбора передавайте внутрь виртуальной таблицы, в её скобки: РегистрНакопления.Товары.Остатки(&НаДату, Склад = &Склад). Если фильтровать результат во внешнем ГДЕ, платформа сначала посчитает остатки по всем складам, а потом отбросит лишнее — медленно.

Циклы и выборки: меньше итераций, правильный порядок

После того как данные получены одним запросом, важно грамотно их перебрать.

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

Удаление из коллекции в цикле. Удалять элементы, двигаясь по коллекции вперёд, нельзя — индексы сдвигаются и часть элементов пропускается. Идите с конца:

Для Сч = Массив.Количество() - 1 По 0 Цикл
    Если УдалитьЭлемент(Массив[Сч]) Тогда
        Массив.Удалить(Сч);
    КонецЕсли;
КонецЦикла;

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

Не стройте строки конкатенацией в длинном цикле. Для склейки числа со строкой используйте Формат(Число, "ЧГ=0") или СтрШаблон(...), а не неявное приведение — так и корректнее, и предсказуемее по формату.

Выгружайте колонку, а не перебирайте строки. Когда из табличной части нужен список значений одного поля для параметра запроса, берите ВыгрузитьКолонку("Номенклатура") — это быстрее ручного цикла с добавлением в массив.

Проверяйте условие выхода из цикла заранее. Если по ходу перебора стало ясно, что искомое найдено, прерывайте цикл через Прервать, а не крутите его до конца вхолостую. Мелочь, но на больших выборках экономит реальное время.

Индексы и регистры: чтобы отбор не сканировал всю таблицу

Когда запрос фильтрует данные по реквизиту, а индекса по нему нет, СУБД читает таблицу целиком. На справочнике в сотню строк это незаметно, на документах за несколько лет — секунды ожидания.

Индексируйте реквизиты, по которым идёт отбор. В свойствах реквизита справочника или документа есть свойство Индексировать. Ставьте его тем полям, по которым регулярно фильтруете в запросах и динамических списках. Если по полю часто ищут вместе с поиском по наименованию — используйте Индексировать с доп. упорядочиванием.

Проектируйте регистры под запросы. Производительность регистра накопления закладывается в его структуру:

  • в измерения выносите то, по чему фильтруете и получаете остатки (склад, номенклатура, организация);
  • в ресурсы — то, что суммируется (количество, сумма);
  • в реквизиты — справочные данные, по которым не строят итоги.

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

Индексы временных таблиц. Если в запросе вы помещаете данные во временную таблицу через ПОМЕСТИТЬ, а затем соединяете её с другой таблицей, добавьте ИНДЕКСИРОВАТЬ ПО тому полю, по которому идёт соединение. Без индекса соединение по временной таблице работает медленно.

Соединение с подзапросом оптимизатор СУБД планирует плохо. Разбейте сложный запрос: сначала ПОМЕСТИТЬ промежуточных данных во временную таблицу с индексом, потом соединение с ней. Чаще всего это ускоряет тяжёлый отчёт сильнее всех прочих правок.

Объекты метаданных: не грузите лишнее

Работа через объектную модель (чтение, запись, проведение документов) тоже оптимизируется.

Не читайте объект целиком ради одного реквизита. Если нужно только значение реквизита, получайте его запросом или через ПолучитьЗначениеРеквизита/реквизиты запросом, а не методом .ПолучитьОбъект(), который дочитывает весь объект с табличными частями.

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

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

Расширения конфигурации вместо правки типовой. Это не про скорость выполнения, а про скорость поддержки: доработки через расширения (появились в 8.3.6) не ломают обновление типовой конфигурации. Загрузка .cfe и обновление конфигурации базы — отдельные шаги.

Архитектура и блокировки: где код мешает сам себе

На многопользовательской базе медленно бывает не из-за алгоритма, а из-за ожидания на блокировках: транзакция одного пользователя держит данные, остальные стоят в очереди.

Управляемые блокировки. Используйте управляемый режим блокировок. Он блокирует только те данные, которые реально читаете и меняете, а не широкие диапазоны. Явно ставьте управляемые блокировки на регистры перед расчётом остатков в транзакции, чтобы два проведения не посчитали остаток одновременно и не ушли в минус.

Короткие транзакции. Внутри транзакции делайте только запись и расчёты, без обращений к пользователю, внешних вызовов и долгих циклов. Чем короче транзакция держит блокировки, тем меньше остальные ждут.

Единый порядок обращения к ресурсам против взаимоблокировок. Deadlock возникает, когда две транзакции берут одни и те же объекты в разном порядке и упираются друг в друга. Лекарство — всегда обращаться к регистрам и справочникам в одном и том же порядке во всём коде.

Клиент-серверное взаимодействие. Помните про границы &НаСервере / &НаКлиенте: каждый переход клиент-сервер передаёт данные по сети. Не гоняйте в цикле вызовы сервера из клиентского кода — соберите данные и вызовите сервер один раз. Тяжёлые вычисления выполняйте на сервере, клиенту отдавайте результат.

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

Что проверить перед тем, как считать код медленным
Выполнено: 0 / 8

Частые вопросы

С чего начинать оптимизацию, если непонятно, что тормозит?
С замера производительности в конфигураторе и подсчёта числа обращений к СУБД. Оптимизируют не «весь код», а конкретную строку или запрос, которые в замере набрали больше всего времени.
Запрос всегда быстрее обработки в цикле?
Почти всегда, когда речь про выборку и агрегацию данных из базы. Один запрос обрабатывается СУБД оптимально, а перебор объектов в коде порождает множество отдельных обращений. Исключение — сложная бизнес-логика над уже полученными в память данными.
Почему отчёт стал медленным, хотя запрос «правильный»?
Частые причины: соединение с подзапросом вместо временной таблицы, непроиндексированная временная таблица, отбор во внешнем ГДЕ вместо параметров виртуальной таблицы, отсутствие индекса по полю отбора. Проверьте план через технологический журнал.
Индексировать все реквизиты подряд — это ускорит базу?
Нет. Лишние индексы замедляют запись (каждый индекс обновляется при изменении данных) и занимают место. Индексируйте только те поля, по которым реально идёт отбор в запросах и динамических списках.
Пользователи жалуются на «зависания», а замер показывает быстрый код. Где смотреть?
Скорее всего, это ожидание на блокировках, а не скорость алгоритма. Смотрите управляемые блокировки, длительность транзакций и взаимоблокировки в технологическом журнале — проблема в конкурентном доступе, а не в самом коде.