При разработке на платформе 1С:Предприятие программистам регулярно приходится сталкиваться с необходимостью обработки табличных частей (ТЧ) или таблиц значений (ТЗ). Одной из наиболее частых подзадач является удаление строк по определенному условию — например, удаление дубликатов, очистка строк с совпадающими значениями текущей строки или, наоборот, сохранение только тех строк, которые соответствуют заданному значению.
В этой статье мы подробно разберем, как выполнять эти операции максимально быстро, проанализируем внутренние механизмы платформы 1С, влияющие на производительность, и выясним, почему популярные среди новичков решения (такие как запросы к временным таблицам для локальных операций) часто оказываются самыми медленными.
Прежде чем перейти к готовым алгоритмам, давайте разберем физику процесса. Когда мы вызываем метод Удалить() для строки табличной части в цикле, платформа выполняет физическое перестроение индексов коллекции в оперативной памяти. Если исходная таблица содержит десятки тысяч строк, а удалению подлежит значительная их часть, операция сдвига элементов и перестроения структуры данных выполняется на каждой итерации цикла. Это превращает алгоритм в крайне неэффективный с вычислительной сложностью O(N * M), где N — общее число строк, а M — количество удаляемых строк.
Кроме того, при обычном обходе коллекции "сверху вниз" (от нулевого индекса к последнему) удаление текущей строки приводит к смещению индексов всех последующих строк. В результате следующий шаг цикла "перепрыгивает" через одну строку, что ведет к логическим ошибкам и неполному удалению данных.
Давайте рассмотрим правильные, оптимизированные сценарии решения обеих задач.
---
Представим ситуацию: у нас есть строка номенклатуры в табличной части, и мы хотим мгновенно очистить всю таблицу, оставив только строки с этой же номенклатурой. Для этой задачи в платформе 1С существует встроенный метод, реализованный на уровне ядра (на языке C++), который работает в десятки раз быстрее любых ручных циклов. Это метод Скопировать() с применением структуры отбора.
Разберем по шагам самый быстрый и изящный алгоритм:
Скопировать(), который на уровне платформы мгновенно выделит память под новую таблицу и перенесет туда только подходящие строки.Посмотрим на практический пример реализации этого алгоритма:
// Получаем текущую строку табличной части
ТекСтрока = ЭлементыФормы.Товары.ТекущаяСтрока;
Если ТекСтрока = Неопределено Тогда
Возврат; // Строка не выбрана
КонецЕсли;
// Шаг 1: Выгружаем табличную часть в Таблицу Значений
ТабЗначений = Товары.Выгрузить();
// Шаг 2: Создаем структуру отбора по нужному значению колонки
ПараметрыОтбора = Новый Структура;
ПараметрыОтбора.Вставить("Номенклатура", ТекСтрока.Номенклатура);
// Шаг 3: Копируем только подходящие строки штатными средствами платформы
ОставшиесяСтроки = ТабЗначений.Скопировать(ПараметрыОтбора);
// Шаг 4: Очищаем исходную ТЧ и загружаем отфильтрованные данные обратно
Товары.Загрузить(ОставшиесяСтроки);
Почему это работает быстро? В данном случае интерпретатор встроенного языка 1С не тратит время на построчный обход элементов. Вся работа по фильтрации, выделению памяти и копированию структуры данных происходит в скомпилированном машинном коде платформы.
---
Данная задача сложнее тем, что штатный метод Скопировать() поддерживает отбор только по условию "Равно". Мы не можем напрямую передать в него условие "Не равно текущей строке". Тем не менее, у нас есть несколько высокоэффективных способов обойти это ограничение.
Чтобы полностью избежать поштучного удаления строк в цикле и последующего сдвига индексов, мы можем применить архитектурный трюк с добавлением временной колонки-маркера.
Рассмотрим этот алгоритм подробнее:
Булево с именем "Удалить".НайтиСтроки(), чтобы получить массив всех дублирующих строк.Истина в колонку "Удалить" для найденных строк (пропуская саму текущую строку).Ложь.Посмотрим на пример кода:
ТекСтрока = ЭлементыФормы.Товары.ТекущаяСтрока;
Если ТекСтрока = Неопределено Тогда
Возврат;
КонецЕсли;
// Шаг 1: Выгрузка данных в память
ТЗ = Товары.Выгрузить();
// Шаг 2: Добавляем временную колонку-маркер
ТЗ.Колонки.Добавить("Удалить", Новый ОписаниеТипов("Булево"));
// Шаг 3: Быстрый поиск дублирующих строк
ПараметрыПоиска = Новый Структура("Номенклатура", ТекСтрока.Номенклатура);
НайденныеСтроки = ТЗ.НайтиСтроки(ПараметрыПоиска);
// Шаг 4: Разметка строк на удаление
Для Каждого СтрокаТЗ Из НайденныеСтроки Цикл
// Исключаем саму текущую строку из удаления (сравниваем индекс или сам объект)
Если ТЗ.Индекс(СтрокаТЗ) <> Товары.Индекс(ТекСтрока) Тогда
СтрокаТЗ.Удалить = Истина;
КонецЕсли;
КонецЦикла;
// Шаг 5-6: Получаем чистую таблицу без удаляемых строк
РезультатТЗ = ТЗ.Скопировать(Новый Структура("Удалить", Ложь));
РезультатТЗ.Колонки.Удалить("Удалить");
// Загружаем оптимизированные данные обратно
Товары.Загрузить(РезультатТЗ);
В чем преимущество этого метода? Мы полностью избавились от тяжелой процедуры физического удаления строк из памяти на каждой итерации цикла. Мы совершили лишь один быстрый проход разметки и один системный вызов копирования структуры, что гарантирует стабильно высокую скорость работы даже на экстремально больших таблицах.
Если ваша табличная часть гарантированно содержит небольшое количество строк (например, до 1000-2000 строк), то тратить ресурсы на пересоздание таблиц в памяти может оказаться избыточным. В этом случае самым надежным способом является обход коллекции с конца к началу (обратный цикл).
Выясним причину, почему цикл должен быть именно обратным. При удалении строки по индексу с конца таблицы все вышестоящие элементы (индексы которых меньше текущего) остаются на своих местах. Индексы еще не обработанных строк не смещаются, благодаря чему цикл отрабатывает корректно, без пропусков строк и без опасности выйти за границы массива.
Посмотрим на пример реализации обратного цикла:
ТекСтрока = ЭлементыФормы.Товары.ТекущаяСтрока;
Если ТекСтрока = Неопределено Тогда
Возврат;
КонецЕсли;
ИндексТекущей = Товары.Индекс(ТекСтрока);
ЗначениеКолонки = ТекСтрока.Номенклатура;
// Обходим табличную часть с конца к началу
Для Индекс = Товары.Количество() - 1 По 0 Цикл
// Пропускаем саму текущую строку, ее удалять нельзя
Если Индекс = ИндексТекущей Тогда
Продолжить;
КонецЕсли;
СтрокаТЧ = Товары[Индекс];
// Проверяем совпадение значения
Если СтрокаТЧ.Номенклатура = ЗначениеКолонки Тогда
Товары.Удалить(Индекс);
// Корректируем индекс текущей строки, если удалили строку перед ней
Если Индекс < ИндексТекущей Тогда
ИндексТекущей = ИндексТекущей - 1;
КонецЕсли;
КонецЕсли;
КонецЦикла;
---
В процессе обсуждения на форумах часто всплывает идея использовать механизм запросов 1С для фильтрации или удаления дублей (например, через конструкцию ПОМЕСТИТЬ ВременнаяТаблица ИЗ &Таблица). Начинающим разработчикам кажется, что SQL-сервер сделает эту работу быстрее. Это глубокое заблуждение.
Проанализируем детально, что происходит "под капотом" платформы при передаче локальной таблицы в запрос:
ТаблицаЗначений.Важное правило оптимизации: Использование запроса оправдано только в том случае, если вам необходимо соединить (ЛЕВОЕ СОЕДИНЕНИЕ) данные вашей локальной таблицы с реальными таблицами базы данных (например, получить актуальные остатки или цены из регистров). Если вам нужно просто отфильтровать или удалить строки внутри одной таблицы — всегда делайте это в оперативной памяти сервера приложений методами платформы. Это работает в десятки и сотни раз быстрее.
---
Для того чтобы ваши алгоритмы летали, возьмите на вооружение два простых правила:
ТаблицуЗначений и планируете осуществлять поиск строк методом НайтиСтроки(), предварительно проиндексируйте колонку поиска. Это существенно сократит время поиска на больших массивах данных:
ТабЗначений.Индексы.Добавить("Номенклатура");