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

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

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

Нередко разработчики пытаются решить эту задачу интуитивно через внутреннее соединение с условиями вида Свойство <> Свойство или помещением сложных логических связок в условие соединения ПО, что приводит либо к получению пустой выборки, либо к декартову произведению строк. Разберем по шагам, почему возникают такие ошибки и как правильно построить эффективный запрос.

Анализ проблемы: почему не работают условия неравенства в соединениях

Представим, что у нас есть таблица входящих данных Т1 (номенклатура, свойство, значение) и табличная часть базы данных Т2 (дополнительные реквизиты). Нам необходимо выбрать строки из Т1, если:

  1. У товара вообще отсутствует данное свойство в табличной части (нет строки с таким свойством).
  2. Строка с таким свойством существует, но значение свойства отличается от входящего.

Типичная ошибка заключается в попытке наложить условие неравенства непосредственно в блоке ПО при внутреннем или левом соединении:


// Ошибочный подход
ПО Т1.Номенклатура = Т2.Ссылка
    И Т1.Свойство <> Т2.Свойство
    И Т1.Значение <> Т2.Значение

Проанализируем ситуацию: если у товара в базе есть несколько свойств (например, «Цвет» и «Размер»), то при проверке входящей строки с «Цветом» условие Т1.Свойство <> Т2.Свойство сработает для строки базы с «Размером». В результате строка свяжется с чужим свойством, произойдет неконтролируемое размножение строк, а логика проверки будет полностью нарушена.

Второе распространенное заблуждение — считать, что в секции ГДЕ при ЛЕВОМ СОЕДИНЕНИИ нельзя обращаться к полям правой таблицы. Секция ГДЕ фильтрует уже сформированный результат соединения. Однако важно помнить: если запись в правой таблице отсутствует, все ее поля содержат NULL. Любое прямое сравнение с NULL (например, Т1.Значение <> Т2.Значение) в языке SQL возвращает Ложь, из-за чего новая строка отбрасывается фильтром.

Решение 1. Классический паттерн через левое соединение по трем ключам (NOT EXISTS)

Самый чистый, надежный и быстродействующий способ — связать таблицы по всем ключевым полям совпадения, включая значение, а затем отсечь найденные строки в секции ГДЕ с помощью проверки на NULL.

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


ВЫБРАТЬ
    Т1.Номенклатура КАК Номенклатура,
    Т1.Свойство КАК Свойство,
    Т1.Значение КАК Значение
ИЗ
    ВременнаяТаблицаНоменклатуры КАК Т1
        ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура.ДополнительныеРеквизиты КАК Т2
        ПО Т1.Номенклатура = Т2.Ссылка
            И Т1.Свойство = Т2.Свойство
            И Т1.Значение = Т2.Значение
ГДЕ
    Т2.Ссылка ЕСТЬ NULL

Разберем логику работы этого запроса:

  1. Мы соединяем таблицы строго по знаку равенства = для всех трех полей: объект, свойство и его значение.
  2. Если в базе уже есть строка с точно таким же свойством и точно таким же значением, условие соединения выполняется полностью, и в колонке Т2.Ссылка появляется ссылка на элемент.
  3. Если свойства нет вовсе, либо если свойство есть, но его значение отличается, левое соединение не найдет точного соответствия. В результирующей строке поля таблицы Т2 заполнятся значением NULL.
  4. Условие ГДЕ Т2.Ссылка ЕСТЬ NULL оставляет в итоговой выборке только те строки, у которых не нашлось полного совпадения в базе данных. Это исключает необходимость подбирать фиктивные константы для замены NULL.

Решение 2. Соединение по идентификаторам свойств с обработкой NULL в секции ГДЕ

Если необходимо явно разделить логику «свойства нет» и «значение изменилось», соединение накладывается только на пару ключевых полей (Номенклатура, Свойство), а проверка различий значений переносится в фильтр с использованием функции ЕСТЬNULL.

Рассмотрим реализацию этого варианта:


ВЫБРАТЬ
    Т1.Номенклатура КАК Номенклатура,
    Т1.Свойство КАК Свойство,
    Т1.Значение КАК Значение
ИЗ
    ВременнаяТаблицаНоменклатуры КАК Т1
        ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура.ДополнительныеРеквизиты КАК Т2
        ПО Т1.Номенклатура = Т2.Ссылка
            И Т1.Свойство = Т2.Свойство
ГДЕ
    Т1.Значение <> ЕСТЬNULL(Т2.Значение, &ЗаведомоНесуществующееЗначение)

Выясним причину работы этого условия: если в базе свойства нет, функция ЕСТЬNULL подставляет фиктивное значение параметра, которое гарантированно не совпадает с реальным входящим значением, и условие неравенства возвращает Истина. Если же свойство найдено, сравниваются реальные значения полей. Главный минус подхода — необходимость передачи специального значения параметра, тип которого должен совпадать с типом поля.

Особенности работы с составными типами данных и типизация

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

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

Если во временной таблице входящее значение хранится как обычная строка, а в базе данных — как ссылка или строка другой длины, прямое сравнение через = или <> всегда будет возвращать неверный результат из-за несовпадения типов.

Для корректного сопоставления применяем приведение типов с помощью функции ВЫРАЗИТЬ:


ВЫБРАТЬ
    Т1.Номенклатура КАК Номенклатура,
    Т1.Свойство КАК Свойство,
    Т1.Значение КАК Значение
ИЗ
    ВременнаяТаблицаНоменклатуры КАК Т1
        ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура.ДополнительныеРеквизиты КАК Т2
        ПО Т1.Номенклатура = Т2.Ссылка
            И Т1.Свойство = Т2.Свойство
            И Т1.Значение = ВЫРАЗИТЬ(Т2.Значение КАК Строка(150))
ГДЕ
    Т2.Ссылка ЕСТЬ NULL

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

Оптимизация производительности на больших объемах данных

Когда обработка оперирует десятками и сотнями тысяч строк, соблюдаем правила оптимизации запросов и записи объектов:

  1. Индексация временных таблиц: при создании временной таблицы в запросе обязательно указываем инструкцию ИНДЕКСИРОВАТЬ ПО Номенклатура, Свойство. Без индексов СУБД выполняет полное сканирование таблиц, что многократно замедляет обработку пакета.
  2. Отключение проверок при загрузке: при программной записи найденных объектов устанавливаем свойство ОбменДанными.Загрузка = Истина. Это отключает подписки на события и второстепенные валидации, ускоряя сохранение в несколько раз:
    
    НоменклатураОбъект = СтрокаВыборки.Номенклатура.ПолучитьОбъект();
    НоменклатураОбъект.ОбменДанными.Загрузка = Истина;
    // Заполнение табличной части...
    НоменклатураОбъект.Записать();
    
  3. Порционная запись в транзакциях: запись сотен тысяч позиций нельзя оборачивать в одну общую транзакцию, так как это переполнит журнал транзакций СУБД и заблокирует таблицы. Оптимально фиксировать транзакции порциями по 500–1000 элементов с помощью явных вызовов НачатьТранзакцию() и ЗафиксироватьТранзакцию().
← На главную