Как эффективно объединить таблицы в запросе 1С по нескольким полям?

Программист
← На главную

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

Проанализируем следующую задачу: есть справочник Товары и регистр сведений ТоварыПоставщиков. В регистре хранятся записи о контрагентах и товарах, причем товар может быть указан либо в реквизите Ссылка1, либо в Ссылка2. Нам нужно получить список всех товаров из справочника и связанных с ними контрагентов из регистра, независимо от того, в каком из полей (Ссылка1 или Ссылка2) находится ссылка на товар.

Решение "в лоб": использование оператора ИЛИ в условии соединения

Первое, что приходит на ум, — это использовать логический оператор ИЛИ прямо в условии соединения таблиц. Такой подход выглядит логично и компактно. Посмотрим на пример кода.


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

Этот запрос действительно вернет нужный результат. Он выберет все записи из регистра, где хотя бы одно из полей Ссылка1 или Ссылка2 совпадает со ссылкой на товар. Однако у этого решения есть серьезный скрытый недостаток — крайне низкая производительность на больших объемах данных.

Почему этот подход медленный?

Причина кроется в том, как система управления базами данных (СУБД) обрабатывает такие условия. Использование оператора ИЛИ в секции ПО (ON в SQL) часто мешает СУБД эффективно использовать индексы, созданные для полей Ссылка1 и Ссылка2. Вместо быстрого поиска по индексу, СУБД может прибегнуть к полному сканированию одной или обеих таблиц (Table Scan), что является очень медленной операцией. Как показывает практика, время выполнения такого запроса может увеличиться в сотни и даже тысячи раз. В обсуждаемом на форуме случае разница составила полсекунды против пяти минут — колоссальное падение производительности.

Оптимальное решение: разбиение запроса с помощью ОБЪЕДИНИТЬ ВСЕ

Чтобы избежать проблем с производительностью, следует отказаться от ИЛИ в условии соединения. Правильный и быстрый способ — это разделить сложный запрос на несколько простых и объединить их результаты. В нашем случае мы создадим два отдельных запроса: один будет соединять таблицы по полю Ссылка1, а второй — по полю Ссылка2. Затем мы объединим их результаты с помощью оператора ОБЪЕДИНИТЬ ВСЕ.

Рассмотрим, как будет выглядеть этот запрос:


ВЫБРАТЬ
    Товары.Ссылка,
    РегТовары1.Контрагент
ИЗ
    РегистрСведений.ТоварыПоставщиков КАК РегТовары1
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Товары КАК Товары
        ПО РегТовары1.Ссылка1 = Товары.Ссылка

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

ВЫБРАТЬ
    Товары.Ссылка,
    РегТовары2.Контрагент
ИЗ
    РегистрСведений.ТоварыПоставщиков КАК РегТовары2
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Товары КАК Товары
        ПО РегТовары2.Ссылка2 = Товары.Ссылка

Почему этот вариант работает быстро?

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

Этот подход, хоть и делает текст запроса более громоздким, является стандартом для оптимизации подобных задач и гарантирует высокую производительность.

Дополнительные аспекты оптимизации

В ходе обсуждения были затронуты и другие важные моменты, связанные с производительностью запросов. Давайте их кратко разберем.

Предварительная фильтрация данных

Существует распространенное мнение, что данные нужно фильтровать как можно раньше. Например, поместить одну из таблиц в подзапрос с жесткими условиями в секции ГДЕ, а затем соединять с этим подзапросом.


ВЫБРАТЬ
    Т.Ссылка,
    Рег.Контрагент
ИЗ
    (ВЫБРАТЬ
        Ссылка1, Ссылка2, Контрагент
    ИЗ
        РегистрСведений.ТоварыПоставщиков
    ГДЕ
        // Жесткие условия отбора
    ) КАК Рег
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ...

Однако, как показывает практика, это не всегда ускоряет запрос. Иногда СУБД строит более оптимальный план выполнения, когда видит полные таблицы и все условия сразу, а не "спрятанные" в подзапросе. Единственный способ узнать наверняка — это замерить оба варианта или проанализировать план выполнения запроса. Не всегда интуитивные оптимизации приводят к лучшему результату.

Использование временных таблиц

В сложных, многоуровневых запросах, особенно если вы многократно обращаетесь к одним и тем же данным (например, к виртуальным таблицам остатков), полезно выносить промежуточные результаты во временные таблицы. Это может не только улучшить читаемость кода, но и помочь СУБД построить более эффективный план, так как для временных таблиц собирается статистика, и они могут быть проиндексированы.

Вывод

При необходимости соединить таблицы по нескольким альтернативным полям, всегда отдавайте предпочтение разделению запроса на части с последующим объединением через ОБЪЕДИНИТЬ ВСЕ. Избегайте использования оператора ИЛИ в условиях соединения (секция ПО), так как это почти гарантированно приведет к серьезным проблемам с производительностью на реальных объемах данных.

← На главную