1C-Code → Основы 1С → Что тормозит и почему
Девять причин, по которым код работает минуты вместо секунд. Почти все видны глазами, если знать, куда смотреть.
Глава 28 из 36 · чтение примерно 26 минут
«Обработка идёт сорок минут» почти никогда не значит, что данных много. Обычно это одна из пяти ошибок, и все они опознаются по виду кода, без всяких замеров.
Самая частая и самая дорогая. Цикл по тысяче строк, внутри — запрос или поиск. Тысяча походов в базу вместо одного.
Опознаётся мгновенно: Новый Запрос или НайтиПо… внутри Для Каждого.
Лечится всегда одинаково: вынести запрос из цикла и передать список параметром. Один запрос по тысяче элементов вместо тысячи запросов по одному.
Про это была целая глава. Виртуальная таблица без параметров собирается целиком, а потом лишнее выбрасывается. Опознаётся так: .Остатки() с пустыми скобками и условие в ГДЕ ниже.
Соединять таблицу с вложенным запросом — значит просить СУБД выполнить его и не дать ей возможности использовать индексы по-человечески.
Лечится временными таблицами: положить промежуточный результат в ПОМЕСТИТЬ, проиндексировать ИНДЕКСИРОВАТЬ ПО и соединять уже с ней.
Отбор по полю, которое не входит ни в один индекс, заставляет читать таблицу целиком.
В 1С индексы появляются не сами: измерения регистров индексируются, реквизиты справочников — только если поставить свойство «Индексировать». Отбор по неиндексированному реквизиту на справочнике в сто тысяч элементов — это сто тысяч прочитанных строк.
5. Обращение к реквизиту через точку в цикле
Строка.Номенклатура.Артикул внутри цикла — это отдельный поход в базу на каждой итерации. Ссылка знает только идентификатор; за артикулом платформа идёт в справочник.
В запросе такая точка безобидна — она превращается в соединение. В коде она стоит дорого.
Лечится тем же: получить нужные поля запросом сразу, а не добирать их по одному.
Индекс — это оглавление: по нему база находит нужные строки, не читая таблицу целиком. Есть условия, при которых оглавление бесполезно, и их узнают по виду.
ПОДОБНО "%текст%" — поиск подстроки где угодно внутри. Оглавление отсортировано по началу строки, а начало здесь неизвестно: приходится читать всё. А вот ПОДОБНО "текст%" — по началу — индексом пользуется.
Функция вокруг поля. ГДЕ ГОД(Дата) = 2026 — база вынуждена посчитать год у каждой строки. То же условие как Дата МЕЖДУ &Начало И &Конец ложится на индекс.
НЕ и ИЛИ по разным полям. «Всё, кроме» — это почти вся таблица, и читать её целиком дешевле, чем ходить по оглавлению.
| Что | Индекс есть | Что из этого следует |
|---|---|---|
| Ссылка у любого объекта | всегда | поиск и соединение по ссылке — дёшево |
| Код и наименование справочника | всегда | НайтиПоКоду и НайтиПоНаименованию быстрые |
| Измерения регистра | всегда, в порядке объявления | отбор по ПЕРВЫМ измерениям работает, по последним — хуже |
| Реквизит справочника или документа | только если поставить «Индексировать» | иначе отбор по нему читает таблицу целиком |
| Реквизит регистра | нет | реквизит не для отбора — для отбора заводят измерение |
| Поле временной таблицы | только если написать ИНДЕКСИРОВАТЬ ПО | без этого соединение с ней перебирает всё |
Порядок измерений регистра — это не оформление
Индекс по измерениям строится в том порядке, в каком они объявлены. Отбор по первому измерению использует его целиком, отбор по одному последнему — почти нет.
Поэтому измерения ставят по убыванию «как часто по нему отбирают»: обычно первым идёт то, что режет данные сильнее — организация, склад, номенклатура.
Поменять порядок потом можно, и это дешёвая правка в конфигураторе, — но заметить проблему без замера нельзя: отчёт просто «немного медленный».
Сложный запрос с тремя соединениями, вложенным подзапросом и группировкой база оптимизирует хуже, чем два простых. Особенно когда внутри есть виртуальные таблицы: каждая — сама по себе целый запрос.
Инструмент для этого — пакетный запрос с временными таблицами. Сначала положить во временную таблицу то, что уже отобрано и свёрнуто, проиндексировать её — и только потом соединять.
Выигрыш здесь не в красоте: во временной таблице лежит мало строк, и база точно знает, сколько именно, — а про подзапрос она этого не знает и выбирает план наугад.
ВЫБРАТЬ
Товар,
СУММА(Количество) КАК Количество
ПОМЕСТИТЬ ВТ_Строки
ИЗ
Документ.РеализацияТоваров.Товары
СГРУППИРОВАТЬ ПО
Товар
ИНДЕКСИРОВАТЬ ПО
Товар
;
ВЫБРАТЬ … ИЗ ВТ_Строки КАК С ЛЕВОЕ СОЕДИНЕНИЕ …
| Часть | Что означает |
|---|---|
ПОМЕСТИТЬ ВТ_Строки | результат ложится во временную таблицу, а не отдаётся наружу |
СГРУППИРОВАТЬ ПО | сворачиваем ДО соединения — именно это и убирает размножение строк |
ИНДЕКСИРОВАТЬ ПО | без этой строки соединение с временной таблицей перебирает её целиком |
точка с запятой | разделитель запросов пакета; выполняются они по очереди, в одной транзакции |
Индексировать по тому, по чему соединяют
Правило для ИНДЕКСИРОВАТЬ ПО простое: перечисляют поля, по которым эту временную таблицу будут соединять или отбирать дальше.
Индексировать всё подряд смысла нет: построение индекса само по себе работа, и на таблице в десять строк она дороже перебора.
Здесь тот же принцип, что и везде в этой главе: на трёх строках разницы не видно, а решение принимается именно на трёх строках.
«Долго» бывает двух разных видов, и лечатся они по-разному.
Много работы — база честно читает миллион строк. Видно в замере: время распределено по запросу.
Ожидание на блокировке — база не делает ничего, она ждёт, пока чужая транзакция отпустит данные. В замере это выглядит как «один запрос выполнялся сорок секунд», хотя сам по себе он мгновенный.
Отличить просто: то, что ждёт, ведёт себя по-разному в разное время дня. Отчёт, который утром считается за секунду, а в конце месяца — за минуту, почти наверняка стоит в очереди, а не считает.
Долгая транзакция мешает всем
Обратная сторона: ваша обработка, открывшая транзакцию на десять минут, держит блокировки все десять минут. Остальные в это время «тормозят», и виноватым будет выглядеть их код.
Отсюда два правила. Не держать транзакцию дольше необходимого — обрабатывать порциями, фиксируя каждую. Не обращаться к внешним системам внутри транзакции — чужой сервер может отвечать сколько угодно.
Когда код ждёт чужую блокировку, это выглядит одинаково — всё висит. А поломки за этим стоят две разные, и лечатся они по-разному.
Конфликт блокировок. Один сеанс держит данные, второй ждёт своей очереди и не дожидается: платформа ждёт ограниченное время и сдаётся с сообщением «Конфликт блокировок при выполнении транзакции превышает максимально допустимое время». Виноват здесь обычно не тот, кто получил ошибку, а тот, кто держал: длинная транзакция, внутри которой читают файл, печатают или ждут ответа чужого сервиса.
Взаимоблокировка. Двое ждут друг друга: первый занял товары и тянется к остаткам, второй занял остатки и тянется к товарам. Ждать можно вечно, поэтому база сама убивает одного из них. Сообщение так и звучит — «обнаружена взаимоблокировка».
Лечение у неё неожиданное: не «блокировать поменьше», а обращаться к таблицам в одном и том же порядке во всём коде. Если оба сначала берут товары, а потом остатки, ждать по кругу становится нечему.
| Признак | Конфликт блокировок | Взаимоблокировка |
|---|---|---|
| Сообщение | «превышает максимально допустимое время» | «обнаружена взаимоблокировка» |
| Кто виноват | тот, кто ДЕРЖАЛ долго | оба — из-за разного порядка |
| Когда вылезает | под нагрузкой, на длинных операциях | редко и «случайно», под нагрузкой |
| Чем лечится | короткая транзакция, ничего лишнего внутри | одинаковый порядок обращения к данным |
| Что НЕ помогает | увеличить таймаут — просто ждут дольше | повторить операцию — повторится и она |
Классическая задача: прочитать остаток, проверить, что хватает, и списать. Между чтением и записью успевает вклиниться второй сеанс — и оба спишут последнюю штуку.
Транзакция сама по себе от этого не спасает: чтение данных их не занимает. Нужна управляемая блокировка, поставленная ДО чтения: «эти строки регистра мои до конца транзакции».
Правило звучит так: если по прочитанному будете принимать решение о записи — блокируйте до чтения. Поставленная после, она защищает от того, чего уже не случится.
Оборотная сторона — не блокировать лишнего. Блокировка на весь регистр вместо трёх товаров превращает параллельную работу в очередь, и все ждут одного.
Все ошибки этой главы объединяет одно: на разработке их не видно. В тестовой базе двадцать документов, и любой запрос отвечает мгновенно.
Поэтому на боевую базу уезжает код, который никто не проверял на боевых объёмах, — и первым его тестировщиком оказывается бухгалтер в последний день квартала.
Что с этим делают: копию боевой базы с обезличенными данными, замер на ней, и простой вопрос к себе перед сдачей — «сколько строк тут будет через год».
Замер производительности в конфигураторе показывает, какие строки сколько заняли. Смотреть надо не на сумму, а на строку с наибольшим числом вызовов: обычно виновата она.
Технологический журнал и журнал запросов СУБД показывают, что на самом деле ушло в базу. Там же видно дописанное RLS условие, которого нет в тексте запроса.
Правило, которое экономит часы: сначала посчитайте, сколько раз. Медленный код почти всегда не сложный, а повторённый.
Замер производительности показывает свои строки кода. Этого хватает, пока тормозит код. Когда тормозит платформа или база, замер честно скажет «здесь три секунды» — и не скажет почему.
Технологический журнал — настройка платформы, после которой сервер пишет в файлы всё: длительные запросы, конфликты блокировок, взаимоблокировки с их участниками, обращения к СУБД. Это единственный способ узнать, кто именно кого держал полчаса назад. Включают его точечно и ненадолго: файлов он пишет много.
Журнал регистрации отвечает на другой вопрос — «что вообще происходило»: кто вошёл, что провёл, где получил ошибку. Первое место, куда смотрят по жалобе «вчера в обед всё встало».
Консоль кластера показывает живое: сколько сеансов, какие из них сейчас выполняют запрос, сколько памяти заняли рабочие процессы. Отсюда видно «висит один» против «плохо всем».
Сначала померить, потом чинить
Порядок, который экономит дни: воспроизвести, померить, починить одно, померить снова.
Без первого замера непонятно, что чинить: интуиция про производительность ошибается почти всегда, и оптимизируют обычно то место, которое занимает три процента времени.
Без второго непонятно, помогло ли. «Стало вроде быстрее» после пяти правок разом — это не результат, а ощущение, и в следующий раз придётся начинать сначала.
Повторное использование возвращаемых значений. У общего модуля есть такое свойство. Включили — и платформа запоминает результат функции для тех же параметров на время вызова или сеанса. Для справочных данных, которые спрашивают сто раз за операцию — курс валюты, настройка, реквизит организации, — это снимает сотню обращений к базе одной галочкой.
Цена: значение может устареть, и функция обязана быть честной — при одних и тех же параметрах возвращать одно и то же. Класть туда «текущие остатки» нельзя.
Меньше походов на сервер. Каждый вызов сервера с клиента — это дорога по сети, и на плохой связи она стоит больше, чем сам расчёт. Три вызова подряд, каждый за одним значением, надо собрать в один, возвращающий структуру.
И обратное: не тащить на клиент лишнее. Таблица на десять тысяч строк, которую потом показывают первыми двадцатью, едет по сети целиком.
Упражнение 1. Сколько походов в базу
Функция ПоходовВБазу(Строк, ВЦикле) считает походы в базу. Если ВЦикле истина — по походу на строку. Если ложь — один поход на все строки. Строк ноль — ноль походов в обоих случаях.
Решается в тренажёре: код запускается и проверяется сразу.
Упражнение 2. Кто виноват в ожидании
Функция ВиноватДержавший(Сообщение) получает текст ошибки: "Конфликт блокировок при выполнении транзакции" или "Обнаружена взаимоблокировка". Вернуть Истина, если виноват тот, кто ДЕРЖАЛ данные долго, и Ложь, если виноваты обе стороны разным порядком обращения к данным.
Решается в тренажёре: код запускается и проверяется сразу.
Девять причин, и первые пять видны прямо в тексте кода: запрос в цикле, отбор в ГДЕ вместо параметров, соединение с подзапросом, условие без индекса, обращение через точку в цикле. Остальные видны только под нагрузкой: один тяжёлый запрос вместо пакета, ожидание чужой блокировки, проверка на трёх строках вместо объёма.
Замер показывает не самую медленную строку, а самую повторяемую — искать надо её. Медленный код почти всегда не сложный, а повторённый. А когда замера мало, отвечают технологический журнал и консоль кластера: «висит один» и «плохо всем» лечатся по-разному.
Читать про код и писать код — разные умения. В тренажёре к этой главе идут упражнения: вы пишете решение, оно запускается на настоящем интерпретаторе 1С и проверяется сразу, без установки платформы.
← Виртуальные таблицы регистров
→ СКД: схема, настройки, ресурсы