Обход строк и колонок ТаблицыЗначений во встроенном языке 1С без лишних тормозов

· 9 мин

Содержание

Перебрать таблицу значений в 1С проще всего циклом Для Каждого Строка Из Таблица Цикл … КонецЦикла: на каждой итерации переменная Строка получает очередную строку, а к полям обращаются через точку — Строка.Сумма. Если нужен номер строки или строки придётся удалять, берите цикл по индексу Для Индекс = 0 По Таблица.Количество() - 1.

Проблемы начинаются не с синтаксиса, а с объёма. Таблица на 50 000 строк и вложенный цикл поиска внутри — и обработка, которая на тестовой базе отрабатывала за секунду, на рабочей висит десять минут. Ниже — как обходить ТаблицуЗначений правильно, где перебор вообще не нужен и какие ошибки чаще всего ломают код.

Как устроена ТаблицаЗначений и где она доступна

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

Объект состоит из трёх частей:

  • 📋 Колонки — коллекция Таблица.Колонки. У каждой колонки есть имя, заголовок и тип значения (ОписаниеТипов).
  • 📄 Строки — элементы типа СтрокаТаблицыЗначений. Сама таблица и есть коллекция строк, отдельного свойства «Строки» у неё нет.
  • ⚡ Индексы — коллекция Таблица.Индексы. Ускоряют поиск по выбранным колонкам.

Создание таблицы вручную:

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

НоваяСтрока = Таблица.Добавить();
НоваяСтрока.Количество = 3;
НоваяСтрока.Сумма = 1500;

Чаще таблица приходит готовой: Запрос.Выполнить().Выгрузить() или ТабличнаяЧасть.Выгрузить() у документа.

ТаблицаЗначений работает на сервере и в толстом клиенте. В управляемой форме на тонком клиенте её нет: реквизит формы типа «ТаблицаЗначений» на клиенте виден как ДанныеФормыКоллекция. Чтобы получить настоящую таблицу, вызовите на сервере РеквизитФормыВЗначение("ИмяРеквизита"), а вернуть изменения — через ЗначениеВРеквизитФормы.

Отсюда практическое правило: тяжёлый перебор делайте в серверной процедуре с директивой &НаСервере или в общем модуле, а на клиент передавайте только итог.

Перебор строк таблицы значений циклом Для Каждого

Самый короткий и читаемый вариант. Подходит для 90% задач, где строки только читаются или меняются их поля.

ИтогоСумма = 0;
Для Каждого Строка Из Таблица Цикл
    Если Строка.Количество = 0 Тогда
        Продолжить;
    КонецЕсли;
    Строка.Сумма = Строка.Количество * Строка.Цена;
    ИтогоСумма = ИтогоСумма + Строка.Сумма;
КонецЦикла;

Что здесь важно:

  • ✏️ Строка — ссылка на строку таблицы, а не копия. Присваивание Строка.Сумма = … меняет саму таблицу.
  • ⏭️ Продолжить пропускает текущую итерацию, Прервать выходит из цикла целиком.
  • 🔢 Номер текущей строки при необходимости даёт Таблица.Индекс(Строка), но звать его на каждой итерации расточительно: если номер нужен постоянно, переходите на цикл по индексу.

Для простой суммы по колонке цикл не нужен: Таблица.Итог("Сумма") вернёт сумму числовой колонки одной строкой кода.

Главное ограничение: в цикле Для Каждого нельзя менять состав коллекции. Удаление строк внутри Для Каждого приводит к пропуску соседних строк — после удаления следующая строка сдвигается на место текущей, и цикл её перешагивает. Ошибки при этом нет, и именно поэтому баг долго живёт в коде.

Цикл по индексу: Для … По … и обратный обход

Строки таблицы нумеруются с нуля. Последняя строка имеет индекс Таблица.Количество() - 1. К строке обращаются через квадратные скобки или метод Получить():

Для Индекс = 0 По Таблица.Количество() - 1 Цикл
    Строка = Таблица[Индекс];   // то же, что Таблица.Получить(Индекс)
    Сообщить(СтрШаблон("%1: %2", Индекс + 1, Строка.Номенклатура));
