Почему не работает условие "не равно" в левом соединении запроса 1С и как это исправить

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

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

На первый взгляд кажется логичным связать виртуальную таблицу остатков и табличную часть документа с помощью левого соединения по номенклатуре и условию «характеристика остатка не равна характеристике в документе». Однако такой запрос выдает непредсказуемый результат — в выборку попадают абсолютно все характеристики. Проанализируем, почему так происходит, и рассмотрим правильные варианты решения этой задачи.

Выясняем причину: почему условие связи «не равно» (<>) работает неверно

Рассмотрим исходный запрос, который вызвал затруднения у автора темы на форуме:


ВЫБРАТЬ
    Остатки.Номенклатура КАК Номенклатура,
    Остатки.Характеристика КАК Характеристика,
    Остатки.КоличествоОстаток КАК КоличествоОстаток,
    ПТиУТ.Характеристика КАК Характеристика1
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПоступлениеТоваров.Товары КАК ПТиУТ
        ПО (Остатки.Номенклатура = ПТиУТ.Номенклатура)
            И (Остатки.Характеристика <> ПТиУТ.Характеристика)
ГДЕ
    ПТиУТ.Ссылка = &ДокПоступления

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

Ошибка 1. Превращение ЛЕВОГО соединения во ВНУТРЕННЕЕ.
Обратим внимание на секцию ГДЕ ПТиУТ.Ссылка = &ДокПоступления. При левом соединении для всех записей из таблицы остатков, для которых не нашлось соответствия в документе, поля правой таблицы (включая Ссылка) заполняются значением NULL. Когда СУБД начинает обрабатывать условие фильтрации ГДЕ, она сравнивает NULL = &ДокПоступления. Результатом такого сравнения всегда будет значение «Ложь» (или «Неизвестно»). В итоге все строки, которые не нашли себе пару в документе, отсекаются. Запрос начинает работать как обычное внутреннее соединение, лишая левое соединение всякого смысла.

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

Как это обрабатывает СУБД:

  1. Берется строка остатка Товар 1 («Красный») и сравнивается со строкой документа Товар 1 («Красный»). Условие Красный <> Красный дает «Ложь». Связь по этой строке не устанавливается.
  2. Берется строка остатка Товар 1 («Синий») и сравнивается с документом Товар 1 («Красный»). Условие Синий <> Красный дает «Истина». Записи успешно соединяются. Мы получаем Товар 1 («Синий»). Это правильный шаг.
  3. А теперь представим, что в документе поступления присутствуют обе характеристики: и «Красный», и «Синий». При сравнении строки остатка «Красный» со строкой документа «Синий» условие Красный <> Синий вернет «Истина»! В результате строка остатка «Красный» успешно соединится с документом и попадет в финальную выборку, хотя она есть в документе и должна быть исключена.

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

Способ 1. Классическое решение через ЛЕВОЕ СОЕДИНЕНИЕ и проверку на NULL

Чтобы правильно решить задачу методом исключения множеств (так называемый Left Anti-Join), нам нужно связать таблицы по условию равенства ключевых полей, а затем отобрать только те строки, которые не смогли соединиться. При этом условие фильтрации по конкретному документу необходимо перенести непосредственно в параметры связи ПО.

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


ВЫБРАТЬ
    Остатки.Номенклатура КАК Номенклатура,
    Остатки.Характеристика КАК Характеристика,
    Остатки.КоличествоОстаток КАК КоличествоОстаток
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПоступлениеТоваров.Товары КАК ПТиУТ
        ПО Остатки.Номенклатура = ПТиУТ.Номенклатура
        И Остатки.Характеристика = ПТиУТ.Характеристика
        И ПТиУТ.Ссылка = &ДокПоступления
ГДЕ
    ПТиУТ.Номенклатура ЕСТЬ NULL

