Почему код 1С работает медленно и как это исправить
Содержание
Чаще всего код на 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 возникает, когда две транзакции берут одни и те же объекты в разном порядке и упираются друг в друга. Лекарство — всегда обращаться к регистрам и справочникам в одном и том же порядке во всём коде.
Клиент-серверное взаимодействие. Помните про границы &НаСервере / &НаКлиенте: каждый переход клиент-сервер передаёт данные по сети. Не гоняйте в цикле вызовы сервера из клиентского кода — соберите данные и вызовите сервер один раз. Тяжёлые вычисления выполняйте на сервере, клиенту отдавайте результат.
Советы про остановку рабочих процессов, перезапуск кластера или сброс сеансов к оптимизации кода не относятся — это администрирование, и делается оно только в согласованное окно, иначе у пользователей оборвутся сеансы. Код оптимизируют в разработке и выкатывают обновлением.
Частые вопросы
С чего начинать оптимизацию, если непонятно, что тормозит?
Запрос всегда быстрее обработки в цикле?
Почему отчёт стал медленным, хотя запрос «правильный»?
ГДЕ вместо параметров виртуальной таблицы, отсутствие индекса по полю отбора. Проверьте план через технологический журнал.