При разработке отчетов и выборок в 1С часто возникает задача: найти клиентов, с которыми велась работа в первом периоде, но по которым полностью отсутствуют документы во втором периоде. Классический подход через левое соединение двух таблиц нередко преподносит сюрпризы в виде пустого результата выборки. Проанализируем типичный запрос, выясним причины его некорректного поведения и рассмотрим правильные варианты реализации задачи.
Рассмотрим структуру исходного пакета запросов, с которым возникли сложности:
Данные собираются уникальные контрагенты из документа РасходнаяНакладная за интервал посещения.ГотовыеДанные выполняется ЛЕВОЕ СОЕДИНЕНИЕ таблицы Данные с вложенным запросом, выбирающим накладные за второй интервал (период отсутствия).ГДЕ ГотовыеДанные.Ссылка = ЗНАЧЕНИЕ(Документ.РасходнаяНакладная.ПустаяСсылка).В такой логике кроются фундаментальные ошибки платформы и языка запросов 1С.
1. Пустая ссылка и значение NULL не эквивалентны.
Когда при левом соединении для строки из левой таблицы во второй таблице не находится соответствия, СУБД возвращает специальное значение NULL, обозначающее отсутствие данных. Значение ЗНАЧЕНИЕ(Документ.РасходнаяНакладная.ПустаяСсылка) представляет собой валидную, но незаполненную ссылку на документ (бинарный ноль в терминах базы данных). Поскольку реальная накладная в базе всегда имеет сформированную ссылку, поле никогда не станет пустой ссылкой в результате соединения. Сравнение ГотовыеДанные.Ссылка = ЗНАЧЕНИЕ(...) гарантированно возвращает пустой результат.
2. Трехзначная логика SQL и оператор ЕСТЬ NULL.
Для отсечения найденных записей и сохранения только тех, по которым накладных не было, необходимо использовать специальный оператор проверки:
ГДЕ
ГотовыеДанные.Ссылка ЕСТЬ NULL
Попытки сравнения вида Ссылка = NULL или Ссылка <> NULL всегда вычисляются СУБД как Ложь (Unknown), что также приводит к пустой выборке.
Разберем ситуацию, когда разработчик меняет ЛЕВОЕ СОЕДИНЕНИЕ на ПОЛНОЕ СОЕДИНЕНИЕ и получает результат. На первый взгляд кажется, что проблема решена, но на практике происходит подмена бизнес-логики:
При полном соединении в результат попадают записи, у которых контрагент присутствовал во втором периоде, но вообще отсутствовал в первом. Если при этом проверяется поле из первой таблицы на NULL, запрос вернет клиентов, начавших сотрудничество только во втором периоде, а не тех, кто пропал. Применение полного соединения маскирует реальную проблему с исходными данными или параметрами дат.
Если условие ЕСТЬ NULL установлено, но запрос все равно пуст, обратим внимание на следующие факторы:
МЕЖДУ &Начало И &Конец строго включает обе границы. Если в параметр &КонецПериода передать дату без указания времени (например, 31.01.2025 00:00:00), то документы, созданные в этот день позже полуночи, будут исключены. Для корректной работы дату окончания необходимо приводить к концу дня с помощью функции КонецДня().Документ.РасходнаяНакладная считывает в том числе непроведенные черновики и помеченные на удаление документы. Если менеджер создал черновик документа во втором периоде, строка присоединится, поле не примет значение NULL, и контрагент ошибочно исключится из выборки.Разберем надежные и производительные способы построения такого запроса.
Если требуется получить просто список контрагентов, логичнее отказаться от соединений во временных таблицах и применить конструкцию отрицания НЕ В. Это делает код читаемым и избавляет от манипуляций с NULL:
ВЫБРАТЬ РАЗЛИЧНЫЕ
Накладная.Контрагент КАК Контрагент
ИЗ
Документ.РасходнаяНакладная КАК Накладная
ГДЕ
Накладная.Проведен
И Накладная.Дата МЕЖДУ &НачалоПериодаПосещения И &КонецПериодаПосещения
И Накладная.Контрагент НЕ В (
ВЫБРАТЬ РАЗЛИЧНЫЕ
Вложенный.Контрагент
ИЗ
Документ.РасходнаяНакладная КАК Вложенный
ГДЕ
Вложенный.Проведен
И Вложенный.Дата МЕЖДУ &НачалоПериодаНеПриезда И &КонецПериодаНеПриезда
)
Для повышения быстродействия объединим оба интервала дат в один и выполним один проход по физической таблице базы данных. С помощью агрегатных функций определим наличие активности в каждом из периодов:
ВЫБРАТЬ
Накладная.Контрагент КАК Контрагент
ИЗ
Документ.РасходнаяНакладная КАК Накладная
ГДЕ
Накладная.Проведен
И (Накладная.Дата МЕЖДУ &НачалоПериодаПосещения И &КонецПериодаПосещения
ИЛИ Накладная.Дата МЕЖДУ &НачалоПериодаНеПриезда И &КонецПериодаНеПриезда)
СГРУППИРОВАТЬ ПО
Накладная.Контрагент
ИМЕЮЩИЕ
СУММА(ВЫБОР
КОГДА Накладная.Дата МЕЖДУ &НачалоПериодаПосещения И &КонецПериодаПосещения
ТОГДА 1
ИНАЧЕ 0
КОНЕЦ) > 0
И СУММА(ВЫБОР
КОГДА Накладная.Дата МЕЖДУ &НачалоПериодаНеПриезда И &КонецПериодаНеПриезда
ТОГДА 1
ИНАЧЕ 0
КОНЕЦ) = 0
Такой подход читает таблицу документов всего один раз и исключает операции соединения больших наборов данных в оперативной памяти сервера СУБД.
В типовых конфигурациях (включая «1С:Управление нашей фирмой») обращение напрямую к таблицам документов для аналитических отчетов считается неоптимальным. Продажи фиксируются в специализированных регистрах (например, РегистрНакопления.Продажи). Виртуальные таблицы оборотов регистра имеют существенные преимущества:
Пример запроса к регистру накопления:
ВЫБРАТЬ РАЗЛИЧНЫЕ
ПродажиПервыйПериод.Покупатель КАК Контрагент
ИЗ
РегистрНакопления.Продажи.Обороты(&НачалоПериода1, &КонецПериода1, , ) КАК ПродажиПервыйПериод
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Продажи.Обороты(&НачалоПериода2, &КонецПериода2, , ) КАК ПродажиВторойПериод
ПО ПродажиПервыйПериод.Покупатель = ПродажиВторойПериод.Покупатель
ГДЕ
ПродажиВторойПериод.Покупатель ЕСТЬ NULL
Если для решения задачи все же используется пакет запросов с промежуточными таблицами, обязательно индексируйте поля связи с помощью инструкции ИНДЕКСИРОВАТЬ ПО:
ВЫБРАТЬ РАЗЛИЧНЫЕ
РасходнаяНакладная.Контрагент КАК Контрагент
ПОМЕСТИТЬ ВТ_КлиентыПервогоПериода
ИЗ
Документ.РасходнаяНакладная КАК РасходнаяНакладная
ГДЕ
РасходнаяНакладная.Проведен
И РасходнаяНакладная.Дата МЕЖДУ &Начало1 И &Конец1
ИНДЕКСИРОВАТЬ ПО
Контрагент
;
Создание индекса по полю Контрагент позволяет СУБД использовать индексный поиск при последующих соединениях вместо полного сканирования временной таблицы, что предотвращает падение производительности на реальных базах с большим объемом документов.