Перебор строк ТаблицыЗначений во встроенном языке 1С 8.3: какой способ выбрать под задачу и объём данных

· 9 мин

Содержание

Обойти таблицу значений в 1С проще всего циклом Для Каждого Стр Из ТЗ Цикл … КонецЦикла; — внутри цикла поля строки доступны как Стр.ИмяКолонки. Для удаления строк нужен цикл по индексу с конца, для поиска — НайтиСтроки() с индексом, а для сумм и группировок — Итог(), Свернуть() или запрос.

Типичная ситуация: обработка загрузила выписку на 40 тысяч строк, а заполнение контрагентов идёт десять минут. Почти всегда виноват не сам цикл, а то, что делается внутри него: вложенный поиск, обращение к реквизитам ссылок через точку, запись объектов по одному. Ниже — методы обхода и примеры кода для каждого случая. Всё сказанное относится к платформе 1С:Предприятие 8.3 и не зависит от конфигурации.

Перебор строк циклом: Для Каждого и по индексу

Цикл Для Каждого

Базовый способ обхода таблицы значений. Читается легко, ошибиться сложно.

Для Каждого СтрокаТЗ Из ТаблицаТоваров Цикл
	Если СтрокаТЗ.Количество = 0 Тогда
		Продолжить;
	КонецЕсли;
	СтрокаТЗ.Сумма = Окр(СтрокаТЗ.Количество * СтрокаТЗ.Цена, 2);
КонецЦикла;
  • ✅ Поля строки можно не только читать, но и менять: СтрокаТЗ — это ссылка на строку таблицы, а не копия.
  • ✅ Продолжить переходит к следующей строке, Прервать выходит из цикла.
  • ❌ Добавлять и удалять строки внутри такого цикла нельзя: при удалении строки сдвигаются и часть из них пропускается без всякой ошибки.

Цикл по индексу

Нужен, когда важен номер строки: сравнить с предыдущей, взять каждую десятую, пройти с конца.

Для Инд = 0 По ТаблицаТоваров.Количество() - 1 Цикл
	СтрокаТЗ = ТаблицаТоваров[Инд];
	Если Инд > 0 И СтрокаТЗ.Номенклатура = ТаблицаТоваров[Инд - 1].Номенклатура Тогда
		СтрокаТЗ.Повтор = Истина;
	КонецЕсли;
КонецЦикла;

Индексы начинаются с нуля. Последняя строка — Количество() - 1. Для пустой таблицы цикл 0 По -1 не выполнится ни разу, отдельная проверка не нужна.

Узнать номер строки, которую вы уже держите в руках, можно методом ТаблицаТоваров.Индекс(СтрокаТЗ). Саму таблицу по строке возвращает СтрокаТЗ.Владелец().

Обход колонок, когда их состав заранее неизвестен

Универсальные обработки (выгрузка в файл, сравнение таблиц) не знают имён колонок. Здесь нужен вложенный обход коллекции Колонки:

Для Каждого СтрокаТЗ Из ТЗ Цикл
	Для Каждого Колонка Из ТЗ.Колонки Цикл
		Значение = СтрокаТЗ[Колонка.Имя];
		// обработка значения
	КонецЦикла;
КонецЦикла;

Проверить, есть ли колонка, до обращения к ней: ТЗ.Колонки.Найти("Сумма") <> Неопределено.

Удаление и отбор строк при обходе таблицы значений

Удаление — главный источник ошибок. Есть два рабочих способа.

Способ 1. Обход с конца. Индекс уменьшается, удаление строки не задевает те, что ещё не проверены.

Инд = ТЗ.Количество() - 1;
Пока Инд >= 0 Цикл
	Если ТЗ[Инд].Количество = 0 Тогда
		ТЗ.Удалить(Инд);
	КонецЕсли;
	Инд = Инд - 1;
КонецЦикла;

Способ 2. Сначала найти, потом удалить. НайтиСтроки() возвращает массив строк, и удалять можно по нему: сама таблица в этот момент не перебирается.

Отбор = Новый Структура("Количество", 0);
Для Каждого СтрокаКУдалению Из ТЗ.НайтиСтроки(Отбор) Цикл
	ТЗ.Удалить(СтрокаКУдалению);
КонецЦикла;

Если нужно не удалить, а получить отдельную таблицу с частью строк, используйте ТЗ.Скопировать(Отбор). Вторым параметром можно перечислить колонки через запятую: ТЗ.Скопировать(Отбор, "Номенклатура, Количество").

НайтиСтроки() ищет только по точному совпадению значений. Условия «больше», «меньше», «содержит» так не задать — для них нужен цикл или запрос.

Ускорение обхода: индексы, Соответствие и выгрузка колонок в массивы

Когда таблица выросла до десятков тысяч строк, медленным становится не сам Для Каждого, а лишняя работа внутри.

