Соединение в запросе 1С собирает в одну строку данные из нескольких таблиц: остаток товара с ценой, номенклатуру со штрихкодом, заказ с оплатой. Ниже — все виды на одной схеме «до/после», готовые функции на остатках и ценах и ошибки, из-за которых строки размножаются, пропадают или становятся NULL. Соединения в запросе 1С 8.3 устроены одинаково в любой конфигурации, имена регистров в примерах — из УТ 11.4, в КА 2.5 они такие же.
Здесь речь о соединении таблиц внутри запроса. Подключение к другой базе описано в статье о COM-соединении.
Соединение пишут в секции ИЗ: первая таблица, вид соединения, вторая таблица с псевдонимом и условие после ПО. Условий может быть несколько через И, в них допустимы параметры и сравнения.
// Остатки склада и цена выбранного вида: товар без цены останется, в поле Цена будет NULL
ВЫБРАТЬ
Остатки.Номенклатура КАК Номенклатура,
Остатки.Характеристика КАК Характеристика,
Остатки.ВНаличииОстаток КАК Количество,
Цены.Цена КАК Цена
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&НаДату, ВидЦены = &ВидЦены) КАК Цены
ПО Остатки.Номенклатура = Цены.Номенклатура
И Остатки.Характеристика = Цены.Характеристика
Товар без цены останется в выборке с NULL в поле «Цена». Замените ЛЕВОЕ на ВНУТРЕННЕЕ — и он исчезнет. Проверить удобно в бесплатной консоли запросов.
В конструкторе запроса (контекстное меню модуля → «Конструктор запроса…») соединения задают на вкладке «Связи»: флажок «Все» у первой таблицы — левое соединение, у второй — правое, у обеих — полное, без флажков — внутреннее. Если связь не задать, таблицы окажутся перечислены через запятую — это декартово произведение. Подробнее — в статье о конструкторе запросов.
Исходные данные — остатки склада и розничные цены:
| Номенклатура | Остатки: количество | Цены: розничная цена |
|---|---|---|
| Кофе | 10 | 450 |
| Чай | 5 | 300 |
| Сахар | 3 | нет строки |
| Печенье | нет строки | 120 |
Результат соединения по номенклатуре («+» — строка попадает в результат):
| Остатки.Номенклатура | Количество | Цены.Номенклатура | Цена | ВНУТРЕННЕЕ | ЛЕВОЕ | ПРАВОЕ | ПОЛНОЕ |
|---|---|---|---|---|---|---|---|
| Кофе | 10 | Кофе | 450 | + | + | + | + |
| Чай | 5 | Чай | 300 | + | + | + | + |
| Сахар | 3 | NULL | NULL | — | + | — | + |
| NULL | NULL | Печенье | 120 | — | — | + | + |
| Строк в результате | 2 | 3 | 3 | 4 | |||
| Вид | Как пишется | Что в результате | В SQL |
|---|---|---|---|
| Внутреннее | ВНУТРЕННЕЕ СОЕДИНЕНИЕ | только строки, у которых нашлась пара | INNER JOIN |
| Левое | ЛЕВОЕ [ВНЕШНЕЕ] СОЕДИНЕНИЕ | все строки левой, поля правой без пары — NULL | LEFT JOIN |
| Правое | ПРАВОЕ СОЕДИНЕНИЕ | все строки правой, поля левой без пары — NULL | RIGHT JOIN |
| Полное | ПОЛНОЕ СОЕДИНЕНИЕ | все строки обеих таблиц | FULL JOIN |
| Декартово произведение | таблицы через запятую или ПО ИСТИНА | каждая строка с каждой: 3 × 3 = 9 | CROSS JOIN |
NULL, даже если в метаданных у них тип Число или Ссылка.Ценообразование УТ 11.4 так присоединяют курс валюты заказа к курсу валюты документа — ЛЕВОЕ СОЕДИНЕНИЕ … ПО (ИСТИНА).Функция возвращает остатки склада с ценой выбранного вида. Цены в УТ хранятся по номенклатуре и характеристике, поэтому регистр цен присоединяется дважды: цена характеристики и запасная цена номенклатуры без характеристики.
// Остатки склада в ценах выбранного вида цен (регистры УТ 11.4).
// Цена берётся по характеристике, а если её нет - цена номенклатуры в целом.
// Размещение: серверный общий модуль или модуль формы с директивой &НаСервере.
Функция ОстаткиСкладаВЦенах(Склад, ВидЦены) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.Номенклатура КАК Номенклатура,
| Остатки.Характеристика КАК Характеристика,
| Остатки.ВНаличииОстаток КАК Количество
|ПОМЕСТИТЬ ВТ_Остатки
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки
|ГДЕ
| Остатки.ВНаличииОстаток > 0
|
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура,
| Характеристика
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Остатки.Номенклатура КАК Номенклатура,
| ВТ_Остатки.Характеристика КАК Характеристика,
| ВТ_Остатки.Количество КАК Количество,
| ЕСТЬNULL(ЦеныХарактеристик.Цена, ЕСТЬNULL(ЦеныТоваров.Цена, 0)) КАК Цена,
| ВТ_Остатки.Количество * ЕСТЬNULL(ЦеныХарактеристик.Цена, ЕСТЬNULL(ЦеныТоваров.Цена, 0)) КАК Сумма,
| ВЫБОР
| КОГДА ЦеныХарактеристик.Цена ЕСТЬ NULL
| И ЦеныТоваров.Цена ЕСТЬ NULL
| ТОГДА ИСТИНА
| ИНАЧЕ ЛОЖЬ
| КОНЕЦ КАК НетЦены
|ИЗ
| ВТ_Остатки КАК ВТ_Остатки
| // 2-я таблица: цена конкретной характеристики
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
| &ТекущаяДата,
| ВидЦены = &ВидЦены
| И (Номенклатура, Характеристика) В
| (ВЫБРАТЬ
| ВТ.Номенклатура,
| ВТ.Характеристика
| ИЗ
| ВТ_Остатки КАК ВТ)) КАК ЦеныХарактеристик
| ПО ВТ_Остатки.Номенклатура = ЦеныХарактеристик.Номенклатура
| И ВТ_Остатки.Характеристика = ЦеныХарактеристик.Характеристика
| // 3-я таблица: цена номенклатуры без характеристики
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
| &ТекущаяДата,
| ВидЦены = &ВидЦены
| И Характеристика = ЗНАЧЕНИЕ(Справочник.ХарактеристикиНоменклатуры.ПустаяСсылка)
| И Номенклатура В
| (ВЫБРАТЬ
| ВТ.Номенклатура
| ИЗ
| ВТ_Остатки КАК ВТ)) КАК ЦеныТоваров
| ПО ВТ_Остатки.Номенклатура = ЦеныТоваров.Номенклатура
|
|УПОРЯДОЧИТЬ ПО
| ВТ_Остатки.Номенклатура.Наименование";
Запрос.УстановитьПараметр("Склад", Склад);
Запрос.УстановитьПараметр("ВидЦены", ВидЦены);
// Без даты срез вернул бы и цены, назначенные на будущее.
Запрос.УстановитьПараметр("ТекущаяДата", ТекущаяДатаСеанса());
// Выполнить() у пакета возвращает результат последнего запроса.
Возврат Запрос.Выполнить().Выгрузить();
КонецФункции
ПОМЕСТИТЬ ВТ_Остатки и ИНДЕКСИРОВАТЬ ПО — остатки ложатся во временную таблицу с индексом по полям соединения: стандарт 1С «Ограничения на соединения с вложенными запросами и виртуальными таблицами» велит соединять объекты метаданных и временные таблицы, а медленную виртуальную таблицу советует выносить во временную. Подробнее — о временных таблицах.ВидЦены = &ВидЦены в параметрах среза отбирает правую таблицу до соединения. Без него у товара будет столько строк, сколько у него видов цен.(Номенклатура, Характеристика) В (…) — срез только по товарам склада, а не по всему прайсу; подробнее — о СрезПоследних.ПО перечислены все измерения регистра цен, оставшиеся после отбора, поэтому на строку остатков приходится не больше одной цены.ЕСТЬNULL(ЦеныХарактеристик.Цена, ЕСТЬNULL(ЦеныТоваров.Цена, 0)) — сначала цена характеристики, потом цена номенклатуры. Одна таблица может входить в запрос несколько раз под разными псевдонимами.НетЦены — явный признак вместо молчаливого нуля.ВТ_Остатки.Номенклатура.Наименование — обращение через точку: платформа сама добавит левое соединение со справочником.Имена сверены по УТ 11.4. У ТоварыНаСкладах измерения Номенклатура, Характеристика, Назначение, Склад, Помещение, Серия, но виртуальная таблица остатков сворачивает измерения, не участвующие в запросе, — в ВТ_Остатки одна строка на товар и характеристику. У ЦеныНоменклатуры измерения Номенклатура, Характеристика, ВидЦены и ресурсы Цена, Упаковка, Валюта; если «Упаковка» заполнена, цена задана за упаковку — пересчитайте её на единицу хранения.
В КА 2.5 есть ещё регистр ЦеныНоменклатуры25 для ценообразования версии 2.5 с измерениями Номенклатура, ХарактеристикаЦО, СерияЦО, УпаковкаЦО, ВидЦены. Если база перешла на него, перепишите условие ПО под эти измерения, иначе строки размножатся.
Полное соединение нужно, когда важны строки без пары с обеих сторон: товары без одной из цен и товары, у которых продажа не выше закупки.
// Сверка двух видов цен полным соединением: товары без одной из цен
// и товары, у которых цена продажи не выше закупочной.
// Сравнение имеет смысл, если оба вида цен в одной валюте и за одну упаковку.
Функция ПроблемныеЦены(ВидЦеныЗакупки, ВидЦеныПродажи, НаДату) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЕСТЬNULL(Закупка.Номенклатура, Продажа.Номенклатура) КАК Номенклатура,
| ЕСТЬNULL(Закупка.Характеристика, Продажа.Характеристика) КАК Характеристика,
| ЕСТЬNULL(Закупка.Цена, 0) КАК ЦенаЗакупки,
| ЕСТЬNULL(Продажа.Цена, 0) КАК ЦенаПродажи,
| ВЫБОР
| КОГДА Закупка.Цена ЕСТЬ NULL
| ТОГДА ""Нет закупочной цены""
| КОГДА Продажа.Цена ЕСТЬ NULL
| ТОГДА ""Нет цены продажи""
| ИНАЧЕ ""Продажа не выше закупки""
| КОНЕЦ КАК Проблема
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&НаДату, ВидЦены = &ВидЦеныЗакупки) КАК Закупка
| ПОЛНОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&НаДату, ВидЦены = &ВидЦеныПродажи) КАК Продажа
| ПО Закупка.Номенклатура = Продажа.Номенклатура
| И Закупка.Характеристика = Продажа.Характеристика
|ГДЕ
| (Закупка.Цена ЕСТЬ NULL
| ИЛИ Продажа.Цена ЕСТЬ NULL
| ИЛИ Продажа.Цена <= Закупка.Цена)
|
|УПОРЯДОЧИТЬ ПО
| Проблема";
Запрос.УстановитьПараметр("ВидЦеныЗакупки", ВидЦеныЗакупки);
Запрос.УстановитьПараметр("ВидЦеныПродажи", ВидЦеныПродажи);
Запрос.УстановитьПараметр("НаДату", НаДату);
Возврат Запрос.Выполнить().Выгрузить();
КонецФункции
Строка, которая есть только справа, приходит с NULL во всех полях левой таблицы, поэтому ключ собирают через ЕСТЬNULL(Закупка.Номенклатура, Продажа.Номенклатура). Условия ЕСТЬ NULL в ГДЕ здесь как раз отбирают строки без пары.
Каждое следующее соединение присоединяет таблицу к уже собранному результату:
ПО можно ссылаться на таблицы выше по тексту, но не ниже.СОЕДИНЕНИЕ Т2 ПО … СОЕДИНЕНИЕ Т3 ПО …. Запись СОЕДИНЕНИЕ Т2 СОЕДИНЕНИЕ Т3 ПО … ПО … — вложенное соединение, по сути подзапрос с другим результатом; тот же стандарт советует заменять его последовательным.ЕСТЬNULL и в следующих условиях ПО.Размножение проверяется запросом: строки и сумма остатков до соединения и после. Здесь соединение нарочно только по номенклатуре — при ценах по характеристикам «после» окажется больше, чем «до».
// Проверка соединения на размножение строк: сколько строк и какое количество
// было в левой таблице до соединения и сколько стало после него.
// Соединение здесь нарочно только по номенклатуре, без характеристики.
Функция ПроверитьРазмножениеСтрок(ВидЦены) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.Номенклатура КАК Номенклатура,
| Остатки.Характеристика КАК Характеристика,
| Остатки.ВНаличииОстаток КАК Количество
|ПОМЕСТИТЬ ВТ_Остатки
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| КОЛИЧЕСТВО(*) КАК Строк,
| ЕСТЬNULL(СУММА(ВТ_Остатки.Количество), 0) КАК Количество
|ИЗ
| ВТ_Остатки КАК ВТ_Остатки
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| КОЛИЧЕСТВО(*) КАК Строк,
| ЕСТЬNULL(СУММА(ВТ_Остатки.Количество), 0) КАК Количество
|ИЗ
| ВТ_Остатки КАК ВТ_Остатки
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&ТекущаяДата, ВидЦены = &ВидЦены) КАК Цены
| ПО ВТ_Остатки.Номенклатура = Цены.Номенклатура";
Запрос.УстановитьПараметр("ВидЦены", ВидЦены);
Запрос.УстановитьПараметр("ТекущаяДата", ТекущаяДатаСеанса());
// [0] - помещение во временную таблицу, [1] - до соединения, [2] - после
Результаты = Запрос.ВыполнитьПакет();
ДоСоединения = Результаты[1].Выгрузить()[0];
ПослеСоединения = Результаты[2].Выгрузить()[0];
Итог = Новый Структура;
Итог.Вставить("СтрокДо", ДоСоединения.Строк);
Итог.Вставить("СтрокПосле", ПослеСоединения.Строк);
Итог.Вставить("КоличествоДо", ДоСоединения.Количество);
Итог.Вставить("КоличествоПосле", ПослеСоединения.Количество);
Итог.Вставить("ЕстьРазмножение", ПослеСоединения.Строк > ДоСоединения.Строк);
Возврат Итог;
КонецФункции
Подставьте в третий запрос пакета своё соединение. Если строк стало больше, ищите дубли ключа в правой таблице: СГРУППИРОВАТЬ ПО ключу и ИМЕЮЩИЕ КОЛИЧЕСТВО(*) > 1.
Правая таблица вернула на один ключ несколько строк. У кофе на складе 10 штук и два штрихкода — для штуки и для коробки:
| Этап | Номенклатура | Штрихкод | Количество |
|---|---|---|---|
| До соединения | Кофе | — | 10 |
| После | Кофе | штрихкод штуки | 10 |
| После | Кофе | штрихкод коробки | 10 |
| СУММА(Количество) | 20 вместо 10 | ||
// Ошибка: у товара два штрихкода - строка остатка удвоится
ВЫБРАТЬ
Остатки.Номенклатура КАК Номенклатура,
Штрихкоды.Штрихкод КАК Штрихкод,
Остатки.ВНаличииОстаток КАК Количество
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ШтрихкодыНоменклатуры КАК Штрихкоды
ПО Остатки.Номенклатура = Штрихкоды.Номенклатура
Исправление — свернуть правую таблицу до одной строки на ключ и соединять по полному ключу:
// Шаг 1: одна строка на товар и характеристику, сколько бы штрихкодов ни было
ВЫБРАТЬ
Штрихкоды.Номенклатура КАК Номенклатура,
Штрихкоды.Характеристика КАК Характеристика,
МАКСИМУМ(Штрихкоды.Штрихкод) КАК Штрихкод,
КОЛИЧЕСТВО(*) КАК ВсегоШтрихкодов
ПОМЕСТИТЬ ВТ_Штрихкоды
ИЗ
РегистрСведений.ШтрихкодыНоменклатуры КАК Штрихкоды
СГРУППИРОВАТЬ ПО
Штрихкоды.Номенклатура,
Штрихкоды.Характеристика
ИНДЕКСИРОВАТЬ ПО
Номенклатура,
Характеристика
;
// Шаг 2: соединение по полному ключу - строки остатков не размножаются
ВЫБРАТЬ
Остатки.Номенклатура КАК Номенклатура,
Остатки.Характеристика КАК Характеристика,
Остатки.ВНаличииОстаток КАК Количество,
ЕСТЬNULL(ВТ_Штрихкоды.Штрихкод, "") КАК Штрихкод,
ЕСТЬNULL(ВТ_Штрихкоды.ВсегоШтрихкодов, 0) КАК ВсегоШтрихкодов
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Штрихкоды КАК ВТ_Штрихкоды
ПО Остатки.Номенклатура = ВТ_Штрихкоды.Номенклатура
И Остатки.Характеристика = ВТ_Штрихкоды.Характеристика
С ценами то же самое: вид цен не задан в параметрах среза или в ПО нет характеристики.
Соединение левое, а условие на правую таблицу стоит в ГДЕ. У товара без пары поле справа равно NULL, сравнение NULL = &Склад не выполняется, и строка отбрасывается.
// Ошибка: условие на правую таблицу в ГДЕ - товары без остатка на складе пропадут
ВЫБРАТЬ
Товары.Ссылка КАК Номенклатура,
ЕСТЬNULL(Остатки.ВНаличииОстаток, 0) КАК Остаток
ИЗ
Справочник.Номенклатура КАК Товары
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
ПО Товары.Ссылка = Остатки.Номенклатура
ГДЕ
Товары.ТипНоменклатуры = ЗНАЧЕНИЕ(Перечисление.ТипыНоменклатуры.Товар)
И Остатки.Склад = &Склад
Условие на правую таблицу переносят в параметры виртуальной таблицы или в ПО:
// Правильно: отбор по складу - в параметрах виртуальной таблицы, в ГДЕ - только левая таблица
ВЫБРАТЬ
Товары.Ссылка КАК Номенклатура,
ЕСТЬNULL(Остатки.ВНаличииОстаток, 0) КАК Остаток
ИЗ
Справочник.Номенклатура КАК Товары
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки
ПО Товары.Ссылка = Остатки.Номенклатура
ГДЕ
Товары.ТипНоменклатуры = ЗНАЧЕНИЕ(Перечисление.ТипыНоменклатуры.Товар)
То же в цепочке, если после левого соединения стоит внутреннее. Исправление — ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Валюты или Цены.Валюта.Наименование через точку:
// Ошибка: ВНУТРЕННЕЕ после ЛЕВОГО. У товара без цены Цены.Валюта = NULL,
// пары в справочнике валют нет - и строка остатка пропадает
ВЫБРАТЬ
Остатки.Номенклатура КАК Номенклатура,
Остатки.Характеристика КАК Характеристика,
Остатки.ВНаличииОстаток КАК Количество,
Цены.Цена КАК Цена,
Валюты.Наименование КАК Валюта
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&НаДату, ВидЦены = &ВидЦены) КАК Цены
ПО Остатки.Номенклатура = Цены.Номенклатура
И Остатки.Характеристика = Цены.Характеристика
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Валюты КАК Валюты
ПО Цены.Валюта = Валюты.Ссылка
Пустой результат при живых данных часто объясняется тем же — см. статью о пустом результате запроса.
Количество * NULL даёт NULL, а СУММА такие значения пропускает — итог занижен молча. Оборачивайте поля внешней таблицы в ЕСТЬNULL(Поле, 0), ссылки — в ЕСТЬNULL(Поле, ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка)), см. функцию ЗНАЧЕНИЕ(). Если NULL дошёл до кода, Выборка.Цена * Выборка.Количество упадёт с ошибкой «Преобразование значения к типу Число не может быть выполнено». И NULL = NULL в запросе не истинно: соединение по полю со значением NULL пару не найдёт.
ПО упомянута таблица, присоединённая ниже.&НаСервере.Левое возвращает все строки левой таблицы, а поля правой без пары заполняет NULL. Внутреннее оставляет только строки, для которых пара нашлась.
Соединение добавляет колонки, ОБЪЕДИНИТЬ — строки: результаты двух запросов идут один под другим. Для сверки двух регистров часто проще ОБЪЕДИНИТЬ ВСЕ с группировкой: в ключах нет NULL и нечему размножаться.
Внутри запроса набора — обычными соединениями. Связи между наборами задаются на вкладке «Связи наборов данных», и тип соединения там не выбирается: связь всегда работает как левое соединение.
Через временную таблицу: первым запросом пакета ВЫБРАТЬ … ПОМЕСТИТЬ ВТ ИЗ &Таблица КАК Т, дальше обычное соединение. Колонки таблицы значений должны быть типизированы.
Можно, в ПО допустимо любое логическое выражение. Но ПО Цены.Период <= Док.Дата найдёт все цены до даты документа, и последнюю придётся выбирать группировкой. Для цены на дату удобнее срез последних.
Если отчёт собирает остатки, цены, заказы и резервы из нескольких регистров и должен сходиться с типовыми отчётами, быстрее поручить его программисту — посмотрите услугу отчёты и обработки 1С. Опишите задачу на странице «Разместить задание»: оценка бесплатная, ответ в течение рабочего дня.
Станьте частью сообщества!
Войдите или зарегистрируйтесь, и вы сможете участвовать в обсуждениях.