При разработке запросов в системе 1С:Предприятие программисты часто сталкиваются с классической проблемой: при использовании ЛЕВОЕ СОЕДИНЕНИЕ одна строка из основной таблицы превращается в несколько, если в правой (присоединяемой) таблице найдено более одного совпадения. В стандартном SQL для решения этой задачи существуют конструкции типа CROSS APPLY или OUTER APPLY, однако язык запросов 1С их не поддерживает. В этой статье мы подробно разберем, как реализовать логику "выбрать первую подходящую запись" различными способами, проанализируем их влияние на производительность и выясним, какой метод лучше использовать в конкретных ситуациях.
Разберем ситуацию на примере. Допустим, у нас есть таблица-источник и справочник Партнеры. В справочнике по какой-то причине (ошибки миграции, дубли) один и тот же код соответствует нескольким элементам. При обычном левом соединении по коду СУБД вернет столько строк для одного кода, сколько ссылок она нашла в справочнике. Наша задача — ограничить выборку так, чтобы для каждой записи слева осталась только одна запись справа.
Самый простой и быстрый в реализации способ — предварительно сгруппировать правую таблицу во вложенном запросе (это легко сделать, используя консоль запросов для управляемых форм) — для этого пригодится универсальная консоль запросов и СКД. Рассмотрим этот метод на примере связи по коду.
Проанализируем структуру запроса:
ВЫБРАТЬ
Источник.Ссылка,
СпрПартнеры.Ссылка КАК ПартнерСсылка
ИЗ
ТаблицаИсточник КАК Источник
ЛЕВОЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ
МАКСИМУМ(Партнеры.Ссылка) КАК Ссылка,
Партнеры.ИсточникКод КАК ИсточникКод
ИЗ
Справочник.Партнеры КАК Партнеры
СГРУППИРОВАТЬ ПО
Партнеры.ИсточникКод
) КАК СпрПартнеры
ПО Источник.Код = СпрПартнеры.ИсточникКод
Важный нюанс: использование функции МАКСИМУМ(Ссылка) позволяет нам гарантированно получить только одно уникальное значение для каждого кода. Ссылка в 1С — это уникальный идентификатор (GUID), и хотя функция МАКСИМУМ для ссылок не несет бизнес-логики (она выберет "старший" GUID), для технического устранения дублей это идеальный вариант.
Согласно стандартам разработки 1С, использование вложенных запросов в соединениях крайне не рекомендуется, особенно если речь идет о соединении с виртуальными таблицами или большими физическими таблицами. Рассмотрим, как переписать решение через временную таблицу.
Разберем по шагам:
// Шаг 1: Подготовка уникальных данных
ВЫБРАТЬ
МАКСИМУМ(Партнеры.Ссылка) КАК Ссылка,
Партнеры.ИсточникКод КАК ИсточникКод
ПОМЕСТИТЬ ВТ_УникальныеПартнеры
ИЗ
Справочник.Партнеры КАК Партнеры
СГРУППИРОВАТЬ ПО
Партнеры.ИсточникКод
ИНДЕКСИРОВАТЬ ПО
ИсточникКод
;
// Шаг 2: Основной запрос
ВЫБРАТЬ
Источник.Ссылка,
ВТ.Ссылка КАК Партнер
ИЗ
ТаблицаИсточник КАК Источник
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_УникальныеПартнеры КАК ВТ
ПО Источник.Код = ВТ.ИсточникКод
Использование ИНДЕКСИРОВАТЬ ПО является критически важным. Без индекса на больших объемах данных (более 1000 строк) производительность ЛЕВОЕ СОЕДИНЕНИЕ упадет в разы, так как СУБД придется выполнять регламентные операции с индексами при каждой итерации соединения.
Иногда нам нужно выбрать не просто любую запись из дублей, а конкретную (например, самую новую по дате создания). В этом случае МАКСИМУМ(Ссылка) нам не поможет. Выясним, как решаются эти задачи через формирование "веса".
Рассмотрим алгоритм:
МАКСИМУМ от этого "веса" для каждого ключа связи.Если мы работаем со строками, это выглядит так: МАКСИМУМ(ПОДСТРОКА(ПредставлениеДаты, 1, 19) + Ссылка). После группировки мы отсекаем дату и получаем ссылку, которая была создана последней.
Ситуация усложняется, когда задачу нужно решить в динамическом списке (например, вывести в списке документов наличие подчиненного документа). Вложенные запросы внутри запроса динамического списка могут привести к "тормозам" при прокрутке. Рассмотрим пример из практики:
ВЫБРАТЬ РАЗЛИЧНЫЕ
Заявка.Ссылка КАК Ссылка,
МАКСИМУМ(ЕСТЬNULL(Маршруты.Ссылка.Проведен, ЛОЖЬ)) КАК ЕстьТранспортировка
ИЗ
Документ.ЗаявкаНаТранспортировку КАК Заявка
ЛЕВОЕ СОЕДИНЕНИЕ Документ.Транспортировка.Маршруты КАК Маршруты
ПО Заявка.Ссылка = Маршруты.ДокументЗаявки
СГРУППИРОВАТЬ ПО
Заявка.Ссылка
В данном случае мы используем группировку по основной ссылке документа. Если у одной заявки есть три маршрута, ЛЕВОЕ СОЕДИНЕНИЕ создаст три строки, но СГРУППИРОВАТЬ ПО Заявка.Ссылка вместе с функцией МАКСИМУМ "схлопнет" их обратно в одну. Это корректный и производительный способ для форм списков.
Проанализируем распространенную ошибку начинающих разработчиков. Попытка написать ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ ПЕРВЫЕ 1 ... ПОДРЯД) приведет к тому, что во всей правой части соединения останется ровно одна запись для всего отчета, а не для каждой строки левой таблицы. Это происходит из-за того, что в 1С вложенный запрос выполняется один раз и его результат помещается в память (или TempDB) целиком, прежде чем начнется процедура соединения. Поэтому единственный способ имитации "Первые 1 для каждой строки" — это агрегатные функции МАКСИМУМ или МИНИМУМ с обязательной группировкой по полям связи.
С точки зрения оптимизатора СУБД (например, MS SQL Server), выбор между вложенным запросом и временной таблицей сводится к следующему:
Hash Join. Это быстро, но требует много оперативной памяти. Если данных много, SQL Server может ошибиться с планом, решив, что во вложенном запросе мало строк.Подводя итог, можно сказать: если вы работаете с небольшими объемами (сотни строк) или пишите запрос для динамического списка — используйте группировку в основном запросе. Если же вы обрабатываете тысячи и миллионы строк в фоновом задании или тяжелом отчете — обязательно выносите правую часть соединения во временную таблицу с индексацией. Для контроля и анализа таких тяжелых запросов пригодится инструмент мониторинга производительности и планов запросов СУБД.