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

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

В практике разработки на платформе 1С:Предприятие часто возникает задача синхронизации данных. Классический пример: у нас есть внешняя ТаблицаЗначений (загруженная из Excel, XML или другого источника) и существующая ТабличнаяЧасть справочника или документа. Нам необходимо выяснить, каких строк из внешней таблицы еще нет в базе данных, чтобы добавить только их, не создавая дублей.

Рассмотрим подробно несколько способов решения этой задачи: от классических запросов до оптимизированных алгоритмов для больших объемов данных.

Способ 1. Использование запроса и левого соединения (Рекомендуемый)

Самый надежный и производительный способ решения данной задачи в 1С — использование языка запросов. Логика здесь строится на операции вычитания множеств через ЛЕВОЕ СОЕДИНЕНИЕ (LEFT JOIN).

Разберем алгоритм по шагам:

  1. Помещаем нашу ТаблицаЗначений во временную таблицу.
  2. Выполняем левое соединение этой временной таблицы с табличной частью справочника по ключевым полям поиска.
  3. В условии ГДЕ отбираем только те записи, где поля из табличной части равны NULL. Это и будет признаком того, что строка есть во входящей таблице, но отсутствует в базе.

Посмотрим на пример кода:


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

// 1. Помещаем ТЗ во временную таблицу
// ВАЖНО: Типизируйте колонки ТЗ перед помещением в запрос, если они не типизированы
Запрос.Текст = 
    "ВЫБРАТЬ
    |   ТЗ.Поле1 КАК Поле1,
    |   ТЗ.Поле2 КАК Поле2,
    |   ТЗ.Поле3 КАК Поле3
    |ПОМЕСТИТЬ ВТ_ВходящиеДанные
    |ИЗ
    |   &ТЗ КАК ТЗ";
    
// Обязательно создаем индекс для ускорения соединения, если строк много
// Это критически важно для производительности
Запрос.Текст = Запрос.Текст + 
    ";
    |ИНДЕКСИРОВАТЬ ПО
    |   Поле1,
    |   Поле2,
    |   Поле3";
    
Запрос.УстановитьПараметр("ТЗ", ТаблицаЗначений);
Запрос.Выполнить();

// 2. Основной запрос: ищем отсутствующие строки
Запрос.Текст = 
    "ВЫБРАТЬ
    |   Входящие.Поле1,
    |   Входящие.Поле2,
    |   Входящие.Поле3
    |ИЗ
    |   ВТ_ВходящиеДанные КАК Входящие
    |       ЛЕВОЕ СОЕДИНЕНИЕ Справочник.НашСправочник.ТЧТовары КАК Существующие
    |       ПО Входящие.Поле1 = Существующие.Поле1
    |       И Входящие.Поле2 = Существующие.Поле2
    |       И Входящие.Поле3 = Существующие.Поле3
    |       И (Существующие.Ссылка = &Ссылка)
    |ГДЕ
    |   Существующие.Ссылка ЕСТЬ NULL";

Запрос.УстановитьПараметр("Ссылка", СсылкаНаОбъект);

НовыеСтроки = Запрос.Выполнить().Выгрузить();

Преимущества: Этот метод корректно обрабатывает NULL значения в полях, работает быстро благодаря индексам и является стандартом "best practice".

Способ 2. Использование конструкции "НЕ В"

Иногда разработчики используют конструкцию НЕ В (NOT IN). Логика проста: выбрать все из ТЗ, где поля не входят в выборку из ТЧ.


ВЫБРАТЬ
    ТЗ.Поле1
ИЗ
    &ТЗ КАК ТЗ
ГДЕ
    (ТЗ.Поле1, ТЗ.Поле2) НЕ В 
        (ВЫБРАТЬ 
             ТЧ.Поле1, ТЧ.Поле2 
         ИЗ 
             Справочник.Спр.ТЧ КАК ТЧ 
         ГДЕ 
             ТЧ.Ссылка = &Ссылка)

Важное предостережение: Мы настоятельно не рекомендуем использовать этот способ, если в ваших полях могут встречаться значения NULL. В языке SQL (и 1С) любое сравнение с NULL внутри подзапроса NOT IN может привести к неочевидному результату (пустой выборке), даже если данные различаются. Используйте этот метод только если вы гарантированно уверены, что все поля заполнены.

Способ 3. Метод "ОБЪЕДИНИТЬ ВСЕ" с группировкой

Для высоконагруженных систем или очень больших таблиц иногда эффективнее использовать метод объединения с маркерами. Мы не используем соединения (JOIN), которые могут быть тяжелыми, а просто складываем две таблицы в одну кучу и считаем вхождения.