Как работает этот механизм: СУБД пытается найти точное совпадение по номенклатуре и характеристике в рамках указанного документа. Если совпадение найдено, поля правой таблицы заполняются реальными данными. Если совпадения нет, поля заполняются значениями NULL. Условие ГДЕ ПТиУТ.Номенклатура ЕСТЬ NULL отсекает все успешно соединившиеся строки, оставляя только уникальные характеристики остатков, которых не было в документе поступления.

Способ 2. Решение через конструкцию НЕ В (NOT IN)

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

Посмотрим на пример кода, предложенный в ветке обсуждения:


ВЫБРАТЬ
    Остатки.Номенклатура КАК Номенклатура,
    Остатки.Характеристика КАК Характеристика,
    Остатки.КоличествоОстаток КАК КоличествоОстаток
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад 
        И Номенклатура В (
            ВЫБРАТЬ ПТиУТ.Номенклатура 
            ИЗ Документ.ПоступлениеТоваров.Товары КАК ПТиУТ 
            ГДЕ ПТиУТ.Ссылка = &ДокПоступления
        )
    ) КАК Остатки
ГДЕ
    НЕ (Остатки.Номенклатура, Остатки.Характеристика) В (
        ВЫБРАТЬ 
            ПТиУТ.Номенклатура, 
            ПТиУТ.Характеристика 
        ИЗ 
            Документ.ПоступлениеТоваров.Товары КАК ПТиУТ 
        ГДЕ 
            ПТиУТ.Ссылка = &ДокПоступления
    )

Здесь мы видим два важных действия:

  1. В параметрах виртуальной таблицы остатков мы сразу ограничиваем выборку номенклатуры только теми позициями, которые есть в документе поступления. Это отсекает лишние данные и ускоряет работу запроса.
  2. В секции ГДЕ мы используем многополевую конструкцию НЕ (Номенклатура, Характеристика) В (...), которая исключает точные пары «Товар-Характеристика», присутствующие в поступлении.

Способ 3. Оптимальный вариант по стандартам разработки 1С (Временные Таблицы)

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

Виртуальная таблица сама по себе является сложным подзапросом на уровне СУБД. Прямая фильтрация через В (ВЫБРАТЬ...) внутри параметров виртуальной таблицы может привести к неоптимальному плану запроса и зависаниям базы данных. Чтобы написать этот запрос идеально с точки зрения производительности, применим архитектурный паттерн с использованием временных таблиц.

Разберем этот процесс по шагам:


// Шаг 1: Выгружаем уникальные товары и характеристики из документа во временную таблицу
ВЫБРАТЬ РАЗЛИЧНЫЕ
    ПТиУТ.Номенклатура КАК Номенклатура,
    ПТиУТ.Характеристика КАК Характеристика
ПОМЕСТИТЬ ВТ_ТоварыДокумента
ИЗ
    Документ.ПоступлениеТоваров.Товары КАК ПТиУТ
ГДЕ
    ПТиУТ.Ссылка = &ДокПоступления

ИНДЕКСИРОВАТЬ ПО
    Номенклатура,
    Характеристика
;

// Шаг 2: Получаем остатки, накладывая легкий фильтр и выполняя левое соединение с временной таблицей
ВЫБРАТЬ
    Остатки.Номенклатура КАК Номенклатура,
    Остатки.Характеристика КАК Характеристика,
    Остатки.КоличествоОстаток КАК КоличествоОстаток
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки(, 
        Склад = &Склад 
        И Номенклатура В (ВЫБРАТЬ РАЗЛИЧНЫЕ ВТ.Номенклатура ИЗ ВТ_ТоварыДокумента КАК ВТ)
    ) КАК Остатки
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ТоварыДокумента КАК ВТ_Товары
        ПО Остатки.Номенклатура = ВТ_Товары.Номенклатура
        И Остатки.Характеристика = ВТ_Товары.Характеристика
ГДЕ
    ВТ_Товары.Номенклатура ЕСТЬ NULL

Преимущества архитектурного подхода:

Используйте данный подход в своей повседневной практике разработки, чтобы ваши запросы всегда были быстрыми, надежными и соответствовали стандартам качества 1С!

← На главную