В чем ошибка связи при левом запросе и как избежать дублирования записей

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

При работе с запросами в 1С начинающие (и даже опытные) разработчики часто сталкиваются с ситуацией, когда результат соединения таблиц выдает гораздо больше строк, чем ожидалось. Классический пример: в одной таблице 4 записи, в другой — 2, а после ЛЕВОЕ СОЕДИНЕНИЕ мы получаем 8. Давайте разберем, почему так происходит, и как правильно составить запрос, чтобы получить только актуальные данные, например, список объектов, которые были поставлены на охрану, но еще не сняты с нее.

Разбираем причину: почему строк становится больше?

Первым делом проанализируем ситуацию, описанную в исходном вопросе. У нас есть документ Перечень с типами «Поставить» и «Снять». В первой временной таблице (Поставить) находятся 4 документа для конкретного контрагента и объекта. Во второй (Снять) — 2 документа для тех же параметров. При выполнении соединения по полям Контрагент и Объект система находит соответствия для каждой строки.

Важно понимать: левое соединение в языке запросов 1С (как и в стандартном SQL) работает по принципу соответствия «каждый с каждым» внутри заданного условия. Если для одной записи из левой таблицы находится несколько подходящих записей в правой, то строка левой таблицы будет продублирована столько раз, сколько совпадений найдено справа. В нашем случае для каждого из 4 документов «Поставить» нашлось по 2 документа «Снять». В итоге получаем 4 * 2 = 8 строк. Это называется частичным декартовым произведением.

Проблема неуникальных ключей связи

Ошибка заключается в том, что связь устанавливается по не вполне уникальным полям. Контрагент и Объект описывают сущность, но не конкретное событие во времени. Чтобы запрос выдал корректный результат (например, 2 документа, которые остались без пары), нам нужно добавить логику выбора последнего события или уникальную привязку.

Рассмотрим подробнее, как можно решить эту задачу, эмулируя работу регистра сведений (среза последних) прямо в запросе по документам.

Решение №1: Группировка по максимальной дате

Самый надежный способ — сначала определить момент последнего события для каждой пары контрагент-объект, а затем соединить эти данные с основной таблицей. Разберем этот процесс по шагам.

  1. Сначала создаем временные таблицы для документов постановки и снятия.
  2. Создаем таблицу «Максимальных дат», где для каждого объекта найдем самую позднюю дату постановки и самую позднюю дату снятия.
  3. Сравниваем эти даты в секции ИМЕЮЩИЕ или через выбор в конечном запросе.

Посмотрим на пример кода, который решает задачу поиска актуальных документов:


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

// Шаг 2: Собираем все снятия
ВЫБРАТЬ
    Перечень.Контрагент,
    Перечень.Объект,
    Перечень.Дата КАК Дата
ПОМЕСТИТЬ ВТ_Снять
ИЗ
    Документ.Перечень КАК Перечень
ГДЕ
    НЕ Перечень.ПометкаУдаления
    И Перечень.Тип = ЗНАЧЕНИЕ(Перечисление.Тип.Снять);

// Шаг 3: Находим последние даты и фильтруем
ВЫБРАТЬ
    МАКСИМУМ(Поставить.Дата) КАК ДатаПоследнейПостановки,
    ЕСТЬNULL(МАКСИМУМ(Снять.Дата), ДАТАВРЕМЯ(1, 1, 1)) КАК ДатаПоследнегоСнятия,
    Поставить.Контрагент,
    Поставить.Объект
ПОМЕСТИТЬ ВТ_МаксДаты
ИЗ
    ВТ_Поставить КАК Поставить
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Снять КАК Снять
        ПО Поставить.Контрагент = Снять.Контрагент
            И Поставить.Объект = Снять.Объект
СГРУППИРОВАТЬ ПО
    Поставить.Контрагент,
    Поставить.Объект
