Почему запрос на поиск контрагентов без документов за период возвращает пустую таблицу?

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

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

Разбор исходного запроса и ключевых ошибок

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

  1. В первой временной таблице Данные собираются уникальные контрагенты из документа РасходнаяНакладная за интервал посещения.
  2. Во второй временной таблице ГотовыеДанные выполняется ЛЕВОЕ СОЕДИНЕНИЕ таблицы Данные с вложенным запросом, выбирающим накладные за второй интервал (период отсутствия).
  3. В итоговом запросе устанавливается фильтр: ГДЕ ГотовыеДанные.Ссылка = ЗНАЧЕНИЕ(Документ.РасходнаяНакладная.ПустаяСсылка).

В такой логике кроются фундаментальные ошибки платформы и языка запросов 1С.

1. Пустая ссылка и значение NULL не эквивалентны.

Когда при левом соединении для строки из левой таблицы во второй таблице не находится соответствия, СУБД возвращает специальное значение NULL, обозначающее отсутствие данных. Значение ЗНАЧЕНИЕ(Документ.РасходнаяНакладная.ПустаяСсылка) представляет собой валидную, но незаполненную ссылку на документ (бинарный ноль в терминах базы данных). Поскольку реальная накладная в базе всегда имеет сформированную ссылку, поле никогда не станет пустой ссылкой в результате соединения. Сравнение ГотовыеДанные.Ссылка = ЗНАЧЕНИЕ(...) гарантированно возвращает пустой результат.

2. Трехзначная логика SQL и оператор ЕСТЬ NULL.

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


ГДЕ
    ГотовыеДанные.Ссылка ЕСТЬ NULL

Попытки сравнения вида Ссылка = NULL или Ссылка <> NULL всегда вычисляются СУБД как Ложь (Unknown), что также приводит к пустой выборке.

Почему замена на полное соединение является ложным решением

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

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

Скрытые причины отсутствия данных в выборке

Если условие ЕСТЬ NULL установлено, но запрос все равно пуст, обратим внимание на следующие факторы:

  1. Неучет времени в границах дат. Конструкция МЕЖДУ &Начало И &Конец строго включает обе границы. Если в параметр &КонецПериода передать дату без указания времени (например, 31.01.2025 00:00:00), то документы, созданные в этот день позже полуночи, будут исключены. Для корректной работы дату окончания необходимо приводить к концу дня с помощью функции КонецДня().
  2. Отсутствие проверки проведения и пометки удаления. Прямое обращение к таблице Документ.РасходнаяНакладная считывает в том числе непроведенные черновики и помеченные на удаление документы. Если менеджер создал черновик документа во втором периоде, строка присоединится, поле не примет значение NULL, и контрагент ошибочно исключится из выборки.
  3. Размножение строк при соединении без группировки. Во вложенном запросе накладные не группируются. Если у клиента несколько накладных во втором периоде, левое соединение создаст дубли, перегружая память и усложняя план выполнения.

Варианты корректного решения задачи

Разберем надежные и производительные способы построения такого запроса.

Способ 1. Использование подзапроса с конструкцией НЕ В

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


ВЫБРАТЬ РАЗЛИЧНЫЕ
    Накладная.Контрагент КАК Контрагент
ИЗ
    Документ.РасходнаяНакладная КАК Накладная
ГДЕ
    Накладная.Проведен
    И Накладная.Дата МЕЖДУ &НачалоПериодаПосещения И &КонецПериодаПосещения
    И Накладная.Контрагент НЕ В (
        ВЫБРАТЬ РАЗЛИЧНЫЕ
            Вложенный.Контрагент
        ИЗ
            Документ.РасходнаяНакладная КАК Вложенный
        ГДЕ
            Вложенный.Проведен
            И Вложенный.Дата МЕЖДУ &НачалоПериодаНеПриезда И &КонецПериодаНеПриезда
    )

Способ 2. Один проход по данным через группировку и секцию ИМЕЮЩИЕ

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


ВЫБРАТЬ
    Накладная.Контрагент КАК Контрагент
ИЗ
    Документ.РасходнаяНакладная КАК Накладная
ГДЕ
    Накладная.Проведен
    И (Накладная.Дата МЕЖДУ &НачалоПериодаПосещения И &КонецПериодаПосещения
        ИЛИ Накладная.Дата МЕЖДУ &НачалоПериодаНеПриезда И &КонецПериодаНеПриезда)
СГРУППИРОВАТЬ ПО
    Накладная.Контрагент
ИМЕЮЩИЕ
    СУММА(ВЫБОР
        КОГДА Накладная.Дата МЕЖДУ &НачалоПериодаПосещения И &КонецПериодаПосещения
            ТОГДА 1
        ИНАЧЕ 0
    КОНЕЦ) > 0
    И СУММА(ВЫБОР
        КОГДА Накладная.Дата МЕЖДУ &НачалоПериодаНеПриезда И &КонецПериодаНеПриезда
            ТОГДА 1
        ИНАЧЕ 0
    КОНЕЦ) = 0

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

Способ 3. Получение данных из регистров накопления (лучшая практика)

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

  1. Они автоматически содержат данные только проведенных документов.
  2. Данные уже предварительно свернуты по измерениям, что радикально ускоряет выполнение запроса.

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


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

Рекомендация по индексации временных таблиц

Если для решения задачи все же используется пакет запросов с промежуточными таблицами, обязательно индексируйте поля связи с помощью инструкции ИНДЕКСИРОВАТЬ ПО:


ВЫБРАТЬ РАЗЛИЧНЫЕ
    РасходнаяНакладная.Контрагент КАК Контрагент
ПОМЕСТИТЬ ВТ_КлиентыПервогоПериода
ИЗ
    Документ.РасходнаяНакладная КАК РасходнаяНакладная
ГДЕ
    РасходнаяНакладная.Проведен
    И РасходнаяНакладная.Дата МЕЖДУ &Начало1 И &Конец1
ИНДЕКСИРОВАТЬ ПО
    Контрагент
;

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

← На главную