При разработке запросов в системе 1С:Предприятие программисты часто сталкиваются с ситуацией, когда при использовании ЛЕВОЕ СОЕДИНЕНИЕ одна строка из левой таблицы (основной) начинает дублироваться. Это происходит из-за того, что в правой таблице по условию связи находится несколько соответствующих записей. В итоге итоговый результат «раздувается», что портит итоги и мешает корректному выводу данных.
В этой статье мы подробно разберем, как заставить запрос выбирать только одну (например, первую или самую актуальную) запись из правой таблицы, игнорируя остальные подходящие варианты. Мы проанализируем различные подходы — от классических группировок до использования механизмов автонумерации и особенностей работы платформы 1С версий 8.2 и 8.3.
Самый надежный и «старый» способ — это обеспечить уникальность записей в правой таблице до того, как она будет соединена с основной. Если нам не важно, какую именно запись из правой таблицы взять (или если нам нужно взять запись с максимальной датой/номером), мы можем использовать временную таблицу с группировкой.
Рассмотрим алгоритм действий:
МАКСИМУМ или МИНИМУМ.Проанализируем пример: нам нужно получить одну любую накладную для каждого контрагента. В этом случае запрос будет выглядеть так:
ВЫБРАТЬ
Контрагенты.Ссылка КАК Контрагент,
МАКСИМУМ(Продажи.Ссылка) КАК СсылкаНаДокумент
ПОМЕСТИТЬ ВТ_МаксимальныеДокументы
ИЗ
Справочник.Контрагенты КАК Контрагенты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Продажи
ПО Контрагенты.Ссылка = Продажи.Контрагент
ГРУППИРОВАТЬ ПО
Контрагенты.Ссылка
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ТЗ1.Наименование,
ВТ.СсылкаНаДокумент
ИЗ
Справочник.Контрагенты КАК ТЗ1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_МаксимальныеДокументы КАК ВТ
ПО ТЗ1.Ссылка = ВТ.Контрагент
Если критерий «первости» записи сложнее, чем просто максимальный ID или дата, мы можем воспользоваться механизмом автонумерации во временных таблицах. Этот метод позволяет пронумеровать строки внутри групп и затем выбрать только те, где номер равен единице.
Разберем по шагам хитрую схему, предложенную опытными разработчиками:
АвтоНомерЗаписи(). Важно помнить, что 1С требует обязательного наличия конструкции УПОРЯДОЧИТЬ ПО и зачастую ВЫБРАТЬ ПЕРВЫЕ при использовании нумерации.МИНИМУМ(НомерЗаписи).Посмотрим на пример реализации:
ВЫБРАТЬ ПЕРВЫЕ 1000000
Т2.Кол1 КАК КлючСвязи,
Т2.Кол2 КАК Данные,
АВТОНОМЕРЗАПИСИ() КАК Номер
ПОМЕСТИТЬ ВТ_Нумерованная
ИЗ
Таблица2 КАК Т2
УПОРЯДОЧИТЬ ПО
Т2.Кол1,
Т2.Кол2
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ.КлючСвязи,
МИНИМУМ(ВТ.Номер) КАК МинНомер
ПОМЕСТИТЬ ВТ_ТолькоПервые
ИЗ
ВТ_Нумерованная КАК ВТ
ГРУППИРОВАТЬ ПО
ВТ.КлючСвязи
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Т1.Кол1,
ТНумерация.Данные
ИЗ
Таблица1 КАК Т1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ТолькоПервые КАК ТПервые
ПО Т1.Кол1 = ТПервые.КлючСвязи
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Нумерованная КАК ТНумерация
ПО ТПервые.МинНомер = ТНумерация.Номер
Обратите внимание на важный технический нюанс: функция АвтоНомерЗаписи() работает только во временных таблицах. Использование ВЫБРАТЬ ПЕРВЫЕ с очень большим числом необходимо для того, чтобы платформа разрешила использование УПОРЯДОЧИТЬ ПО внутри запроса, создающего временную таблицу.
Этот метод кажется наиболее изящным, так как он позволяет ограничить правую таблицу непосредственно в секции ГДЕ основного запроса. Однако здесь кроется ловушка, связанная с версиями платформы 1С.
Рассмотрим типичный запрос, который часто вызывает споры:
ВЫБРАТЬ
ТЗ1.Кол1 КАК Кол1,
ТЗ2.Кол2 КАК Кол2
ИЗ
ТЗ1 КАК ТЗ1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ТЗ2 КАК ТЗ2
ПО ТЗ1.Кол1 = ТЗ2.Кол1
ГДЕ
(ТЗ2.Кол1, ТЗ2.Кол2) В
(ВЫБРАТЬ ПЕРВЫЕ 1
ТЗ2_Подзапрос.Кол1,
ТЗ2_Подзапрос.Кол2
ИЗ
ТЗ2 КАК ТЗ2_Подзапрос
ГДЕ
ТЗ2_Подзапрос.Кол1 = ТЗ1.Кол1)
Важное наблюдение: Выясним причину, почему в платформе 8.2 этот запрос может выдать две записи вместо одной, а в 8.3 — только одну. Дело в трансляции запроса на уровень SQL. В версии 8.2 такой запрос преобразуется в WHERE EXISTS с коррелированным подзапросом, который проверяет наличие связи, но не ограничивает выборку из T2 в основном соединении эффективно.
В современных версиях платформы (8.3) оптимизатор запросов работает иначе. Он создает вложенный SELECT TOP 1, который корректно сопоставляет каждую строку из T1 только с одной строкой из T2. Тем не менее, для обеспечения 100% стабильности результата рекомендуется всегда добавлять УПОРЯДОЧИТЬ ПО в подзапросе, иначе «первая» запись может выбираться СУБД произвольно.
Если нам нужно вытащить всего одно поле из правой таблицы (например, дату последней продажи или номер телефона), мы можем вообще отказаться от СОЕДИНЕНИЯ в пользу подзапроса в списке ВЫБРАТЬ.
Рассмотрим этот подход:
ВЫБРАТЬ
Т1.Наименование,
(ВЫБРАТЬ ПЕРВЫЕ 1
Т2.Кол2
ИЗ
Таблица2 КАК Т2
ГДЕ
Т2.Кол1 = Т1.Кол1
УПОРЯДОЧИТЬ ПО
Т2.Дата СУББ) КАК ПоследнееЗначение
ИЗ
Таблица1 КАК Т1
Этот метод очень эффективен, если правая таблица имеет правильные индексы. Платформа 8.3 часто транслирует такие запросы в конструкцию OUTER APPLY (на MS SQL Server), что выполняется крайне быстро. Однако, если из правой таблицы нужно получить 5-10 разных колонок, этот метод станет громоздким, так как для каждой колонки придется писать отдельный подзапрос.
Если ваша «правая таблица» является Регистром Сведений, то задача решается максимально просто. Не нужно изобретать велосипед с группировками — используйте виртуальную таблицу СрезПоследних. Платформа 1С автоматически гарантирует, что для каждой уникальной комбинации измерений будет возвращена только одна запись, актуальная на указанную дату.
Проанализировав ситуацию, мы можем выделить следующие правила выбора метода:
ПЕРВЫЕ 1, чтобы результат был предсказуемым и не зависел от случая или настроек СУБД.Таким образом, хотя язык запросов 1С напрямую не позволяет написать LEFT JOIN TOP 1, комбинация временных таблиц, условий фильтрации и подзапросов позволяет элегантно решить задачу ограничения количества записей из правой таблицы.