Как правильно использовать ПОЛНОЕ СОЕДИНЕНИЕ в запросах 1С, чтобы не терять данные?

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

При работе с языком запросов 1С часто возникает задача объединить данные из двух разных источников, например, фактические остатки товаров на складе и ожидаемые поступления. Идеальным инструментом для этого кажется ПОЛНОЕ СОЕДИНЕНИЕ, ведь оно должно показывать все записи из обеих таблиц, даже если для них нет пары. Однако на практике можно столкнуться с ситуацией, когда записи из одной из таблиц таинственным образом исчезают из результата. Давайте разберемся, почему так происходит и как этого избежать.

Проанализируем типичную ситуацию. Есть два запроса: один получает ожидаемые поступления, другой — свободные остатки. Мы хотим объединить их в одну таблицу.

Запрос 1: Ожидаемые поступления


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

Запрос 2: Объединение с остатками (неверная версия)


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

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

Анализ проблемы: почему условие "ГДЕ" ломает внешнее соединение?

Корень проблемы кроется в порядке выполнения операторов в SQL-запросе. Секция ГДЕ (WHERE) применяется после того, как выполнено соединение таблиц (JOIN). Давайте пошагово разберем, что происходит:

  1. Выполняется ПОЛНОЕ СОЕДИНЕНИЕ. Система берет все строки из таблицы остатков и все строки из таблицы поступлений и пытается их сопоставить по характеристике и складу.
  2. Если для строки из таблицы поступлений не нашлось соответствующей строки в таблице остатков, система все равно включает ее в промежуточный результат. Однако все поля, относящиеся к таблице остатков (СвободныеОстаткиОстатки.Характеристика, СвободныеОстаткиОстатки.Склад, СвободныеОстаткиОстатки.Номенклатура и т.д.), для этой строки будут иметь значение NULL.
  3. Применяется условие ГДЕ. Теперь система фильтрует полученный набор данных по условию ГДЕ СвободныеОстаткиОстатки.Номенклатура = &Ссылка.

Именно на последнем шаге и происходит "магия". Для той самой строки, которая была только в поступлениях, поле СвободныеОстаткиОстатки.Номенклатура равно NULL. Сравнение NULL = &Ссылка всегда дает результат ЛОЖЬ (не ИСТИНА и не NULL). В итоге эта строка отбрасывается фильтром.

Таким образом, условие в секции ГДЕ, наложенное на поле одной из таблиц, фактически превращает ПОЛНОЕ СОЕДИНЕНИЕ в ЛЕВОЕ или ПРАВОЕ (в нашем случае — в ПРАВОЕ, так как остаются только строки, где есть данные из правой таблицы `СвободныеОстаткиОстатки`).

Решение 1: Перенос условий отбора в правильное место

Чтобы отбор не "ломал" логику внешнего соединения, его нужно применять не ко всему результату, а к каждой таблице до момента их объединения. Для этого есть два правильных места.

Способ А: Параметры виртуальной таблицы (предпочтительный)

Самый правильный и производительный способ — передать условия отбора прямо в параметры виртуальной таблицы. Это позволяет СУБД сначала отобрать нужные записи и только потом выполнять соединение.


ИЗ
    РегистрНакопления.СвободныеОстатки.Остатки(&ТекДата, Номенклатура = &Ссылка) КАК СвободныеОстаткиОстатки
    ПОЛНОЕ СОЕДИНЕНИЕ ОжидаемыеПоступления КАК ОжидаемыеПоступления
    ...

Способ Б: Условия в секции ПО

Условия отбора также можно перенести в секцию ПО (ON). Это тоже сработает, но использование параметров виртуальных таблиц считается более идиоматичным для платформы 1С.

Важный момент! Отбор нужно применить к обеим таблицам, если это необходимо. В нашем исходном примере отбор по номенклатуре уже был в первом запросе для временной таблицы, а для второго его нужно было добавить в параметры виртуальной таблицы.

Решение 2: Обработка NULL-значений с помощью ЕСТЬNULL

Даже после исправления отбора мы столкнемся со второй проблемой. Если мы перенесем условие и уберем ГДЕ, результат может выглядеть так:

Характеристика|Склад|ВНаличииОстаток|Количество
S, белый|Магазин|8|
L, белый|Магазин2|7|
| | |50

Почему поля Характеристика и Склад пустые для строки с поступлением? Потому что в секции ВЫБРАТЬ мы указали СвободныеОстаткиОстатки.Характеристика и СвободныеОстаткиОстатки.Склад. А для строки, которая есть только в поступлениях, эти поля равны NULL.

Чтобы это исправить, нужно использовать функцию ЕСТЬNULL() (аналог COALESCE в стандартном SQL). Она принимает два параметра и возвращает первый из них, если он не NULL, в противном случае — второй.

Итоговый правильный запрос:

Давайте соберем все исправления в один корректный запрос, объединив обе таблицы без использования временной.


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

Проанализируем этот код:

  1. Мы выбираем ключевые поля (Характеристика, Склад) с помощью ЕСТЬNULL. Если запись есть в остатках, возьмется значение оттуда. Если нет — из поступлений.
  2. Для количественных полей мы также используем ЕСТЬNULL, заменяя возможное значение NULL на 0 для корректного отображения и дальнейших вычислений.
  3. Отбор по номенклатуре для регистра остатков перенесен в параметры виртуальной таблицы — это критически важно.
  4. Для второй таблицы (регистра сведений) отбор по номенклатуре можно оставить в ГДЕ, так как она не является виртуальной. Однако для единообразия и ясности его можно было бы также перенести в условия соединения ПО.
  5. Условие соединения теперь включает и номенклатуру, что является хорошей практикой для повышения точности связи.

Такой запрос даст именно тот результат, который требовался изначально, корректно отображая все данные.

Альтернативный подход: ОБЪЕДИНИТЬ ВСЕ

В некоторых случаях конструкция с ПОЛНОЕ СОЕДИНЕНИЕ может быть громоздкой. Существует элегантная альтернатива — оператор ОБЪЕДИНИТЬ ВСЕ (UNION ALL). Этот подход часто бывает более читаемым и гибким.

Суть метода в следующем:

  1. Создаем первый подзапрос, который выбирает остатки. Добавляем в него фиктивное поле для поступлений со значением 0.
  2. Создаем второй подзапрос, который выбирает поступления. Добавляем в него фиктивное поле для остатков со значением 0.
  3. Объединяем результаты этих двух запросов с помощью ОБЪЕДИНИТЬ ВСЕ.
  4. Во внешнем запросе группируем полученный объединенный результат по нужным измерениям (Характеристика, Склад) и суммируем числовые поля.

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


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

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

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

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

← На главную