Как в запросе 1С при левом соединении выбрать только одну связанную запись и избежать дублей

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке запросов в системе 1С:Предприятие программисты часто сталкиваются с классической проблемой: при использовании ЛЕВОЕ СОЕДИНЕНИЕ одна строка из основной таблицы превращается в несколько, если в правой (присоединяемой) таблице найдено более одного совпадения. В стандартном SQL для решения этой задачи существуют конструкции типа CROSS APPLY или OUTER APPLY, однако язык запросов 1С их не поддерживает. В этой статье мы подробно разберем, как реализовать логику "выбрать первую подходящую запись" различными способами, проанализируем их влияние на производительность и выясним, какой метод лучше использовать в конкретных ситуациях.

Причина возникновения проблемы

Разберем ситуацию на примере. Допустим, у нас есть таблица-источник и справочник Партнеры. В справочнике по какой-то причине (ошибки миграции, дубли) один и тот же код соответствует нескольким элементам. При обычном левом соединении по коду СУБД вернет столько строк для одного кода, сколько ссылок она нашла в справочнике. Наша задача — ограничить выборку так, чтобы для каждой записи слева осталась только одна запись справа.

Решение 1: Использование вложенного запроса с группировкой

Самый простой и быстрый в реализации способ — предварительно сгруппировать правую таблицу во вложенном запросе (это легко сделать, используя консоль запросов для управляемых форм) — для этого пригодится универсальная консоль запросов и СКД. Рассмотрим этот метод на примере связи по коду.

Проанализируем структуру запроса:


ВЫБРАТЬ
    Источник.Ссылка,
    СпрПартнеры.Ссылка КАК ПартнерСсылка
ИЗ
    ТаблицаИсточник КАК Источник
    ЛЕВОЕ СОЕДИНЕНИЕ (
        ВЫБРАТЬ
            МАКСИМУМ(Партнеры.Ссылка) КАК Ссылка,
            Партнеры.ИсточникКод КАК ИсточникКод
        ИЗ
            Справочник.Партнеры КАК Партнеры
        СГРУППИРОВАТЬ ПО
            Партнеры.ИсточникКод
    ) КАК СпрПартнеры
    ПО Источник.Код = СпрПартнеры.ИсточникКод

Важный нюанс: использование функции МАКСИМУМ(Ссылка) позволяет нам гарантированно получить только одно уникальное значение для каждого кода. Ссылка в 1С — это уникальный идентификатор (GUID), и хотя функция МАКСИМУМ для ссылок не несет бизнес-логики (она выберет "старший" GUID), для технического устранения дублей это идеальный вариант.

Решение 2: Использование временных таблиц (Стандарт 1С)

Согласно стандартам разработки 1С, использование вложенных запросов в соединениях крайне не рекомендуется, особенно если речь идет о соединении с виртуальными таблицами или большими физическими таблицами. Рассмотрим, как переписать решение через временную таблицу.

Разберем по шагам:

  1. Выбираем уникальные связки из правой таблицы и помещаем их во временную таблицу.
  2. Индексируем временную таблицу по полю связи для ускорения соединения (что поддерживает консоль запросов УФ).
  3. Соединяем основную таблицу с подготовленной временной таблицей.

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

// Шаг 2: Основной запрос
ВЫБРАТЬ
    Источник.Ссылка,
    ВТ.Ссылка КАК Партнер
ИЗ
    ТаблицаИсточник КАК Источник
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_УникальныеПартнеры КАК ВТ
    ПО Источник.Код = ВТ.ИсточникКод

Использование ИНДЕКСИРОВАТЬ ПО является критически важным. Без индекса на больших объемах данных (более 1000 строк) производительность ЛЕВОЕ СОЕДИНЕНИЕ упадет в разы, так как СУБД придется выполнять регламентные операции с индексами при каждой итерации соединения.

Решение 3: Метод "Веса" для выбора конкретной записи (последней или первой)

Иногда нам нужно выбрать не просто любую запись из дублей, а конкретную (например, самую новую по дате создания). В этом случае МАКСИМУМ(Ссылка) нам не поможет. Выясним, как решаются эти задачи через формирование "веса".

Рассмотрим алгоритм:

  1. Создаем временную таблицу, где объединяем критерий сортировки (например, Дату) и Ссылку в одну строку или число.
  2. Выбираем МАКСИМУМ от этого "веса" для каждого ключа связи.
  3. В финальном запросе "расшифровываем" вес обратно, получая нужную ссылку.

Если мы работаем со строками, это выглядит так: МАКСИМУМ(ПОДСТРОКА(ПредставлениеДаты, 1, 19) + Ссылка). После группировки мы отсекаем дату и получаем ссылку, которая была создана последней.

Особенности работы в Динамических списках

Ситуация усложняется, когда задачу нужно решить в динамическом списке (например, вывести в списке документов наличие подчиненного документа). Вложенные запросы внутри запроса динамического списка могут привести к "тормозам" при прокрутке. Рассмотрим пример из практики:


ВЫБРАТЬ РАЗЛИЧНЫЕ
    Заявка.Ссылка КАК Ссылка,
    МАКСИМУМ(ЕСТЬNULL(Маршруты.Ссылка.Проведен, ЛОЖЬ)) КАК ЕстьТранспортировка
ИЗ
    Документ.ЗаявкаНаТранспортировку КАК Заявка
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.Транспортировка.Маршруты КАК Маршруты
    ПО Заявка.Ссылка = Маршруты.ДокументЗаявки
СГРУППИРОВАТЬ ПО
    Заявка.Ссылка

В данном случае мы используем группировку по основной ссылке документа. Если у одной заявки есть три маршрута, ЛЕВОЕ СОЕДИНЕНИЕ создаст три строки, но СГРУППИРОВАТЬ ПО Заявка.Ссылка вместе с функцией МАКСИМУМ "схлопнет" их обратно в одну. Это корректный и производительный способ для форм списков.

Почему ПЕРВЫЕ 1 не работает во вложенном запросе?

Проанализируем распространенную ошибку начинающих разработчиков. Попытка написать ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ ПЕРВЫЕ 1 ... ПОДРЯД) приведет к тому, что во всей правой части соединения останется ровно одна запись для всего отчета, а не для каждой строки левой таблицы. Это происходит из-за того, что в 1С вложенный запрос выполняется один раз и его результат помещается в память (или TempDB) целиком, прежде чем начнется процедура соединения. Поэтому единственный способ имитации "Первые 1 для каждой строки" — это агрегатные функции МАКСИМУМ или МИНИМУМ с обязательной группировкой по полям связи.

Влияние на план запроса SQL Server

С точки зрения оптимизатора СУБД (например, MS SQL Server), выбор между вложенным запросом и временной таблицей сводится к следующему:

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

← На главную