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