КонецЦикла;

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

Обход с шагом и сравнение соседних строк

У цикла Для … По шаг всегда равен единице. Если нужна каждая вторая или каждая десятая строка, используйте Пока со своим счётчиком:

Шаг = 10;
Индекс = 0;
Пока Индекс < Таблица.Количество() Цикл
    Строка = Таблица[Индекс];
    // обработка каждой десятой строки
    Индекс = Индекс + Шаг;
КонецЦикла;

Сравнение с предыдущей строкой — типичная задача после сортировки, например, чтобы найти смену контрагента и вставить промежуточный итог. Начинайте счётчик с единицы, чтобы Таблица[Индекс - 1] не вышел за границу:

Таблица.Сортировать("Контрагент");
Для Индекс = 1 По Таблица.Количество() - 1 Цикл
    Если Таблица[Индекс].Контрагент <> Таблица[Индекс - 1].Контрагент Тогда
        // здесь начинается группа следующего контрагента
    КонецЕсли;
КонецЦикла;

Удаление строк при переборе

Удалять безопасно только с конца. Тогда удаление строки не сдвигает те, что ещё не проверены:

ВсегоСтрок = Таблица.Количество();
Для Сч = 1 По ВсегоСтрок Цикл
    Строка = Таблица[ВсегоСтрок - Сч];
    Если Строка.Количество = 0 Тогда
        Таблица.Удалить(Строка);
    КонецЕсли;
КонецЦикла;

Индекс считается от зафиксированного до цикла ВсегоСтрок. При первом проходе берётся строка ВсегоСтрок - 1, при последнем — строка с индексом 0. Удаление уже пройденных хвостовых строк на оставшиеся индексы не влияет.

Альтернатива без обратного цикла — сначала собрать строки в массив, потом удалить:

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

Здесь цикл идёт по массиву, а не по таблице, поэтому пропусков нет.

Когда какой цикл брать

ЗадачаЦиклПочему
Прочитать или изменить поляДля КаждогоКороче, нет риска ошибиться с границей
Нужен номер строкиДля … ПоИндекс уже есть в счётчике
Сравнить строку с соседнейДля … ПоДоступ к Таблица[Индекс - 1]
Удалить часть строкОбратный Для … По или массив из НайтиСтрокиНет пропусков
Обработать каждую N-ю строкуПока с ручным счётчикомУ Для … По шаг всегда равен 1

Обход колонок и доступ к полям строки

Иногда имена колонок заранее неизвестны — например, таблица пришла из универсальной выгрузки. Тогда перебирают коллекцию Колонки:

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

К полю строки можно обратиться тремя способами:

  1. Строка.Сумма — через точку, имя известно при написании кода.
  2. Строка["Сумма"] — по имени в строке, когда имя колонки хранится в переменной.
  3. Строка[2] — по индексу колонки, если порядок колонок фиксирован.

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

Как не делать лишнюю работу на каждой строке

  • 🧮 Выносите из цикла всё, что не зависит от строки. Получение константы, ставки НДС, даты, реквизита организации — до цикла, в переменную.
  • 🚫 Не обращайтесь к реквизитам через точку от ссылки. Строка.Номенклатура.ЕдиницаИзмерения внутри цикла — это неявное чтение объекта из базы на каждой строке. Нужные реквизиты заберите в исходный запрос отдельными колонками.
  • 📦 Выгружайте колонку целиком, если нужен только список значений. Таблица.ВыгрузитьКолонку("Номенклатура") вернёт массив без ручного цикла. Обратная операция — ЗагрузитьКолонку(Массив, "Колонка").
  • 🧹 Заполняйте колонку одним вызовом. Таблица.ЗаполнитьЗначения(0, "Сумма") обнулит колонку быстрее и понятнее, чем цикл.

Поиск и отбор строк таблицы значений без полного перебора

Главная причина медленных обработок — вложенные циклы. Для каждой строки первой таблицы перебирается вся вторая: 10 000 × 10 000 — уже сто миллионов сравнений. Встроенные методы и индексы снимают эту проблему.

