На INFOSTART TECH EVENT 2026 на стенде Инфостарт Обучения я проводил небольшой батл: участник получал обработку с намеренно медленным кодом и 15 минут на оптимизацию, а затем мы сравнивали его решение с моим и разбирали, почему один вариант работает быстрее другого.
Как появилась эта идея, я рассказал в предыдущей статье. А в этой статье перехожу от историй к практике: я собрал ошибки, которые встретились в исходных обработках и решениях участников, и разобрал способы их исправления.
В конце статьи вы сможете скачать учебные задания, чтобы самостоятельно проверить свои навыки, разобрать код с коллегами или использовать обработки для обучения начинающих разработчиков.

Медленный код в 1С почти всегда медленный по одним и тем же причинам: запрос в цикле, отбор не в том месте запроса и скрытые обращения к базе через точку. Код, который на тестовой базе отрабатывает за секунду, на рабочей базе с сотнями тысяч позиций выполняет сотни тысяч запросов вместо одного. Отсюда блокировки, нагрузка на СУБД и жалобы пользователей.
Оптимизация – это не украшательство, а условие того, что решение выдержит промышленную нагрузку. Ниже собраны шесть групп типовых ошибок, которые чаще всего мешают коду работать быстро.
Каждая ошибка разобрана по одной схеме: в чем ошибка, как правильно и почему это верно.
1. Запрос в цикле
Самая дорогая и самая частая ошибка: запрос выполняется на каждой итерации обхода справочника, документов или табличной части.
ВыборкаКонтрагентов = Справочники.Контрагенты.Выбрать(); Пока ВыборкаКонтрагентов.Следующий() Цикл Запрос = Новый Запрос; Запрос.Текст = "ВЫБРАТЬ | Обороты.СуммаОборот КАК Сумма |ИЗ | РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, , ) КАК Обороты |ГДЕ | Обороты.Контрагент = &Контрагент"; Запрос.УстановитьПараметр("Контрагент", ВыборкаКонтрагентов.Ссылка); ... КонецЦикла;
В чем ошибка. Каждый запрос – отдельное обращение сервера 1С к СУБД: разбор текста, построение плана, передача результата по сети. Тысяча контрагентов – тысяча обращений, и большинство из них вернет пустой результат, потому что у контрагента не было продаж. Вдобавок Справочники.Контрагенты.Выбрать() читает все реквизиты каждого элемента, хотя нужна только ссылка. Тот же вариант с проверкой строк табличной части – запрос остатков на каждую строку документа.
Как правильно. Один запрос, который сразу возвращает весь нужный набор:
Запрос = Новый Запрос; Запрос.Текст = "ВЫБРАТЬ | Обороты.Контрагент КАК Контрагент, | ПРЕДСТАВЛЕНИЕ(Обороты.Контрагент) КАК КонтрагентПредставление, | СУММА(Обороты.СуммаОборот) КАК Сумма |ИЗ | РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, , ) КАК Обороты |СГРУППИРОВАТЬ ПО | Обороты.Контрагент"; Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(НачалоПериода)); Запрос.УстановитьПараметр("КонецПериода", КонецДня(КонецПериода));
Для строк документа – выгрузить табличную часть во временную таблицу, свернуть по номенклатуре и соединить с остатками один раз.
Почему верно. СУБД обрабатывает множество строк за один проход, использует индексы и таблицы итогов регистра. В результат попадают только строки с данными, пустых обращений нет. Если нужен список «все элементы, даже без движений», источником становится справочник, а регистр присоединяется к нему левым соединением – но в том же одном запросе.
2. Скрытые обращения к базе
В коде Ссылка.Реквизит выглядит как обращение к полю, а на деле это чтение объекта из базы. Внутри цикла это тот же запрос в цикле, только невидимый.
2.1. ПолучитьОбъект() ради чтения пары полей
ДокОбъект = ВыборкаДокументов.Ссылка.ПолучитьОбъект(); Сообщить("Найден документ: " + ДокОбъект.Номер + " от " + ДокОбъект.Дата);
В чем ошибка. Объект для чтения данных не нужен вообще. ПолучитьОбъект() читает документ целиком, со всеми табличными частями, и создает объект в памяти – ради двух полей, которые уже есть в выборке.
Как правильно. Для чтения достаточно ссылки, а еще лучше – поля самой выборки или запроса: ВыборкаДокументов.Номер, ВыборкаДокументов.Дата. Обращение Ссылка.Номер тоже работает, но при первом обращении платформа все равно читает объект в кеш, поэтому в цикле поля берут из выборки. ПолучитьОбъект() нужен только тогда, когда объект будут изменять и записывать.
Почему верно. Читается одна строка шапки нужными колонками вместо всего объекта с табличными частями.
2.2. Разыменование в цикле и «.Ссылка» у ссылки
Сообщить(Выборка.Контрагент.Ссылка.Наименование); Сообщить(ВыборкаНоменклатуры.Ссылка.Артикул); ВызватьИсключение "Не хватает товара " + СтрокаТЧ.Номенклатура.Наименование;
В чем ошибка. Контрагент уже ссылка, .Ссылка к ней ничего не добавляет. Каждое .Наименование или .Артикул в коде – чтение элемента справочника. В выборке справочника реквизит доступен напрямую (ВыборкаНоменклатуры.Артикул), переход через ссылку заставляет читать элемент повторно.
Как правильно. Получать реквизиты в самом запросе:
|ВЫБРАТЬ | Обороты.Контрагент КАК Контрагент, | Обороты.Контрагент.Наименование КАК КонтрагентНаименование, | ПРЕДСТАВЛЕНИЕ(Обороты.Контрагент) КАК КонтрагентПредставление
Почему верно. Платформа превращает обращение через точку в запросе в одно соединение на стороне СУБД вместо N отдельных чтений. Для вывода пользователю лучше ПРЕДСТАВЛЕНИЕ(): оно учитывает настройки представления объекта.
2.3. Неявное приведение ссылки к строке
Сообщить("Товар: " + Строка.Номенклатура + " итог: " + Строка.Сумма);
В чем ошибка. При сложении ссылки со строкой платформа получает ее представление, то есть идет в базу. В коде этого не видно, а на каждой строке результата выполняется лишний запрос.
Как правильно. Выбрать ПРЕДСТАВЛЕНИЕ(Номенклатура) КАК НоменклатураПредставление в запросе и выводить готовую строку.
Почему верно. Все представления получены одним запросом вместе с данными.
3. Параметры виртуальных таблиц: отбор и даты
Виртуальная таблица регистра (Остатки, Обороты, СрезПоследних) – не готовая таблица, а подзапрос, который платформа строит по ее параметрам. Все, что передано в параметры, применяется до расчета. Все, что написано в ГДЕ, – после.
3.1. Отбор в ГДЕ вместо параметра «Условие»
|ИЗ | РегистрНакопления.Запасы.Остатки(, ) КАК Запасы |ГДЕ | Запасы.Номенклатура = &Номенклатура
В чем ошибка. Платформа сначала рассчитывает остатки по всем товарам и складам, а затем отбрасывает лишнее. Для регистра с миллионом записей это расчет миллиона строк ради одной.
Как правильно.
|ИЗ | РегистрНакопления.Запасы.Остатки(, Номенклатура = &Номенклатура) КАК Запасы
Если значений много, в параметр передается подзапрос к временной таблице: Номенклатура В (ВЫБРАТЬ Т.Номенклатура ИЗ ТабДок КАК Т).
Почему верно. Условие попадает внутрь расчета: платформа читает итоги и движения только по нужным значениям измерения, используя индексы. Правило простое: условия на измерения – в параметры виртуальной таблицы, условия на ресурсы (КоличествоОстаток > 0) – в ГДЕ или ИМЕЮЩИЕ, потому что ресурсы до расчета еще не известны.
3.2. Текущая дата в параметре Остатков
| РегистрНакопления.Запасы.Остатки(&Период) КАК Запасы ... Запрос.УстановитьПараметр("Период", ТекущаяДата());
В чем ошибка. С явной датой платформа обязана рассчитать остатки на момент: взять ближайший рассчитанный период итогов и добавить или вычесть движения. Без даты она берет готовые текущие итоги. Кроме того, ТекущаяДата() по стандартам не используется для работы с данными.
Как правильно. Для актуальных остатков – Остатки() без периода. Если дата все же нужна (например, отчет «на сегодня» с учетом часового пояса) – ТекущаяДатаСеанса().
Почему верно. Чтение текущих итогов – самый быстрый путь получения остатков. ТекущаяДатаСеанса() учитывает часовой пояс сеанса, а не компьютера сервера.
3.3. Конец периода без КонецДня
Запрос.УстановитьПараметр("КонецПериода", Дата("20260101"));
В чем ошибка. Дата из поля ввода – это начало суток, 00:00:00. Обороты, МЕЖДУ и Документы.Выбрать(Начало, Конец) включают границу, но все, что проведено в последний день после полуночи, в выборку не попадет.
Как правильно.
Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(ДатаНачала)); Запрос.УстановитьПараметр("КонецПериода", КонецДня(ДатаОкончания));
Почему верно. Граница периода охватывает последний день целиком. Для стандартного периода то же дает Период.ДатаОкончания, которая уже приводится к концу дня.
3.4. Дата вместо момента времени при контроле остатков
В чем ошибка. Для проверки остатков перед проведением в параметр передают Документ.Дата. Остатки на дату – это состояние на начало этой секунды: не учтены документы, проведенные в ту же секунду раньше текущего.
Как правильно.
Запрос.УстановитьПараметр("Граница", Новый Граница(ДокументОбъект.МоментВремени(), ВидГраницы.Исключая));
Почему верно. Момент времени включает и дату, и ссылку на документ, поэтому остатки считаются точно до текущего документа, без его собственных движений.
3.5. Срез последних без отбора по измерениям
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(, ) КАК Цены |ГДЕ | Цены.Номенклатура = &Номенклатура
В чем ошибка. Срез возвращает последнюю запись по каждой комбинации всех измерений регистра. Если есть измерение «Вид цены», на один товар приходится несколько строк: код возьмет первую попавшуюся или выведет товар несколько раз.
Как правильно.
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, | ВидЦен = &ВидЦен И Номенклатура В (&СписокНоменклатуры)) КАК Цены
Почему верно. Отбор в параметрах одновременно ускоряет расчет среза и делает результат однозначным: одна цена на товар.
На батле было хорошо видно: знать синтаксис запроса недостаточно. Важно понимать, на каком этапе отбираются данные и что платформа делает с виртуальной таблицей. На моем курсе по языку запросов мы разбираем виртуальные и временные таблицы, работу с результатом и способы анализа производительности. Это помогает не просто запомнить, куда перенести условие, а самостоятельно находить причину медленного запроса.
4. Обработка данных в коде вместо запроса
Данные целиком передаются на сервер приложений, а суммирование, группировка и отбор выполняются циклом на встроенном языке.
4.1. Суммирование в цикле
Пока Выборка.Следующий() Цикл ОбщаяСумма = ОбщаяСумма + Выборка.Сумма; КонецЦикла;
В чем ошибка. По сети передаются тысячи строк ради одного числа.
Как правильно.
|ВЫБРАТЬ | ЕСТЬNULL(СУММА(Запасы.СуммаОстаток), 0) КАК ОбщаяСумма |ИЗ | РегистрНакопления.Запасы.Остатки() КАК Запасы
Почему верно. СУБД складывает значения там, где они хранятся, и возвращает одну строку. ЕСТЬNULL нужен, потому что агрегат по пустому набору возвращает NULL, а не ноль.
4.2. Группировка через ТаблицаЗначений.НайтиСтроки
Пока ВыборкаДокументов.Следующий() Цикл Для Каждого СтрокаТЧ Из ВыборкаДокументов.Запасы Цикл НайденныеСтроки = ТаблицаИтогов.НайтиСтроки( Новый Структура("Номенклатура", СтрокаТЧ.Номенклатура)); Если НайденныеСтроки.Количество() > 0 Тогда НайденныеСтроки[0].Сумма = НайденныеСтроки[0].Сумма + СтрокаТЧ.Сумма; Иначе ...
В чем ошибка. Три проблемы сразу. Обращение к табличной части через выборку дочитывает ее отдельным запросом на каждый документ. НайтиСтроки без индекса перебирает всю таблицу итогов на каждую строку, и время растет квадратично. Колонки без типов хранят значения как Произвольный.
Как правильно. Группировать в запросе к табличной части:
|ВЫБРАТЬ | Т.Номенклатура КАК Номенклатура, | ПРЕДСТАВЛЕНИЕ(Т.Номенклатура) КАК НоменклатураПредставление, | СУММА(Т.Сумма) КАК Сумма |ИЗ | Документ.РасходнаяНакладная.Запасы КАК Т |ГДЕ | Т.Ссылка.Дата МЕЖДУ &НачалоПериода И &КонецПериода | И Т.Ссылка.Проведен |СГРУППИРОВАТЬ ПО | Т.Номенклатура
Если данные уже в таблице значений и пришли не из базы – ТаблицаИтогов.Свернуть("Номенклатура", "Сумма"). Если поиск по таблице неизбежен – ТаблицаИтогов.Индексы.Добавить("Номенклатура") до цикла.
Почему верно. Одно обращение к базе, группировка средствами СУБД, никакого квадратичного поиска.
Если данные нужны для отчета, задача на этом не заканчивается: нужно еще правильно настроить группировки, ресурсы и параметры, с которыми будет работать пользователь. Эти вопросы я разбираю на курсе по СКД – от набора данных на основе запроса до работы с несколькими источниками и расшифровкой. В итоге можно увереннее проектировать отчет целиком, а не только его запрос.
4.3. Фильтр в коде вместо условия запроса
ВыборкаДокументов = Документы.РасходнаяНакладная.Выбрать(ДатаНачала, ДатаОкончания); Пока ВыборкаДокументов.Следующий() Цикл Если ВыборкаДокументов.Договор.ВидДоговора = Перечисления.ВидыДоговоров.СПокупателем Тогда
В чем ошибка. Читаются все документы за период, проверка выполняется в коде, а договор на каждой итерации читается отдельно через точку.
Как правильно. Условие переносится в запрос: ГДЕ Т.Договор.ВидДоговора = ЗНАЧЕНИЕ(Перечисление.ВидыДоговоров.СПокупателем).
Почему верно. Платформа превратит условие в соединение с таблицей договоров, и СУБД вернет только нужные документы. ЗНАЧЕНИЕ() подставляет константу прямо в текст запроса, без лишнего параметра.
4.4. Выгрузить() ради одного обхода
ТаблицаИтогов = Запрос.Выполнить().Выгрузить(); Для Каждого Строка Из ТаблицаИтогов Цикл
В чем ошибка. Весь результат целиком материализуется в памяти сервера в виде таблицы значений, хотя нужен только последовательный проход.
Как правильно. Выборка = Запрос.Выполнить().Выбрать(); Пока Выборка.Следующий() Цикл.
Почему верно. Выборка читает строки порциями, потребление памяти не зависит от размера результата. Выгрузить() оправдан, когда таблица нужна дальше: для поиска, передачи в другой запрос или в форму.
5. Конкатенация строк вместо СтрШаблон
Склейка строк через + – самая распространенная привычка, которую не замечают даже при ревью.
Сообщить("У контрагента " + Выборка.Контрагент.Наименование + " продажи на сумму: " + Выборка.Сумма); Сообщить("Время выполнения: " + ВремяВыполненияСек + " сек."); ВызватьИсключение "Не хватает товара " + СтрокаТЧ.Номенклатура.Наименование;
В чем ошибка.
Каждый
+создает новую строку в памяти. В цикле на тысячи итераций это заметные накладные расходы.Фраза разорвана на куски, ее невозможно перевести: стандарты 1С требуют, чтобы строка для
НСтр()была целым предложением.Типы приводятся неявно: число выводится с форматом по умолчанию, дата – с секундами, ссылка – запросом представления к базе (см. 2.3).
Легко потерять пробел на стыке и получить «на сумму:1 500».
Как правильно.
Шаблон = НСтр("ru = 'У контрагента %1 продажи на сумму: %2'"); Сообщить(СтрШаблон(Шаблон, Выборка.КонтрагентПредставление, Формат(Выборка.Сумма, "ЧДЦ=2"))); Сообщить(СтрШаблон(НСтр("ru = 'Время выполнения: %1 сек.'"), ВремяВыполненияСек)); ВызватьИсключение СтрШаблон(НСтр("ru = 'Не хватает товара %1'"), Выборка.НоменклатураПредставление);
Почему верно. Фраза читается и переводится целиком. Форматирование значений задано явно через Формат(). Строка собирается за один вызов. Если склеивать нужно много фрагментов в цикле (текст файла, длинный протокол), используют массив и СтрСоединить() – это следующая ступень после СтрШаблон.
Заодно о выводе: вместо Сообщить() стандарты рекомендуют ОбщегоНазначения.СообщитьПользователю() из БСП или Новый СообщениеПользователю – у них есть привязка к полю формы.
6. Другие частые ошибки оптимизации
Ошибки из этого раздела реже бросаются в глаза, но на больших базах и при большом числе пользователей дают не меньший эффект.
6.1. Серверный вызов в цикле на клиенте
&НаКлиенте Процедура ЗаполнитьЦены(Команда) Для Каждого Строка Из Объект.Товары Цикл Строка.Цена = ПолучитьЦенуНаСервере(Строка.Номенклатура); КонецЦикла; КонецПроцедуры
В чем ошибка. Каждый вызов сервера – это обмен по сети с передачей контекста. Сто строк – сто обращений, и на медленном канале пользователь ждет секундами. Если серверная процедура объявлена как &НаСервере, на сервер еще и передаются все данные формы.
Как правильно. Собрать параметры на клиенте и сделать один вызов, который вернет все сразу. Если данные формы не нужны – &НаСервереБезКонтекста.
&НаКлиенте Процедура ЗаполнитьЦены(Команда) ЗаполнитьЦеныНаСервере(); // один вызов КонецПроцедуры &НаСервере Процедура ЗаполнитьЦеныНаСервере() // один запрос цен по всем товарам табличной части, // затем заполнение строк из Соответствия Номенклатура → Цена КонецПроцедуры
Почему верно. Одно обращение к серверу вместо N, а цены по всем строкам получаются одним запросом. Если серверной процедуре не нужны данные формы, нужные значения передают параметром в &НаСервереБезКонтекста-функцию – тогда по сети идет только список товаров.
6.2. Обращение через точку к полю составного типа
|ВЫБРАТЬ | Движения.Регистратор.Контрагент КАК Контрагент
В чем ошибка. Если поле может ссылаться на десятки типов документов, платформа строит левое соединение с таблицей каждого из них. Запрос с одним полем превращается в запрос с десятками соединений.
Как правильно. Явно указать нужный тип: ВЫРАЗИТЬ(Движения.Регистратор КАК Документ.РасходнаяНакладная).Контрагент.
Почему верно. Выполняется одно соединение с одной таблицей.
6.3. Поиск в массиве внутри цикла
Для Каждого Элемент Из БольшойСписок Цикл Если ОбработанныеЭлементы.Найти(Элемент) = Неопределено Тогда ОбработанныеЭлементы.Добавить(Элемент);
В чем ошибка. Массив.Найти() перебирает массив целиком. Внутри цикла время растет квадратично: на 100 тысячах элементов это минуты.
Как правильно. Использовать Соответствие, где ключ – искомое значение: Если ОбработанныеЭлементы[Элемент] = Неопределено Тогда ОбработанныеЭлементы.Вставить(Элемент, Истина). Для таблицы значений – индекс по колонке поиска.
Почему верно. Поиск в соответствии по ключу выполняется за постоянное время, независимо от размера коллекции.
6.4. Длинные транзакции
В чем ошибка. В транзакцию попадает все подряд: обращение к внешнему сервису, формирование файла, ожидание ответа, обработка тысяч документов одним блоком. Пока транзакция открыта, удерживаются блокировки, и другие пользователи ждут или получают ошибку блокировки.
Как правильно. В транзакции – только чтение под блокировкой и запись, все остальное – до или после. Массовую обработку делить на порции, каждую в своей транзакции. Блокировать только то, что действительно изменяется, управляемой блокировкой с отбором по конкретным значениям, а не всю таблицу.
Почему верно. Чем короче транзакция и уже блокировка, тем меньше конфликтов между пользователями и меньше риск взаимоблокировок.
6.5. Лишние поля и тяжелые временные таблицы
В чем ошибка. В запросе выбираются поля «на всякий случай», временная таблица собирается без отбора и без индекса, а потом соединяется с другими таблицами по полю, по которому ее не индексировали.
Как правильно. Выбирать только нужные поля, отбирать данные до помещения во временную таблицу, добавлять ИНДЕКСИРОВАТЬ ПО для полей, по которым она будет соединяться с большими таблицами.
Почему верно. Меньше данных читается и передается, а соединение по индексу работает быстрее, чем полный перебор временной таблицы.
Чек-лист перед ревью
☐ Нет запросов,
ПолучитьОбъект()и обращений через точку внутри циклов.☐ Условия на измерения переданы в параметры виртуальной таблицы, а не в
ГДЕ.☐ Для актуальных остатков период не передается; для контроля документа –
Новый Граница(МоментВремени(), ВидГраницы.Исключая).☐ Конец периода –
КонецДня(), текущая дата –ТекущаяДатаСеанса().☐ Виртуальные таблицы не соединяются напрямую – сначала временная таблица с индексом.
☐ Виртуальные таблицы свернуты по ключу соединения, нет дублей по складам и видам цен.
☐ Агрегаты обернуты в
ЕСТЬNULL.☐ Представления ссылок получены в запросе через
ПРЕДСТАВЛЕНИЕ().☐ Строки собираются через
СтрШаблониНСтр, числа и даты – черезФормат().
Итог
Почти все ошибки оптимизации сводятся к одному: работа, которую СУБД сделает за один проход, переносится в циклы на встроенном языке. Запросы в цикле, отбор в ГДЕ вместо параметров виртуальной таблицы, суммирование и поиск в коде, обращения через точку – каждая из этих ошибок умножает число обращений к базе на количество строк.
Но убрать запрос из цикла – только половина работы. Скрытые обращения к базе через точку и неверно заданные параметры виртуальных таблиц способны свести выигрыш на нет. Отсюда три вывода:
Оптимизация – это перенос работы в запрос, а не просто удаление цикла. Скрытые обращения к базе (точка,
ПолучитьОбъект(), ссылка в строке) так же дороги, как явные.Параметры виртуальных таблиц – главный рычаг: условия на измерения и правильная граница периода дают выигрыш на порядки без новой логики.
Быстрая версия обязана давать тот же результат. Замер времени без сверки данных доказывает только скорость.
Материалы для тренировок
Все задачи и файлы обработок можно скачать здесь. Попробуйте оптимизировать их самостоятельно, предложите в качестве упражнения начинающим разработчикам в команде или используйте для совместного разбора на ревью. Только сравнивайте не одну скорость: после изменений результат работы обработки должен остаться прежним.
Если вам удобнее читать в телеграм, приглашаю подписаться на мой канал!
BrummeBear
норм