Два запроса — один результат: все способы в языке запросов 1С

· 10 мин

Содержание

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

Выбор между ними зависит от одного вопроса: вам нужны дополнительные строки или дополнительные колонки? Ниже — синтаксис обоих операторов, разбор внутреннего и левого соединения, вложенные и временные таблицы, работа с виртуальными таблицами регистров и ошибки, на которых чаще всего спотыкаются.

Все примеры — на русскоязычном языке запросов платформы 8.3. Собирать и проверять их удобно в консоли запросов (внешняя обработка «Консоль запросов» из комплекта ИТС или аналог в расширении конфигурации).

ОБЪЕДИНИТЬ и ОБЪЕДИНИТЬ ВСЕ: склеиваем наборы строк

ОБЪЕДИНИТЬ берёт результат второго запроса и дописывает его строки под строки первого. Классический случай — свести в один список данные из разных источников: например, поступления и реализации, или остатки на двух складах, посчитанные отдельно.

ВЫБРАТЬ
    Поступление.Номенклатура КАК Номенклатура,
    Поступление.Количество КАК Приход,
    0 КАК Расход
ИЗ
    Документ.ПоступлениеТоваровУслуг.Товары КАК Поступление

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

ВЫБРАТЬ
    Реализация.Номенклатура,
    0,
    Реализация.Количество
ИЗ
    Документ.РеализацияТоваровУслуг.Товары КАК Реализация

Два жёстких требования к объединяемым запросам:

  • 📐 Одинаковое число колонок в каждом запросе. Семь колонок слева — ровно семь справа.
  • 🔗 Совместимые типы в соответствующих позициях. Число встаёт под число, ссылка — под ссылку. Если в первом запросе колонка числовая, а во втором на её месте строка — платформа объединит их в составной тип, и дальше с таким полем работать неудобно.

Имена колонок результат берёт из первого запроса — псевдонимы во втором и последующих запросах можно не писать, они игнорируются. Поэтому КАК Номенклатура, КАК Приход, КАК Расход указаны только в первом блоке.

Чтобы «добить» недостающие колонки, подставляют константы-заглушки: 0 для числа, "" для строки, ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка) для ссылки.

Чем отличается ОБЪЕДИНИТЬ от ОБЪЕДИНИТЬ ВСЕ

Разница в одном слове, но она важная:

ОператорЧто делаетКогда брать
ОБЪЕДИНИТЬ ВСЕСклеивает строки как есть, ничего не проверяяПочти всегда — быстрее и предсказуемее
ОБЪЕДИНИТЬСклеивает и убирает полные дубли строкТолько когда дубли реально нужно схлопнуть

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

УПОРЯДОЧИТЬ ПО и ИТОГИ относятся ко всему объединённому результату и пишутся один раз, в самом конце последнего запроса. Нельзя упорядочить каждый подзапрос отдельно — будет ошибка синтаксиса.

Внутреннее соединение: только совпавшие строки

СОЕДИНЕНИЕ работает иначе: оно не добавляет строки, а подтягивает к строкам одной таблицы колонки из другой по условию связи. Самый строгий вариант — ВНУТРЕННЕЕ СОЕДИНЕНИЕ (аналог INNER JOIN).

ВЫБРАТЬ
    Заказы.Ссылка КАК Заказ,
    Заказы.Контрагент КАК Контрагент,
    Оплаты.Сумма КАК СуммаОплаты
ИЗ
    Документ.ЗаказПокупателя КАК Заказы
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.ВзаиморасчётыСКлиентами КАК Оплаты
        ПО Заказы.Ссылка = Оплаты.Заказ

В результат попадут только те заказы, по которым есть запись об оплате. Если по заказу платежей нет — строка отсекается целиком. Внутреннее соединение всегда уменьшает (или оставляет без изменений) количество строк левой таблицы — оно отбирает пересечение двух наборов по ключу.

Условие связи задаётся после ПО. Полей в условии может быть несколько — соединяют через И:

        ПО Заказы.Организация = Оплаты.Организация
            И Заказы.Контрагент = Оплаты.Контрагент

Внутреннее соединение берут, когда строки без пары в принципе не нужны: расчёт только по оплаченным заказам, сопоставление строго совпавших позиций, выборка документов, у которых точно есть движения.

Левое соединение: сохраняем все строки основной таблицы

