Как в 1С удалить из одной таблицы значений строки, совпадающие со второй таблицей?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

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

В этой статье мы подробно разберем четыре наиболее эффективных и безопасных способа решения этой задачи. Мы проанализируем достоинства и недостатки каждого подхода, рассмотрим проблему смещения индексов, научимся ускорять поиск с помощью индексации и специального объекта Соответствие, а также разберем особенности реализации этой логики через запросы.

Способ 1. Безопасный обратный цикл (Обход таблицы с конца)

Начнем с разбора важного правила разработки в 1С: никогда не удаляйте строки из таблицы значений при обходе этой же таблицы в прямом цикле (например, с использованием конструкции Для каждого или стандартного счетчика от 0 до количества строк).

Давайте выясним причину, почему прямой цикл гарантированно приведет к ошибкам:

  1. При удалении текущей строки все последующие за ней строки автоматически сдвигаются на одну позицию назад (их индексы уменьшаются на единицу).
  2. Переменная цикла или счетчик продолжает увеличиваться по стандартному шагу.
  3. В результате система перескакивает через одну строку, которая шла сразу за удаленной, и она вообще не проверяется алгоритмом.
  4. В конце цикла программа попытается обратиться по индексу, который уже вышел за новые границы таблицы значений, что вызовет аварийное завершение работы процедуры.

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


// Предположим, у нас есть две таблицы значений: Таблица1 и Таблица2.
// Задача: удалить из Таблица1 все строки, ФизЛицо которых есть в Таблица2.

Индекс = Таблица1.Количество() - 1;
Пока Индекс >= 0 Цикл
    СтрокаТЗ1 = Таблица1[Индекс];
    
    // Ищем совпадение во второй таблице значений
    ПараметрыПоиска = Новый Структура("ФизЛицо", СтрокаТЗ1.ФизЛицо);
    НайденныеСтроки = Таблица2.НайтиСтроки(ПараметрыПоиска);
    
    // Если совпадение найдено, удаляем строку из первой таблицы
    Если НайденныеСтроки.Количество() > 0 Тогда
        Таблица1.Удалить(Индекс);
    КонецЕсли;
    
    Индекс = Индекс - 1;
КонецЦикла;

Этот способ прост для понимания и отлично работает на таблицах небольшого размера (до нескольких сотен строк). Однако при увеличении объема данных производительность будет падать из-за того, что на каждом шаге цикла метод НайтиСтроки() выполняет сканирование всей второй таблицы.

Способ 2. Оптимизация поиска через индексацию колонок

Если мы решили использовать встроенный метод поиска НайтиСтроки(), нам крайне важно оптимизировать скорость его работы. По умолчанию платформа выполняет полный последовательный перебор строк таблицы значений при каждом вызове поиска. Чтобы этого избежать, мы должны предварительно проиндексировать ключевую колонку.

Давайте добавим индекс для колонки "ФизЛицо" во второй таблице. Это заставит платформу построить в оперативной памяти специальный поисковый индекс, что ускорит выборку в сотни раз:


// Добавляем индекс по колонке поиска
Таблица2.Индексы.Добавить("ФизЛицо");

// Теперь поиск по этой колонке будет происходить практически мгновенно
Индекс = Таблица1.Количество() - 1;
Пока Индекс >= 0 Цикл
    СтрокаТЗ1 = Таблица1[Индекс];
    
    НайденныеСтроки = Таблица2.НайтиСтроки(Новый Структура("ФизЛицо", СтрокаТЗ1.ФизЛицо));
    Если НайденныеСтроки.Количество() > 0 Тогда
        Таблица1.Удалить(Индекс);
    КонецЕсли;
    
    Индекс = Индекс - 1;
КонецЦикла;

Обратите внимание: индексировать имеет смысл только ту таблицу, по которой осуществляется поиск (в нашем случае это Таблица2). Индексация требует небольших затрат времени на ее создание, но они с лихвой окупаются при количестве строк более 1000.

Способ 3. Сверхбыстрый алгоритм через объект "Соответствие"

Что делать, если таблицы содержат десятки тысяч строк, а использование СУБД (запросов) нежелательно или невозможно из-за отсутствия строгой типизации колонок? В этом случае лучшим решением по скорости работы будет использование объекта Соответствие.

Объект Соответствие в 1С работает по принципу хеш-таблицы. Скорость поиска элемента по ключу в нем составляет $O(1)$ — то есть поиск выполняется практически мгновенно вне зависимости от размера коллекции. Мы можем перенести все ключевые значения из второй таблицы в соответствие, а затем за один проход очистить первую таблицу.