Алгоритм:

  1. Выбираем данные из ТЗ и добавляем поле-индикатор 1.
  2. Выбираем данные из ТЧ и добавляем поле-индикатор -1.
  3. Объединяем таблицы через ОБЪЕДИНИТЬ ВСЕ.
  4. Группируем по полям поиска и суммируем индикатор.
  5. Если СУММА(Индикатор) = 1, значит строка была только в ТЗ (новая). Если -1 — только в ТЧ (удаленная). Если 0 — есть и там, и там.

ВЫБРАТЬ
    ТЗ.Поле1,
    ТЗ.Поле2,
    1 КАК Индикатор
ПОМЕСТИТЬ ВТ_Общая
ИЗ &ТЗ КАК ТЗ

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    ТЧ.Поле1,
    ТЧ.Поле2,
    -1
ИЗ Справочник.Спр.ТЧ КАК ТЧ
ГДЕ ТЧ.Ссылка = &Ссылка
;

ВЫБРАТЬ
    Общая.Поле1,
    Общая.Поле2
ИЗ
    ВТ_Общая КАК Общая
СГРУППИРОВАТЬ ПО
    Общая.Поле1,
    Общая.Поле2
ИМЕЮЩИЕ
    СУММА(Общая.Индикатор) = 1

Способ 4. Программный поиск (без запросов)

Если по каким-то причинам использование запросов невозможно (например, данные еще не записаны в БД, и мы работаем с формой объекта), придется искать программно.

Неэффективный подход (наивный алгоритм)

Часто новички пишут двойной цикл или используют НайтиСтроки внутри цикла:


Для Каждого СтрТЗ Из ТаблицаЗначений Цикл
    Отбор = Новый Структура("Поле1, Поле2", СтрТЗ.Поле1, СтрТЗ.Поле2);
    Найденные = Объект.ТЧ.НайтиСтроки(Отбор);
    Если Найденные.Количество() = 0 Тогда
        // Добавляем строку
    КонецЕсли;
КонецЦикла;

Этот код имеет сложность O(N*M) и будет работать очень медленно на таблицах размером более 100-500 строк.

Оптимизированный программный подход (через Соответствие)

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

Рассмотрим правильный алгоритм:


// 1. Индексируем существующие строки
СуществующиеСтроки = Новый Соответствие;
Для Каждого СтрТЧ Из Объект.ТЧ Цикл
    // Формируем уникальный ключ. Если полей много, можно использовать хеш-функцию
    // или склеить значения в строку с разделителем
    Ключ = Строка(СтрТЧ.Поле1) + "|" + Строка(СтрТЧ.Поле2) + "|" + Строка(СтрТЧ.Поле3);
    СуществующиеСтроки.Вставить(Ключ, Истина);
КонецЦикла;

// 2. Ищем новые строки (проход по ТЗ)
НовыеСтрокиТЗ = Новый Массив;
Для Каждого СтрТЗ Из ТаблицаЗначений Цикл
    Ключ = Строка(СтрТЗ.Поле1) + "|" + Строка(СтрТЗ.Поле2) + "|" + Строка(СтрТЗ.Поле3);
    
    // Мгновенная проверка наличия ключа
    Если СуществующиеСтроки.Получить(Ключ) = Неопределено Тогда
        НовыеСтрокиТЗ.Добавить(СтрТЗ);
    КонецЕсли;
КонецЦикла;

Особые ситуации

Сравнение по большому количеству полей

Если полей для сравнения не 3, а 10 или 20, писать условия соединения в запросе становится неудобно и медленно. В таких случаях рекомендуется добавить в таблицу вычисляемое поле "Хеш" (например, MD5 от строки, содержащей значения всех значимых полей). Сравнение в запросе будет производиться всего по одной колонке — хешу.

Учет дублей строк

Обратите внимание, что стандартные методы (LEFT JOIN, NOT IN, свертка) работают с множествами уникальных значений. Если в вашей ТЗ две абсолютно одинаковые строки (например, два одинаковых товара), а в ТЧ — одна, то описанные выше методы не покажут, что вторую строку нужно добавить. Система посчитает, что "товар такой-то существует".

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

Вывод

Для большинства задач оптимальным решением является Способ 1 (Запрос с временной таблицей и левым соединением). Он обеспечивает баланс между простотой кода, читаемостью и высокой производительностью. Если же вы работаете с данными только в оперативной памяти (на клиенте или без записи в БД), используйте Оптимизированный программный подход через Соответствие.

← На главную