А вот если нужно вывести все заказы, включая неоплаченные, и просто показать сумму оплаты там, где она есть, — берут ЛЕВОЕ СОЕДИНЕНИЕ (LEFT JOIN).

ВЫБРАТЬ
    Заказы.Ссылка КАК Заказ,
    Заказы.Контрагент КАК Контрагент,
    ЕСТЬNULL(Оплаты.Сумма, 0) КАК СуммаОплаты
ИЗ
    Документ.ЗаказПокупателя КАК Заказы
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ВзаиморасчётыСКлиентами КАК Оплаты
        ПО Заказы.Ссылка = Оплаты.Заказ

Левое соединение сохраняет все строки той таблицы, что стоит слева от оператора (в примере — заказы). Если пары справа нет, колонки правой таблицы заполняются значением NULL.

Вот здесь — главная ловушка. NULL — это не ноль и не пустая ссылка, это «значение отсутствует». Арифметика с ним даёт NULL: 5 + NULL вернёт NULL, а не 5. Поэтому числовые поля из правой таблицы почти всегда оборачивают в функцию ЕСТЬNULL:

ЕСТЬNULL(Оплаты.Сумма, 0)

Первый аргумент — поле, второй — чем заменить, если там NULL. Для сумм подставляют 0, для строк — "".

Есть ещё ПРАВОЕ СОЕДИНЕНИЕ (зеркало левого) и ПОЛНОЕ СОЕДИНЕНИЕ (сохраняет строки обеих таблиц, незаполненное — NULL). На практике правое почти не используют: его всегда можно переписать как левое, поменяв таблицы местами, и читается это понятнее.

Что именно нужно получить из двух запросов?

Вложенные запросы и временные таблицы

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

Вложенный запрос пишется прямо в секции ИЗ, в скобках, с обязательным псевдонимом:

ВЫБРАТЬ
    Данные.Номенклатура КАК Номенклатура,
    Данные.Остаток КАК Остаток
ИЗ
    (ВЫБРАТЬ
        Остатки.Номенклатура КАК Номенклатура,
        СУММА(Остатки.КоличествоОстаток) КАК Остаток
    ИЗ
        РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
    СГРУППИРОВАТЬ ПО
        Остатки.Номенклатура) КАК Данные
ГДЕ
    Данные.Остаток > 0

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

Временная таблица лишена этого минуса. Сначала запрос с конструкцией ПОМЕСТИТЬ сохраняет промежуточный результат, а следующий запрос пакета обращается к нему как к обычной таблице:

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

ВЫБРАТЬ
    ВТ_Остатки.Номенклатура КАК Номенклатура,
    ВТ_Остатки.Остаток КАК Остаток
ИЗ
    ВТ_Остатки КАК ВТ_Остатки
        ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК Ном
        ПО ВТ_Остатки.Номенклатура = Ном.Ссылка

Два запроса разделены точкой с запятой — это пакетный запрос. Чтобы временные таблицы жили между запросами пакета, объекту Запрос нужно назначить менеджер временных таблиц:

Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст = ТекстПакета;
Результат = Запрос.Выполнить();

Конструкция ИНДЕКСИРОВАТЬ ПО в примере — не для красоты. Если по полю временной таблицы потом идёт соединение или отбор, индекс по этому полю заметно ускоряет выполнение второго запроса.

Правило простое: разовый мелкий подзапрос — вложенный; данные, к которым потом присоединяются другие таблицы или идёт отбор, — временная таблица с индексом по полю связи.

Дубликаты и производительность при объединении

Лишние строки после соединения появляются обычно по одной причине: условие связи неполное. Если одной строке слева соответствует несколько строк справа, левая строка «размножится» — выведется столько раз, сколько нашлось пар.

Типичный пример — соединяете шапку документа с его табличной частью: одна шапка, пять строк товаров, в результате шапочные поля повторятся пять раз. Это не ошибка платформы, а следствие связи «один ко многим». Варианты:

  • ✅ Добавить в условие ПО недостающие поля, чтобы связь стала «один к одному».
  • ✅ Если детализация не нужна — сгруппировать результат через СГРУППИРОВАТЬ ПО с агрегатными функциями (СУММА, МАКСИМУМ).
  • ✅ Если нужны именно уникальные строки без агрегации — применить ВЫБРАТЬ РАЗЛИЧНЫЕ.

