Частые ошибки оптимизации кода 1С

Телеграм канал автора статьи: https://t.me/v1c_director

Статья основана на активности стойки Инфостарт Обучение на конференции Infostart TechEvent 2026. Участники оптимизировали код учебных обработок, а мы собрали и сгруппировали ошибки, которые встретились и в исходных вариантах, и в оптимизированных версиях, подготовленных экспертами.

Медленный код в 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.
  • Представления ссылок получены в запросе через ПРЕДСТАВЛЕНИЕ().
  • Строки собираются через СтрШаблон и НСтр, числа и даты — через Формат().

Итог

Почти все ошибки оптимизации сводятся к одному: работа, которую СУБД сделает за один проход, переносится в циклы на встроенном языке. Запросы в цикле, отбор в ГДЕ вместо параметров виртуальной таблицы, суммирование и поиск в коде, обращения через точку — каждая из этих ошибок умножает число обращений к базе на количество строк.

Но убрать запрос из цикла — только половина работы. Скрытые обращения к базе через точку и неверно заданные параметры виртуальных таблиц способны свести выигрыш на нет. Отсюда три вывода:

  1. Оптимизация — это перенос работы в запрос, а не просто удаление цикла. Скрытые обращения к базе (точка, ПолучитьОбъект(), ссылка в строке) так же дороги, как явные.
  2. Параметры виртуальных таблиц — главный рычаг: условия на измерения и правильная граница периода дают выигрыш на порядки без новой логики.
  3. Быстрая версия обязана давать тот же результат. Замер времени без сверки данных доказывает только скорость.

Опубликовано

в

от