Давайте подробно разберем этот высокопроизводительный алгоритм:


// Шаг 1. Создаем Соответствие для хранения уникальных ключей исключения
КлючиДляУдаления = Новый Соответствие;
Для каждого Стр Из Таблица2 Цикл
    // Записываем ключ (ФизЛицо). Значение может быть любым, например, Истина.
    КлючиДляУдаления.Вставить(Стр.ФизЛицо, Истина);
КонецЦикла;

// Шаг 2. Обходим первую таблицу с конца и проверяем наличие ключа в Соответствии
Индекс = Таблица1.Количество() - 1;
Пока Индекс >= 0 Цикл
    Если КлючиДляУдаления[Таблица1[Индекс].ФизЛицо] <> Неопределено Тогда
        // Если ключ найден в соответствии — удаляем строку
        Таблица1.Удалить(Индекс);
    КонецЕсли;
    
    Индекс = Индекс - 1;
КонецЦикла;

Этот подход работает со скоростью линейной сложности $O(N + M)$ и является абсолютным лидером по производительности среди всех чисто программных методов обхода коллекций в оперативной памяти 1С.

Способ 4. Использование временных таблиц в Запросе

Использование механизмов СУБД для обработки больших объемов данных является стандартом разработки под платформу 1С. Мы можем передать обе таблицы значений в запрос в качестве параметров, поместить их во временные таблицы, произвести левое соединение и выгрузить результат обратно в исходную таблицу.

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

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


// Обязательно задаем типы колонок при создании таблиц (если они не были заданы ранее)
МассивТипов = Новый Массив;
МассивТипов.Добавить(Тип("СправочникСсылка.ФизическиеЛица"));
ОписаниеТипаФизЛицо = Новый ОписаниеТипов(МассивТипов);

// Пример инициализации структуры таблиц
Таблица1 = Новый ТаблицаЗначений;
Таблица1.Колонки.Добавить("ФизЛицо", ОписаниеТипаФизЛицо);
// (Заполнение Таблица1 данными...)

Таблица2 = Новый ТаблицаЗначений;
Таблица2.Колонки.Добавить("ФизЛицо", ОписаниеТипаФизЛицо);
// (Заполнение Таблица2 данными...)

// Переходим к выполнению запроса
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;

// Передаем исходные таблицы в качестве параметров
Запрос.УстановитьПараметр("Таблица1", Таблица1);
Запрос.УстановитьПараметр("Таблица2", Таблица2);

Запрос.Текст = 
    "ВЫБРАТЬ
    |	ТЗ1.ФизЛицо КАК ФизЛицо
    |ПОМЕСТИТЬ ВТ_Таблица1
    |ИЗ
    |	&Таблица1 КАК ТЗ1
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ТЗ2.ФизЛицо КАК ФизЛицо
    |ПОМЕСТИТЬ ВТ_Таблица2
    |ИЗ
    |	&Таблица2 КАК ТЗ2
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ТЗ1.ФизЛицо КАК ФизЛицо
    |ИЗ
    |	ВТ_Таблица1 КАК ТЗ1
    |	ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Таблица2 КАК ТЗ2 ПО ТЗ1.ФизЛицо = ТЗ2.ФизЛицо
    |ГДЕ
    |	ТЗ2.ФизЛицо ЕСТЬ NULL";

// Выгружаем очищенный результат обратно в Таблицу1
Таблица1 = Запрос.Выполнить().Выгрузить();

Этот метод идеален для интеграции в существующие сложные механизмы проведения документов или построения отчетов, где данные уже обрабатываются на сервере. Однако помните о накладных расходах: на малых объемах данных (менее 500 строк) время, затраченное на сериализацию таблиц и передачу их СУБД, может оказаться больше, чем время выполнения простого алгоритма на клиенте через Соответствие.

Безопасность удаления строк по ссылкам через НайтиСтроки()

В процессе обсуждения этой темы на форумах часто возникает дискуссия: безопасно ли удалять строки непосредственно внутри цикла, который обходит массив результатов метода НайтиСтроки()? Давайте проанализируем эту ситуацию:


Для каждого Стр Из Таблица1 Цикл
    // Метод возвращает массив прямых ссылок на строки Таблицы2
    СтрокиДляУдаления = Таблица2. НайтиСтроки(Новый Структура("ФизЛицо", Стр.ФизЛицо));
    Для каждого НайденнаяСтрока Из СтрокиДляУдаления Цикл
        Таблица2.Удалить(НайденнаяСтрока);
    КонецЦикла;
КонецЦикла;

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

Резюме и рекомендации

Для выбора оптимального решения воспользуемся простыми правилами:

← На главную