Несколько приёмов, которые держат соединения быстрыми:

  • Соединяйте по ссылкам и измерениям, а не по строковым реквизитам. Сравнение строк медленнее и чувствительно к регистру и пробелам.
  • Избегайте соединения с вложенным запросом справа — его нельзя проиндексировать. Выносите такой подзапрос во временную таблицу.
  • Не соединяйте две виртуальные таблицы напрямую — об этом ниже.
  • Отбор по данным левой таблицы ставьте в ГДЕ, а параметры виртуальных таблиц — внутрь самих таблиц, а не в ГДЕ.

Соединения с виртуальными таблицами регистров

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

Первое — параметры отбора передавайте внутрь скобок виртуальной таблицы, а не в секцию ГДЕ:

// Правильно: отбор уходит внутрь, СУБД читает меньше данных
РегистрНакопления.ТоварыНаСкладах.Остатки(
    &НаДату,
    Склад = &Склад
        И Номенклатура В (&СписокНоменклатуры)) КАК Остатки

Если тот же отбор написать в ГДЕ, платформа сначала развернёт остатки по всем складам и товарам, а потом отфильтрует — это лишняя работа на большой базе.

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

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

// Шаг 2: обороты — в свою временную таблицу
ВЫБРАТЬ ... ПОМЕСТИТЬ ВТ_Обороты
ИЗ РегистрНакопления.ТоварыНаСкладах.Обороты(&Начало, &Конец,) КАК Обороты
ИНДЕКСИРОВАТЬ ПО Номенклатура;

// Шаг 3: соединяем уже временные таблицы
ВЫБРАТЬ ...
ИЗ ВТ_Остатки КАК О
    ПОЛНОЕ СОЕДИНЕНИЕ ВТ_Обороты КАК Об
    ПО О.Номенклатура = Об.Номенклатура

Если в одном регистре нужны и остатки, и обороты сразу — не городите два обращения, возьмите готовую виртуальную таблицу ОстаткиИОбороты. Она отдаёт и то, и другое одним проходом.

Частые ошибки при объединении данных

Разберём то, что ломает запрос или искажает цифры и не всегда очевидно из текста ошибки.

Диагностика: выберите симптом

Ещё две частые путаницы по сути:

  • ОБЪЕДИНИТЬ вместо СОЕДИНЕНИЕ и наоборот. Нужны новые строки — объединение; нужны новые колонки по ключу — соединение. Если в результате не хватает колонок — вы взяли не тот инструмент.
  • Псевдонимы во втором запросе объединения. Писать их не запрещено, но имена колонок всё равно берутся из первого запроса. Опираться на псевдоним из второго блока — ошибка.

Частые вопросы

Чем ОБЪЕДИНИТЬ отличается от СОЕДИНЕНИЯ простыми словами?
ОБЪЕДИНИТЬ ставит результаты друг под друга — растёт число строк. СОЕДИНЕНИЕ ставит их рядом по общему полю — растёт число колонок. Это два разных действия, не синонимы.
Что быстрее — ОБЪЕДИНИТЬ или ОБЪЕДИНИТЬ ВСЕ?
ОБЪЕДИНИТЬ ВСЕ быстрее: он просто складывает строки. ОБЪЕДИНИТЬ дополнительно ищет и убирает полные дубли, а это лишняя сортировка. Если дублей быть не может — всегда берите ОБЪЕДИНИТЬ ВСЕ.
Можно ли объединить больше двух запросов?
Да, число запросов в цепочке ОБЪЕДИНИТЬ ВСЕ не ограничено. Главное — во всех одинаковое количество колонок и совместимые типы. УПОРЯДОЧИТЬ ПО пишется один раз в конце последнего запроса.
Почему после ЛЕВОГО СОЕДИНЕНИЯ появились пустые значения?
Это NULL в строках, у которых не нашлось пары в правой таблице. Для чисел замените их на ноль функцией ЕСТЬNULL(Поле, 0), для строк — на пустую строку.
Когда брать временную таблицу вместо вложенного запроса?
Когда к промежуточному результату дальше присоединяются другие таблицы или по нему идёт отбор. Временную таблицу можно проиндексировать через ИНДЕКСИРОВАТЬ ПО, вложенный запрос — нет, поэтому на больших данных временная таблица заметно быстрее.