Перебор строк ТаблицыЗначений во встроенном языке 1С 8.3: какой способ выбрать под задачу и объём данных
Содержание
Обойти таблицу значений в 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 колонкам, передача списка в запрос | выигрыш заметен на больших объёмах |
Итог(), Свернуть() | суммы и группировки | Свернуть() меняет таблицу |
| Запрос к ТЗ | соединение с данными базы, сложные условия | колонки должны быть типизированы, выполняется на сервере |
Правило выбора короткое: если внутри цикла есть обращение к базе или поиск — сначала уберите его (запросом, индексом или Соответствие), и только потом думайте о массивах.
Таблица значений на клиенте, блокировки и параллельная обработка
Клиент и сервер
Тип ТаблицаЗначений в тонком клиенте и веб-клиенте недоступен. Реквизит формы такого типа на клиенте превращается в ДанныеФормыКоллекция: её тоже можно обходить через Для Каждого, но методов НайтиСтроки() с индексами, Свернуть() и запроса там нет.
Тяжёлый обход переносите в серверную процедуру:
&НаСервере
Процедура ПересчитатьНаСервере()
ТЗ = РеквизитФормыВЗначение("Товары");
// обход, запрос, Свернуть
ЗначениеВРеквизитФормы(ТЗ, "Товары");
КонецПроцедуры
Блокировки
Сама таблица значений живёт в памяти, обход ничего в базе не блокирует. Блокировки появляются, когда внутри цикла вы читаете остатки и записываете документы или движения в транзакции.
Чтобы не ставить блокировку на каждую строку по отдельности, заблокируйте сразу все значения из таблицы:
НачатьТранзакцию();
Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.ИсточникДанных = ТЗ;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
Блокировка.Заблокировать();
// чтение остатков и запись
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Имя регистра здесь — пример, подставьте свой. Управляемая блокировка работает только внутри транзакции. Держите транзакцию короткой: длинный цикл с записью внутри одной транзакции заставит других пользователей ждать.
Параллельная обработка фоновыми заданиями
Один сеанс выполняет код последовательно. Ускорить обработку очень большой таблицы можно, разделив её на порции и запустив несколько фоновых заданий.
Порция = ТЗ.СкопироватьКолонки();
Для Инд = НачалоПорции По КонецПорции Цикл
ЗаполнитьЗначенияСвойств(Порция.Добавить(), ТЗ[Инд]);
КонецЦикла;
Параметры = Новый Массив;
Параметры.Добавить(Порция);
ФоновыеЗадания.Выполнить("ОбработкаТоваровСервер.ОбработатьПорцию", Параметры);
- 📦 В задание передаётся копия порции. Изменения внутри задания исходную таблицу не меняют — результат возвращайте через временное хранилище.
- 🔒 Порции не должны пересекаться по ключам, которые блокируются, иначе задания будут ждать друг друга или упадут на конфликте блокировок.
- ⚙️ Процедура задания должна быть в серверном общем модуле, вызов идёт по полному имени
Модуль.Процедура.
Ожидание завершения и сбор результатов в типовых конфигурациях удобнее делать через общий модуль ДлительныеОперации из БСП, а не писать с нуля.
Ошибки при обходе таблицы значений и отладка
Как найти медленное место
- Откройте конфигуратор и запустите отладку.
- Включите
Отладка → Замер производительности. - Выполните обработку и остановите замер.
- Отсортируйте строки по времени: сверху окажутся строки цикла, где тратится больше всего.
Если наверху — строка с точкой (СтрокаТЗ.Контрагент.ИНН) или НайтиСтроки() без индекса, лечение описано выше. Если Записать() — дело не в обходе, а в записи объектов.
В отладчике содержимое таблицы видно целиком: добавьте переменную в «Выражения» или наведите курсор. Для большой таблицы удобнее вычислить выражение ТЗ.Количество() или ТЗ.НайтиСтроки(...) прямо в окне выражений.
Частые вопросы
Можно ли изменить значение поля в строке во время обхода Для Каждого?
Стр.Сумма = 100 меняет таблицу. Нельзя только удалять и добавлять строки.Как обойти таблицу значений в обратном порядке?
Пока с индексом от Количество() - 1 до нуля или отсортировать копию методом Сортировать() по нужной колонке в порядке Убыв.Чем таблица значений отличается от табличной части при обходе?
Выгрузить() в таблицу значений, а затем загружают обратно через Загрузить().Как выйти из цикла, когда нужная строка найдена?
Прервать. Если нужна одна строка по точному значению, вместо цикла используйте ТЗ.Найти(Значение, "ИмяКолонки") — метод вернёт первую найденную строку или Неопределено.