Разберем распространенную, но очень коварную проблему, с которой часто сталкиваются начинающие разработчики при написании запросов в 1С. Задача звучит просто: получить остатки товаров по определенному складу за исключением тех характеристик номенклатуры, которые уже присутствуют в конкретном документе поступления.
На первый взгляд кажется логичным связать виртуальную таблицу остатков и табличную часть документа с помощью левого соединения по номенклатуре и условию «характеристика остатка не равна характеристике в документе». Однако такой запрос выдает непредсказуемый результат — в выборку попадают абсолютно все характеристики. Проанализируем, почему так происходит, и рассмотрим правильные варианты решения этой задачи.
Рассмотрим исходный запрос, который вызвал затруднения у автора темы на форуме:
ВЫБРАТЬ
Остатки.Номенклатура КАК Номенклатура,
Остатки.Характеристика КАК Характеристика,
Остатки.КоличествоОстаток КАК КоличествоОстаток,
ПТиУТ.Характеристика КАК Характеристика1
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПоступлениеТоваров.Товары КАК ПТиУТ
ПО (Остатки.Номенклатура = ПТиУТ.Номенклатура)
И (Остатки.Характеристика <> ПТиУТ.Характеристика)
ГДЕ
ПТиУТ.Ссылка = &ДокПоступления
В этом запросе кроются сразу две фундаментальные логические ошибки, из-за которых СУБД возвращает неверный результат.
Ошибка 1. Превращение ЛЕВОГО соединения во ВНУТРЕННЕЕ.
Обратим внимание на секцию ГДЕ ПТиУТ.Ссылка = &ДокПоступления. При левом соединении для всех записей из таблицы остатков, для которых не нашлось соответствия в документе, поля правой таблицы (включая Ссылка) заполняются значением NULL. Когда СУБД начинает обрабатывать условие фильтрации ГДЕ, она сравнивает NULL = &ДокПоступления. Результатом такого сравнения всегда будет значение «Ложь» (или «Неизвестно»). В итоге все строки, которые не нашли себе пару в документе, отсекаются. Запрос начинает работать как обычное внутреннее соединение, лишая левое соединение всякого смысла.
Ошибка 2. Логика работы условия «не равно» (<>) в секции связи ПО.
Представим, что на складе у нас есть Товар 1 с характеристиками «Красный» и «Синий». В документе поступления этот же Товар 1 присутствует только с характеристикой «Красный». Мы ожидаем получить на выходе только характеристику «Синий».
Как это обрабатывает СУБД:
Красный <> Красный дает «Ложь». Связь по этой строке не устанавливается.Синий <> Красный дает «Истина». Записи успешно соединяются. Мы получаем Товар 1 («Синий»). Это правильный шаг.Красный <> Синий вернет «Истина»! В результате строка остатка «Красный» успешно соединится с документом и попадет в финальную выборку, хотя она есть в документе и должна быть исключена.Таким образом, условие <> в соединении таблиц не исключает записи, а наоборот — находит любые несовпадающие пары, лавинообразно увеличивая количество строк в результате запроса.
Чтобы правильно решить задачу методом исключения множеств (так называемый Left Anti-Join), нам нужно связать таблицы по условию равенства ключевых полей, а затем отобрать только те строки, которые не смогли соединиться. При этом условие фильтрации по конкретному документу необходимо перенести непосредственно в параметры связи ПО.
Рассмотрим правильный шаблон такого запроса:
ВЫБРАТЬ
Остатки.Номенклатура КАК Номенклатура,
Остатки.Характеристика КАК Характеристика,
Остатки.КоличествоОстаток КАК КоличествоОстаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПоступлениеТоваров.Товары КАК ПТиУТ
ПО Остатки.Номенклатура = ПТиУТ.Номенклатура
И Остатки.Характеристика = ПТиУТ.Характеристика
И ПТиУТ.Ссылка = &ДокПоступления
ГДЕ
ПТиУТ.Номенклатура ЕСТЬ NULL
Как работает этот механизм: СУБД пытается найти точное совпадение по номенклатуре и характеристике в рамках указанного документа. Если совпадение найдено, поля правой таблицы заполняются реальными данными. Если совпадения нет, поля заполняются значениями NULL. Условие ГДЕ ПТиУТ.Номенклатура ЕСТЬ NULL отсекает все успешно соединившиеся строки, оставляя только уникальные характеристики остатков, которых не было в документе поступления.
В ходе обсуждения на форуме участники пришли к рабочему решению с использованием оператора отрицания вхождения НЕ В. Этот подход понятен интуитивно и отлично решает проблему, когда нужно отобрать только те характеристики, которых гарантированно нет в документе поступления, ограничив при этом выборку только товарами из этого же документа.
Посмотрим на пример кода, предложенный в ветке обсуждения:
ВЫБРАТЬ
Остатки.Номенклатура КАК Номенклатура,
Остатки.Характеристика КАК Характеристика,
Остатки.КоличествоОстаток КАК КоличествоОстаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад
И Номенклатура В (
ВЫБРАТЬ ПТиУТ.Номенклатура
ИЗ Документ.ПоступлениеТоваров.Товары КАК ПТиУТ
ГДЕ ПТиУТ.Ссылка = &ДокПоступления
)
) КАК Остатки
ГДЕ
НЕ (Остатки.Номенклатура, Остатки.Характеристика) В (
ВЫБРАТЬ
ПТиУТ.Номенклатура,
ПТиУТ.Характеристика
ИЗ
Документ.ПоступлениеТоваров.Товары КАК ПТиУТ
ГДЕ
ПТиУТ.Ссылка = &ДокПоступления
)
Здесь мы видим два важных действия:
ГДЕ мы используем многополевую конструкцию НЕ (Номенклатура, Характеристика) В (...), которая исключает точные пары «Товар-Характеристика», присутствующие в поступлении.Несмотря на то, что Способ 2 полностью решает задачу, в реальных высоконагруженных базах данных прямое соединение или использование сложных подзапросов с виртуальными таблицами (такими как Остатки) крайне не рекомендуется стандартами разработки фирмы «1С».
Виртуальная таблица сама по себе является сложным подзапросом на уровне СУБД. Прямая фильтрация через В (ВЫБРАТЬ...) внутри параметров виртуальной таблицы может привести к неоптимальному плану запроса и зависаниям базы данных. Чтобы написать этот запрос идеально с точки зрения производительности, применим архитектурный паттерн с использованием временных таблиц.
Разберем этот процесс по шагам:
// Шаг 1: Выгружаем уникальные товары и характеристики из документа во временную таблицу
ВЫБРАТЬ РАЗЛИЧНЫЕ
ПТиУТ.Номенклатура КАК Номенклатура,
ПТиУТ.Характеристика КАК Характеристика
ПОМЕСТИТЬ ВТ_ТоварыДокумента
ИЗ
Документ.ПоступлениеТоваров.Товары КАК ПТиУТ
ГДЕ
ПТиУТ.Ссылка = &ДокПоступления
ИНДЕКСИРОВАТЬ ПО
Номенклатура,
Характеристика
;
// Шаг 2: Получаем остатки, накладывая легкий фильтр и выполняя левое соединение с временной таблицей
ВЫБРАТЬ
Остатки.Номенклатура КАК Номенклатура,
Остатки.Характеристика КАК Характеристика,
Остатки.КоличествоОстаток КАК КоличествоОстаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(,
Склад = &Склад
И Номенклатура В (ВЫБРАТЬ РАЗЛИЧНЫЕ ВТ.Номенклатура ИЗ ВТ_ТоварыДокумента КАК ВТ)
) КАК Остатки
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ТоварыДокумента КАК ВТ_Товары
ПО Остатки.Номенклатура = ВТ_Товары.Номенклатура
И Остатки.Характеристика = ВТ_Товары.Характеристика
ГДЕ
ВТ_Товары.Номенклатура ЕСТЬ NULL
Преимущества архитектурного подхода:
ВТ_ТоварыДокумента.Номенклатура и Характеристика позволяет СУБД мгновенно выполнять соединение таблиц.Используйте данный подход в своей повседневной практике разработки, чтобы ваши запросы всегда были быстрыми, надежными и соответствовали стандартам качества 1С!