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