Индекс для поиска

Без индекса каждый вызов НайтиСтроки() просматривает всю таблицу. Если он стоит внутри другого цикла, время растёт как произведение размеров двух таблиц.

ТЗ.Индексы.Добавить("Контрагент");
Для Каждого СтрокаВыписки Из Выписка Цикл
	Найденные = ТЗ.НайтиСтроки(Новый Структура("Контрагент", СтрокаВыписки.Контрагент));
	// ...
КонецЦикла;

Индекс добавляют один раз, до цикла. Состав колонок индекса должен совпадать с полями отбора.

Соответствие вместо вложенного цикла

Если по ключу нужна одна строка, быстрее один раз построить Соответствие:

ПоКонтрагенту = Новый Соответствие;
Для Каждого СтрокаТЗ Из ТЗ Цикл
	ПоКонтрагенту.Вставить(СтрокаТЗ.Контрагент, СтрокаТЗ);
КонецЦикла;

СтрокаТЗ = ПоКонтрагенту.Получить(Контрагент); // Неопределено, если нет

ВыгрузитьКолонку и ЗагрузитьКолонку

ВыгрузитьКолонку() возвращает массив значений одной колонки. Это удобно, когда расчёт касается одной-двух колонок, а таблица широкая, или когда массив нужно передать дальше — в параметр запроса, в отбор В (&Список).

Количества = ТЗ.ВыгрузитьКолонку("Количество");
Цены = ТЗ.ВыгрузитьКолонку("Цена");
Суммы = Новый Массив;
Для Инд = 0 По Количества.ВГраница() Цикл
	Суммы.Добавить(Окр(Количества[Инд] * Цены[Инд], 2));
КонецЦикла;
ТЗ.ЗагрузитьКолонку(Суммы, "Сумма");

Выигрыш от массивов скромнее, чем от индекса: он заметен на сотнях тысяч строк. Прежде чем переписывать рабочий код, замерьте время (об этом в разделе про отладку).

Готовые методы вместо ручного цикла

ЗадачаМетодВместо
Сумма по колонкеТЗ.Итог("Сумма")цикла с накоплением
ГруппировкаТЗ.Свернуть("Контрагент", "Сумма")цикла с поиском и сложением
УпорядочитьТЗ.Сортировать("Дата Убыв")ручной сортировки
Уникальные значенияТЗ.Скопировать(, "Контрагент") + Свернуть("Контрагент")цикла с проверкой дублей

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

Обращение к реквизитам через точку

Строка СтрокаТЗ.Контрагент.ИНН в цикле выглядит безобидно, но для каждой ссылки платформа читает объект из базы. На 40 тысячах строк это 40 тысяч обращений. Нужные реквизиты лучше получить одним запросом заранее — например, через запрос к таблице значений, о котором следующий раздел.

Запрос к таблице значений вместо цикла

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

Подготовка: типизированные колонки

Колонки таблицы должны иметь тип. Если создать колонку без ОписаниеТипов, запрос завершится ошибкой.

ТЗ = Новый ТаблицаЗначений;
ТЗ.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТЗ.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));

Таблица, полученная выгрузкой результата запроса или табличной части, уже типизирована.

Пример запроса

Таблицу сначала помещают во временную таблицу, а уже потом используют в соединениях:

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|	Т.Номенклатура КАК Номенклатура,
|	Т.Количество КАК Количество
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
|	&ТаблицаТоваров КАК Т
|;
|ВЫБРАТЬ
|	ВТ.Номенклатура КАК Номенклатура,
|	ВТ.Номенклатура.Артикул КАК Артикул,
|	СУММА(ВТ.Количество) КАК Количество
|ИЗ
|	ВТ_Товары КАК ВТ
|ГДЕ
|	ВТ.Количество > 0
|СГРУППИРОВАТЬ ПО
|	ВТ.Номенклатура,
|	ВТ.Номенклатура.Артикул";
Запрос.УстановитьПараметр("ТаблицаТоваров", ТЗ);
Результат = Запрос.Выполнить().Выгрузить();

Здесь за один проход получены артикулы всех товаров, отброшены нулевые строки и выполнена группировка. Обращение к Артикул через точку в запросе превращается в одно соединение, а не в отдельное чтение на каждую строку.

Если временная таблица нужна в нескольких запросах подряд, задайте Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц; и используйте его во всех запросах.

Выборка вместо выгрузки

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

Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
	// Выборка.Номенклатура, Выборка.Количество
КонецЦикла;

Сравнение методов обхода: что быстрее и когда применять