ИМЕЮЩИЕ
    МАКСИМУМ(Поставить.Дата) > ЕСТЬNULL(МАКСИМУМ(Снять.Дата), ДАТАВРЕМЯ(1, 1, 1));

Обработка пустых значений (NULL)

Проанализируем важный нюанс: если объект никогда не снимался с охраны, то в правой таблице (Снять) не будет записей. В результате левого соединения поле Снять.Дата примет значение NULL. Сравнение Дата > NULL всегда будет давать ложный результат, и такой объект не попадет в выборку.

Для решения этой проблемы мы используем функцию ЕСТЬNULL. Мы заменяем отсутствующую дату снятия на минимально возможную дату в системе — ДАТАВРЕМЯ(1, 1, 1). Теперь условие ДатаПостановки > ДатаСнятия отработает корректно даже для новых объектов.

Решение №2: Получение конкретного документа

После того как мы нашли максимальные даты в таблице ВТ_МаксДаты, нам часто нужно получить саму ссылку на документ Поставить, который является последним. Для этого мы соединяем нашу таблицу дат обратно с исходными данными.


ВЫБРАТЬ
    Т.Контрагент,
    Т.Объект,
    Поставить.Ссылка
ИЗ
    ВТ_МаксДаты КАК Т
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Поставить КАК Поставить
        ПО Т.Контрагент = Поставить.Контрагент
            И Т.Объект = Поставить.Объект
            И Т.ДатаПоследнейПостановки = Поставить.Дата

Обратите внимание: Если в одну секунду может быть введено два документа по одному объекту, такое соединение по дате может снова выдать дубли. В реальных задачах для обеспечения уникальности рекомендуется использовать связку даты и ссылки (Момент времени) или проверять дополнительные поля.

Как получить разницу в количестве документов (4 - 2 = 2)?

Если ваша задача состоит не в поиске «последнего состояния», а именно в математическом вычитании количества (например, сколько раз ставили и сколько снимали), то механизм СОЕДИНЕНИЕ вам не подойдет. Здесь лучше использовать ОБЪЕДИНИТЬ ВСЕ с числовыми коэффициентами.

Разберем, как это реализовать:

  1. Выбираем документы «Поставить» и назначаем им количество 1.
  2. Выбираем документы «Снять» и назначаем им количество -1.
  3. Объединяем таблицы и группируем по объекту, суммируя поле «Количество».

ВЫБРАТЬ
    Перечень.Контрагент,
    Перечень.Объект,
    1 КАК Счетчик
ИЗ
    Документ.Перечень КАК Перечень
ГДЕ
    Тип = ЗНАЧЕНИЕ(Перечисление.Тип.Поставить)

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

ВЫБРАТЬ
    Перечень.Контрагент,
    Перечень.Объект,
    -1 КАК Счетчик
ИЗ
    Документ.Перечень КАК Перечень
ГДЕ
    Тип = ЗНАЧЕНИЕ(Перечисление.Тип.Снять)

После группировки этой выборки по полям Контрагент и Объект с использованием функции СУММА(Счетчик), вы получите искомое значение 2. Если сумма больше нуля — объект на охране, если ноль — снят.

Рекомендации по проектированию

Ситуация, когда программисту приходится «выкручиваться» сложными запросами к таблицам документов для получения текущего состояния, часто свидетельствует об архитектурном недочете. В системе 1С для таких целей предназначен объект Регистр сведений.

Если бы данные записывались в периодический регистр сведений СостоянияОбъектов с измерениями Контрагент и Объект, то получение актуального статуса (кто на охране) занимало бы одну строку кода через виртуальную таблицу СрезПоследних. Если у вас есть возможность изменять конфигурацию, создание такого регистра будет самым правильным и производительным решением в долгосрочной перспективе.

Подводя итог: при использовании левого соединения всегда следите за уникальностью ключей. Если ключи не уникальны — используйте предварительную группировку или временные таблицы с агрегатными функциями (МАКСИМУМ, СУММА), чтобы контролировать количество строк в результате.

← На главную