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

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

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

Посмотрим на пример проблемы

Допустим, у нас есть некая таблица номенклатуры (назовем ее ар1) с остатками, и мы хотим присоединить к ней данные из физической таблицы регистра накопления Закупки за определенный период. В коде это выглядит примерно так:


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

На первый взгляд, все логично: мы используем полное соединение, чтобы получить номенклатуру, которая есть в таблице остатков (даже если по ней не было закупок), и ту номенклатуру, которая была закуплена (даже если ее изначально не было в первой таблице). Затем мы отсекаем лишние закупки с помощью секции ГДЕ. Но при выполнении мы замечаем, что номенклатура, по которой не было движений в регистре закупок за указанный период (количество равно 0), полностью пропадает из результата. Разберем подробнее, почему так происходит.

Логический порядок выполнения запроса СУБД и скрытое поведение NULL

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

  1. Сначала выполняется блок ИЗ, в котором происходит физическое соединение таблиц. Если для записи из таблицы ар1 не найдено соответствие в таблице Закупки, все поля из таблицы Закупки (включая Период и ХозяйственнаяОперация) заполняются специальным системным значением NULL.
  2. Только после того, как "склеивание" завершено, система переходит к обработке блока ГДЕ, который фильтрует полученный результат.
  3. Платформа 1С (как и классический SQL) обрабатывает сравнения со значением NULL особым образом. Любые операторы сравнения (>=, <=, =, <>) при взаимодействии с NULL возвращают результат ЛОЖЬ.

Таким образом, когда система доходит до пустой присоединенной строки и пытается проверить условие Закупки.Период >= &НачалоПериода, по факту она проверяет выражение NULL >= &НачалоПериода. Получив логическую ложь, база данных навсегда удаляет эту строку из итоговой выборки. Именно поэтому полное соединение неявно деградирует до внутреннего. Вы сами отсекли нужные вам данные.

Решение 1: Перенос условия в секцию ПО (Соединение таблиц)

Первый вариант решения, который часто предлагают разработчики — перенести условия из блока ГДЕ непосредственно в условия соединения (в секцию ПО). В этом случае фильтрация происходит в момент сопоставления таблиц, а не после него.


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

Важное предостережение: если вы используете левое соединение, этот метод отлично сработает. Но в случае с полным соединением результат может вас удивить. Условие в секции ПО лишь определяет правила склейки. Если условие по периоду не выполнится, строки просто не соединятся. Но поскольку соединение полное, СУБД выведет обе несоединившиеся строки: строку из таблицы остатков (с пустыми закупками) и строку из таблицы закупок, которая не попала в период (с пустыми остатками). В итоге вы получите "задвоенные" данные и избыточные строки, не подходящие под ваши условия.

Решение 2: Использование параметров виртуальных таблиц (Рекомендуемый метод)

Проанализируем главную архитектурную ошибку исходного кода. Обращение напрямую к физической таблице регистра накопления для получения итогов (или оборотов) за период — это плохая практика. Платформа 1С специально предоставляет разработчикам мощный инструмент — виртуальные таблицы. Именно они должны использоваться для получения среза данных.

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


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

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

Решение 3: Использование временных таблиц

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

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

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

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

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

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

Этот подход гарантирует, что логика внешнего и полного соединения отработает безупречно, а планировщик запросов СУБД (будь то MS SQL, PostgreSQL или другой) построит максимально эффективный план выполнения, так как он не будет перегружен условиями при соединении больших объемов данных.

← На главную