НайтиСтроки и Найти

НайтиСтроки(Структура) возвращает массив строк, у которых значения колонок совпадают с отбором:

Отбор = Новый Структура("Номенклатура, Склад", ТоварСсылка, СкладСсылка);
Найденные = Таблица.НайтиСтроки(Отбор);
Если Найденные.Количество() > 0 Тогда
    Найденные[0].Количество = Найденные[0].Количество + 1;
КонецЕсли;

Найденные строки — это строки самой таблицы, их изменение меняет исходник.

Найти(Значение, "Колонка1, Колонка2") ищет первую строку, где значение встречается в любой из перечисленных колонок, и возвращает строку либо Неопределено. Удобен для проверки «есть ли такой товар вообще».

Индексы

Если поиск по одним и тем же колонкам повторяется в цикле, добавьте индекс один раз перед циклом:

Таблица.Индексы.Добавить("Номенклатура, Склад");

После этого НайтиСтроки с отбором по этим колонкам перестаёт просматривать таблицу целиком. Индекс стоит памяти и пересчитывается при добавлении строк, поэтому ставьте его после заполнения таблицы и только под реальные отборы. На таблице в 30 строк индекс не нужен.

Соответствие вместо поиска

Когда ключ один и уникален, удобнее построить Соответствие одним проходом:

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

Для Каждого Строка Из ТаблицаТоваров Цикл
    Цена = Цены.Получить(Строка.Номенклатура);
    Строка.Цена = ?(Цена = Неопределено, 0, Цена);
КонецЦикла;

Вместо вложенного цикла — два последовательных прохода.

Готовые операции над всей таблицей

НужноМетодВместо
Свернуть дубли и просуммироватьСвернуть("Номенклатура", "Количество, Сумма")Цикл с поиском и сложением
ОтсортироватьСортировать("Сумма Убыв, Номенклатура")Ручная сортировка
Получить подмножество строкСкопировать(Отбор, "Колонки")Цикл с добавлением в новую таблицу
Сумма по колонкеИтог("Сумма")Цикл-накопитель
Уникальные значенияСкопировать(, "Колонка") и Свернуть("Колонка")Цикл с проверкой на дубль

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

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

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

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

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

Колонки, которые используются в запросе, должны иметь заданный тип. Если колонку создали через Колонки.Добавить("Имя") без ОписаниеТипов, запрос выдаст ошибку о недопустимом типе. Указывайте тип при создании колонки.

Ошибки, на которых спотыкаются при обходе ТаблицыЗначений

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

Ещё два антипаттерна, которые не дают ошибки, но съедают ресурсы:

  • 🔁 Запрос внутри цикла по строкам. Тысяча строк — тысяча обращений к базе. Соберите все ключи массивом через ВыгрузитьКолонку и выполните один запрос с условием В (&Массив).
  • 🧱 Добавление строк по одной в таблицу, которую можно получить запросом. Если данные лежат в базе, сразу выгружайте результат запроса, а не заполняйте таблицу вручную из выборки.

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

Чем ТаблицаЗначений отличается от табличной части документа?
Табличная часть хранится в базе вместе с объектом, ТаблицаЗначений живёт только в памяти. Табличную часть переводят в таблицу методом Выгрузить(), обратно — Загрузить().
Можно ли перебирать результат запроса без выгрузки в таблицу?
Да, через Выбрать() и цикл Пока Выборка.Следующий(). Для однократного прохода по большому результату это экономнее по памяти, чем Выгрузить().
Как проверить, что таблица пустая?
Сравните Таблица.Количество() с нулём. Перебирать строки для этого не нужно.
Как получить первую и последнюю строку?
Первая — Таблица[0], последняя — Таблица[Таблица.Количество() - 1]. Перед обращением проверьте, что строк больше нуля, иначе будет ошибка индекса.
Можно ли добавить колонку в уже заполненную таблицу?
Можно: Колонки.Добавить() создаст колонку у всех строк, значения в ней будут пустыми до заполнения.