МетодКогда подходитОграничения
Для Каждогочтение и правка полейнельзя удалять и добавлять строки
Цикл по индексунужен номер строки, удаление с концалегко ошибиться с границей
НайтиСтроки() + индексмногократный поиск по равенствутолько точное совпадение
Соответствиепоиск одной строки по ключунужно построить заранее
ВыгрузитьКолонку()расчёт по 1–2 колонкам, передача списка в запросвыигрыш заметен на больших объёмах
Итог(), Свернуть()суммы и группировкиСвернуть() меняет таблицу
Запрос к ТЗсоединение с данными базы, сложные условияколонки должны быть типизированы, выполняется на сервере

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

Таблица значений на клиенте, блокировки и параллельная обработка

Клиент и сервер

Тип ТаблицаЗначений в тонком клиенте и веб-клиенте недоступен. Реквизит формы такого типа на клиенте превращается в ДанныеФормыКоллекция: её тоже можно обходить через Для Каждого, но методов НайтиСтроки() с индексами, Свернуть() и запроса там нет.

Тяжёлый обход переносите в серверную процедуру:

&НаСервере
Процедура ПересчитатьНаСервере()
	ТЗ = РеквизитФормыВЗначение("Товары");
	// обход, запрос, Свернуть
	ЗначениеВРеквизитФормы(ТЗ, "Товары");
КонецПроцедуры

Блокировки

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

Чтобы не ставить блокировку на каждую строку по отдельности, заблокируйте сразу все значения из таблицы:

НачатьТранзакцию();
Попытка
	Блокировка = Новый БлокировкаДанных;
	ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");
	ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
	ЭлементБлокировки.ИсточникДанных = ТЗ;
	ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
	Блокировка.Заблокировать();
	// чтение остатков и запись
	ЗафиксироватьТранзакцию();
Исключение
	ОтменитьТранзакцию();
	ВызватьИсключение;
КонецПопытки;

Имя регистра здесь — пример, подставьте свой. Управляемая блокировка работает только внутри транзакции. Держите транзакцию короткой: длинный цикл с записью внутри одной транзакции заставит других пользователей ждать.

Параллельная обработка фоновыми заданиями

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

Порция = ТЗ.СкопироватьКолонки();
Для Инд = НачалоПорции По КонецПорции Цикл
	ЗаполнитьЗначенияСвойств(Порция.Добавить(), ТЗ[Инд]);
КонецЦикла;

Параметры = Новый Массив;
Параметры.Добавить(Порция);
ФоновыеЗадания.Выполнить("ОбработкаТоваровСервер.ОбработатьПорцию", Параметры);
  • 📦 В задание передаётся копия порции. Изменения внутри задания исходную таблицу не меняют — результат возвращайте через временное хранилище.
  • 🔒 Порции не должны пересекаться по ключам, которые блокируются, иначе задания будут ждать друг друга или упадут на конфликте блокировок.
  • ⚙️ Процедура задания должна быть в серверном общем модуле, вызов идёт по полному имени Модуль.Процедура.

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

Ошибки при обходе таблицы значений и отладка

Диагностика: выберите симптом

Как найти медленное место

  1. Откройте конфигуратор и запустите отладку.
  2. Включите Отладка → Замер производительности.
  3. Выполните обработку и остановите замер.
  4. Отсортируйте строки по времени: сверху окажутся строки цикла, где тратится больше всего.

Если наверху — строка с точкой (СтрокаТЗ.Контрагент.ИНН) или НайтиСтроки() без индекса, лечение описано выше. Если Записать() — дело не в обходе, а в записи объектов.

В отладчике содержимое таблицы видно целиком: добавьте переменную в «Выражения» или наведите курсор. Для большой таблицы удобнее вычислить выражение ТЗ.Количество() или ТЗ.НайтиСтроки(...) прямо в окне выражений.

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

Можно ли изменить значение поля в строке во время обхода Для Каждого?
Да. Переменная цикла ссылается на саму строку таблицы, присваивание Стр.Сумма = 100 меняет таблицу. Нельзя только удалять и добавлять строки.
Как обойти таблицу значений в обратном порядке?
Через цикл Пока с индексом от Количество() - 1 до нуля или отсортировать копию методом Сортировать() по нужной колонке в порядке Убыв.
Чем таблица значений отличается от табличной части при обходе?
Синтаксис обхода одинаковый. Но табличная часть хранится в объекте, и её изменения попадут в базу только после записи объекта. Для сложной обработки табличную часть выгружают методом Выгрузить() в таблицу значений, а затем загружают обратно через Загрузить().
Как выйти из цикла, когда нужная строка найдена?
Оператором Прервать. Если нужна одна строка по точному значению, вместо цикла используйте ТЗ.Найти(Значение, "ИмяКолонки") — метод вернёт первую найденную строку или Неопределено.
Сколько строк выдержит таблица значений?
Ограничение — память процесса, где выполняется код. Сотни тысяч строк обычно обрабатываются нормально; если данные читаются из базы, для миллионов строк лучше выборка запроса порциями, а не выгрузка всего в таблицу.