Два запроса — один результат: все способы в языке запросов 1С
Содержание
Соединить два запроса в языке запросов 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), для строк — на пустую строку.Когда брать временную таблицу вместо вложенного запроса?
ИНДЕКСИРОВАТЬ ПО, вложенный запрос — нет, поэтому на больших данных временная таблица заметно быстрее.