В разработке на платформе 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 = Товары.Ссылка и РегТовары2.Ссылка2 = Товары.Ссылка). СУБД может легко использовать существующие индексы для этих полей, что обеспечивает максимальную скорость выполнения каждого отдельного соединения.ОБЪЕДИНИТЬ ВСЕ просто "склеивает" результаты двух запросов. Он работает быстрее, чем его аналог ОБЪЕДИНИТЬ, потому что не выполняет дополнительную операцию по поиску и удалению дубликатов строк. В нашей задаче дубликаты маловероятны (если одна и та же запись регистра не ссылается на один и тот же товар в обоих полях, что является скорее ошибкой данных), поэтому ОБЪЕДИНИТЬ ВСЕ — идеальный выбор.Этот подход, хоть и делает текст запроса более громоздким, является стандартом для оптимизации подобных задач и гарантирует высокую производительность.
В ходе обсуждения были затронуты и другие важные моменты, связанные с производительностью запросов. Давайте их кратко разберем.
Предварительная фильтрация данных
Существует распространенное мнение, что данные нужно фильтровать как можно раньше. Например, поместить одну из таблиц в подзапрос с жесткими условиями в секции ГДЕ, а затем соединять с этим подзапросом.
ВЫБРАТЬ
Т.Ссылка,
Рег.Контрагент
ИЗ
(ВЫБРАТЬ
Ссылка1, Ссылка2, Контрагент
ИЗ
РегистрСведений.ТоварыПоставщиков
ГДЕ
// Жесткие условия отбора
) КАК Рег
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ...
Однако, как показывает практика, это не всегда ускоряет запрос. Иногда СУБД строит более оптимальный план выполнения, когда видит полные таблицы и все условия сразу, а не "спрятанные" в подзапросе. Единственный способ узнать наверняка — это замерить оба варианта или проанализировать план выполнения запроса. Не всегда интуитивные оптимизации приводят к лучшему результату.
Использование временных таблиц
В сложных, многоуровневых запросах, особенно если вы многократно обращаетесь к одним и тем же данным (например, к виртуальным таблицам остатков), полезно выносить промежуточные результаты во временные таблицы. Это может не только улучшить читаемость кода, но и помочь СУБД построить более эффективный план, так как для временных таблиц собирается статистика, и они могут быть проиндексированы.
Вывод
При необходимости соединить таблицы по нескольким альтернативным полям, всегда отдавайте предпочтение разделению запроса на части с последующим объединением через ОБЪЕДИНИТЬ ВСЕ. Избегайте использования оператора ИЛИ в условиях соединения (секция ПО), так как это почти гарантированно приведет к серьезным проблемам с производительностью на реальных объемах данных.