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