Часто при разработке на платформе 1С возникает задача: связать главную таблицу (например, Документ1) с подчиненной таблицей (Документ2), но так, чтобы в результате на каждую строку первого документа приходилась ровно одна строка второго. При этом связанных документов может быть несколько, и нам нужно выбрать из них "лучший" по определенному условию — например, сначала искать проведенный документ, а если проведенных нет, то брать любой непроведенный.
Разберем подробнее, почему стандартные соединения здесь не справляются с первого раза, проанализируем поведение агрегатных функций со ссылками и напишем оптимальный пакетный запрос для решения этой задачи.
Первая мысль, которая приходит в голову — сгруппировать данные по первому документу и использовать агрегатную функцию МАКСИМУМ для ссылки на второй документ. Но как платформа сравнивает ссылки, если это не числа и не даты?
Функции МАКСИМУМ() и МИНИМУМ() для ссылочных типов работают на основе сравнения их внутренних уникальных идентификаторов (УИД / UUID). Сравнение ссылок не учитывает дату создания, номер документа или физический порядок записи в базе данных. Для пользователя результат выборки максимальной ссылки может казаться случайным, но технически алгоритм всегда детерминирован.
Использование конструкции МАКСИМУМ(Док.Ссылка) — это стандартный и вполне корректный паттерн в языке запросов 1С, когда нам нужно извлечь «любую одну» строку из группы дублей, и конкретный порядок при этом не важен. Но как быть с приоритетом проведенных документов?
Чтобы гарантированно получить нужный результат (приоритет проведенного документа перед непроведенным) и избежать "размножения" строк при левом соединении, мы применим трехшаговый алгоритм с использованием временных таблиц.
В отличие от T-SQL, где мы могли бы использовать конструкцию коррелированного подзапроса вроде SELECT TOP 1 ... ORDER BY ... прямо в списке выборки, язык запросов 1С не поддерживает ПЕРВЫЕ 1 во вложенных запросах на уровне полей. Поэтому подход с группировкой и временными таблицами остается единственным архитектурно верным способом.
Разберем по шагам алгоритм, который поможет нам решить задачу:
ВЫБОР назначаем проведенным документам приоритет 2, а непроведенным — 1.МАКСИМУМ() к самой ссылке Документа2.Посмотрим на пример пакетного запроса, реализующего описанную выше логику. Данный код универсален и легко адаптируется под ваши объекты конфигурации.
ВЫБРАТЬ
Док1.Ссылка КАК Документ1,
Док2.Ссылка КАК Документ2,
ВЫБОР
КОГДА Док2.Проведен ТОГДА 2
ИНАЧЕ 1
КОНЕЦ КАК Приоритет
ПОМЕСТИТЬ ВТ_ВсеСвязи
ИЗ
Документ.Документ1 КАК Док1
ЛЕВОЕ СОЕДИНЕНИЕ Документ.Документ2 КАК Док2
ПО Док1.Ссылка = Док2.ДокументОснование
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_ВсеСвязи.Документ1 КАК Документ1,
МАКСИМУМ(ВТ_ВсеСвязи.Приоритет) КАК МаксимальныйПриоритет
ПОМЕСТИТЬ ВТ_МаксПриоритет
ИЗ
ВТ_ВсеСвязи КАК ВТ_ВсеСвязи
СГРУППИРОВАТЬ ПО
ВТ_ВсеСвязи.Документ1
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_МаксПриоритет.Документ1 КАК Документ1,
МАКСИМУМ(ВТ_ВсеСвязи.Документ2) КАК Документ2
ИЗ
ВТ_МаксПриоритет КАК ВТ_МаксПриоритет
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ВсеСвязи КАК ВТ_ВсеСвязи
ПО ВТ_МаксПриоритет.Документ1 = ВТ_ВсеСвязи.Документ1
И ВТ_МаксПриоритет.МаксимальныйПриоритет = ВТ_ВсеСвязи.Приоритет
СГРУППИРОВАТЬ ПО
ВТ_МаксПриоритет.Документ1
Проанализируем этот код. В первом запросе пакета мы собираем все возможные комбинации и проставляем маркер Приоритет. Во втором запросе находим, какой наивысший приоритет доступен для каждого отдельного Документа1. В третьем — отсекаем документы с более низким приоритетом и сворачиваем оставшиеся дубли с помощью агрегатной функции МАКСИМУМ.
Часто разработчики пытаются решить подобную задачу объединением множества левых соединений, вложенных конструкций ВЫБОР и проверок ЕСТЬNULL в одном огромном запросе. Это серьезный антипаттерн в 1С.
Выясним причину, почему такие гигантские запросы вредны:
ИЛИ, подзапросов и вложенных условий заставляют планировщик СУБД строить неэффективные маршруты доступа к данным, что приводит к многократным сканированиям таблиц и замедлению работы.СГРУППИРОВАТЬ в самом конце выборки.Правильный подход — дробление сложного запроса на логические блоки с помощью временных таблиц (как мы продемонстрировали выше). Сначала подготовьте компактные справочники соответствий «один к одному» (например, "Документ1 — Один лучший Документ2") в отдельные ВТ. И только в самом конце выполните простой, плоский запрос, соединив основную выборку с этими подготовленными ВТ. Это не только сделает код изящным, но и значительно повысит производительность системы.