При работе с запросами в 1С:Предприятии мы часто сталкиваемся с необходимостью объединения данных из нескольких источников, таких как регистры накопления и регистры сведений. Однако неправильный подход к выбору типа соединения и формированию условий связи может привести к нежелательным результатам, например, к появлению дубликатов в результирующей выборке. Именно с такой ситуацией столкнулся наш коллега, получивший четыре записи вместо двух ожидаемых при использовании ПОЛНОЕ СОЕДИНЕНИЕ ПО (ИСТИНА).
Давайте разберем эту проблему шаг за шагом, выясним ее причины и предложим оптимальные пути решения.
Первопричиной появления избыточных записей в запросе коллеги является использование конструкции ПОЛНОЕ СОЕДИНЕНИЕ ПО (ИСТИНА). Эта запись, на первый взгляд, может показаться неочевидной, но по своей сути она эквивалентна перекрестному соединению (CROSS JOIN) или декартову произведению двух таблиц. Разберем подробнее, что это означает:
Перекрестное соединение (Декартово произведение): При таком типе соединения каждая строка первой таблицы объединяется с каждой строкой второй таблицы. Если в первой таблице у нас N записей, а во второй M записей, то результатом перекрестного соединения будет N * M записей.
Пример из запроса: Если, как в случае коллеги, во временной таблице ВремТабРегистратор оказалось 2 записи, а в РегистрСведенийПокупатель (после применения условий отбора по Продавцу и Периоду) также 2 записи, то ПОЛНОЕ СОЕДИНЕНИЕ ПО (ИСТИНА) приведет к появлению 2 * 2 = 4 записей в результате. Это именно та ситуация, о которой говорится в первом сообщении.
Почему "ПО (ИСТИНА)"? Условие ПО (ИСТИНА) всегда истинно, то есть оно не накладывает никаких ограничений на связь между строками. В результате СУБД объединяет все возможные комбинации строк из обеих таблиц, что и является декартовым произведением.
Таким образом, наша главная задача — отказаться от декартова произведения и использовать тот тип соединения, который соответствует логике связи между данными.
Для корректного построения запросов критически важно понимать различные типы соединений и их назначение. Давайте рассмотрим основные из них:
Что делает: Возвращает только те строки, для которых есть совпадения в обеих соединяемых таблицах по указанному условию связи. Несовпадающие строки игнорируются.
Когда использовать: Когда вам нужны только те данные, которые присутствуют в обеих таблицах и имеют прямую логическую связь.
Пример:
ВЫБРАТЬ
ЗаказКлиента.Номер,
ЗаказКлиента.Дата,
Клиенты.Наименование КАК Клиент
ИЗ
Документ.ЗаказКлиента КАК ЗаказКлиента
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Клиенты
ПО ЗаказКлиента.Клиент = Клиенты.Ссылка
В этом примере мы получим только те заказы, у которых есть соответствующий клиент в справочнике.
Что делает: Включает все записи из "левой" (первой) таблицы и только те записи из "правой" (второй) таблицы, которые соответствуют условию соединения. Если для записи из левой таблицы нет совпадения в правой, поля правой таблицы будут заполнены значениями NULL.
Когда использовать: Когда вам нужны все данные из одной таблицы (левой) и связанные с ней данные из другой (правой), даже если таких связанных данных нет.
Пример:
ВЫБРАТЬ
Товары.Наименование КАК Товар,
Остатки.КоличествоОстаток КАК Остаток
ИЗ
Справочник.Номенклатура КАК Товары
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиТоваров.Остатки КАК Остатки
ПО Товары.Ссылка = Остатки.Номенклатура
Здесь мы получим весь список товаров. Для товаров, по которым есть остатки, отобразится количество. Для товаров без остатков поле "Остаток" будет NULL.
Именно этот тип соединения чаще всего используется в 1С и является наиболее универсальным для получения полной картины по одной из таблиц.
Что делает: Аналогично левому соединению, но включает все записи из "правой" таблицы и совпадающие из "левой". В 1С конструктор запросов обычно меняет таблицы местами, чтобы использовать левое соединение, поэтому правое соединение редко встречается в явном виде и часто может быть заменено левым соединением с перестановкой таблиц.
Когда использовать: Когда вам нужны все данные из правой таблицы и связанные с ней данные из левой.
Что делает: Возвращает все записи из обеих таблиц. Если для записи из одной таблицы нет соответствия в другой, соответствующие поля будут заполнены значениями NULL.
Когда использовать: Когда требуется сохранить все данные из обоих источников независимо от наличия соответствия, например, для анализа всех возможных комбинаций данных.
Пример:
ВЫБРАТЬ
Товары.Наименование КАК Товар,
Остатки.КоличествоОстаток КАК Остаток
ИЗ
Справочник.Номенклатура КАК Товары
ПОЛНОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиТоваров.Остатки КАК Остатки
ПО Товары.Ссылка = Остатки.Номенклатура
Этот запрос покажет все товары (даже без остатков) и все записи об остатках (даже если номенклатура удалена, но остатки есть), сопоставляя их по ссылке.
Что делает: Выполняет декартово произведение, т.е. каждая строка одной таблицы соединяется с каждой строкой другой таблицы, если нет явно заданного условия связи. В 1С достигается путем отсутствия условий связи (например, как ЛЕВОЕ СОЕДИНЕНИЕ ... ПО ИСТИНА или просто перечисление таблиц через запятую в секции ИЗ).
Когда использовать: Крайне редко, в специфических случаях, когда требуется сгенерировать все возможные пары из двух наборов данных (например, для построения матрицы или генерации тестовых данных).
Важнейший аспект при работе с соединениями — это понимание разницы между условиями в секциях ПО и ГДЕ:
Секция ПО (условия связи): Условия, заданные в секции ПО, определяют, как строки из одной таблицы будут связаны со строками из другой таблицы. Эти условия применяются до того, как будут отобраны окончательные результаты. Они формируют "связанный" набор данных.
Секция ГДЕ (условия отбора): Условия в секции ГДЕ применяются после того, как таблицы уже были соединены согласно условиям ПО. Они фильтруют полученный результат соединения.
Важный нюанс для ЛЕВОЕ СОЕДИНЕНИЕ: Если мы хотим отфильтровать данные из "правой" таблицы до соединения, то условие отбора для нее следует включать в секцию ПО. Если же условие для правой таблицы поставить в секцию ГДЕ, то для строк левой таблицы, не имеющих соответствия в правой после применения фильтра в ГДЕ, соответствующие поля правой таблицы будут не NULL, а строки целиком будут отброшены, что превратит ЛЕВОЕ СОЕДИНЕНИЕ в аналог ВНУТРЕННЕГО СОЕДИНЕНИЯ для этих отфильтрованных строк.
Давайте вернемся к исходному запросу коллеги:
ВЫБРАТЬ
| Работа.Регистратор
|ПОМЕСТИТЬ
| ВремТабРегистратор
|ИЗ
| РегистрНакопления.Работа КАК Работа
|ГДЕ
| Работа.Продавец = &ИмяПользователя
| И Работа.Период = &ТекущаяДата
|СГРУППИРОВАТЬ ПО
| Работа.Регистратор
|;
|ВЫБРАТЬ
| РегистрСведенийПокупатель.Период,
| РегистрСведенийПокупатель.ФИО,
| РегистрСведенийПокупатель.НомерСкидКарты,
| ВремТабРегистратор.Регистратор
|ИЗ
| РегистрСведений.РегистрСведенийПокупатель КАК РегистрСведенийПокупатель
|ПОЛНОЕ СОЕДИНЕНИЕ
| ВремТабРегистратор КАК ВремТабРегистратор
|ПО (ИСТИНА)
|ГДЕ
| РегистрСведенийПокупатель.Продавец = &ИмяПользователя
| И РегистрСведенийПокупатель.Период = &ТекущаяДата
Мы уже выяснили, что ПОЛНОЕ СОЕДИНЕНИЕ ПО (ИСТИНА) является причиной декартова произведения. Теперь обратим внимание на сообщение коллеги: "тогда регистратор выводиться пустым" при попытке использовать другое соединение. Это указывает на то, что либо не было задано условие связи в секции ПО, либо это условие было некорректным, в результате чего для записей из РегистрСведенийПокупатель не нашлось соответствующих Регистраторов.
Для того чтобы правильно соединить данные, нам необходимо найти общие поля между ВремТабРегистратор и РегистрСведенийПокупатель. Исходя из условий ГДЕ в обеих частях запроса (Продавец = &ИмяПользователя И Период = &ТекущаяДата), можно предположить, что Продавец и Период являются ключевыми полями, по которым эти два регистра логически связаны.
Однако, в первой временной таблице ВремТабРегистратор мы выбираем и группируем только по Работа.Регистратор, не включая Продавец и Период в ее выходные поля. Это означает, что ВремТабРегистратор не содержит этих полей для последующего использования в условии ПО, что и является причиной проблем с "пустым регистратором" при попытке осмысленного соединения.
Давайте исправим запрос, следуя логике, которую мы выяснили:
ВремТабРегистраторЧтобы иметь возможность соединить ВремТабРегистратор с РегистрСведенийПокупатель по полям Продавец и Период, мы должны включить эти поля в выборку временной таблицы и соответствующим образом изменить условие СГРУППИРОВАТЬ ПО. Мы по-прежнему хотим получить уникальные регистраторы для комбинации продавца и периода.
ВЫБРАТЬ
Работа.Регистратор,
Работа.Продавец, // Добавляем Продавца в выборку
Работа.Период // Добавляем Период в выборку
ПОМЕСТИТЬ ВремТабРегистратор
ИЗ
РегистрНакопления.Работа КАК Работа
ГДЕ
Работа.Продавец = &ИмяПользователя
И Работа.Период = &ТекущаяДата
СГРУППИРОВАТЬ ПО
Работа.Регистратор,
Работа.Продавец, // Группируем также по Продавцу
Работа.Период // И по Периоду
;
Теперь ВремТабРегистратор будет содержать уникальные комбинации Регистратор, Продавец, Период.
Если нам нужны все уникальные регистраторы, полученные из РегистрНакопления.Работа, и к ним мы хотим подтянуть информацию о покупателях (ФИО, НомерСкидКарты) из РегистрСведенийПокупатель, соответствующую тому же продавцу и периоду, то нам необходимо использовать ЛЕВОЕ СОЕДИНЕНИЕ. Это гарантирует, что даже если для какого-то регистратора не найдется информации о покупателе по данным условиям, строка с регистратором все равно попадет в результат, а поля покупателя будут NULL.
Условие связи будет основываться на полях Продавец и Период, которые теперь доступны в обеих соединяемых частях.
Важный момент для условий ГДЕ: Поскольку условия отбора по Продавец и Период применяются к обеим таблицам, мы можем включить их в условие ПО для ЛЕВОГО СОЕДИНЕНИЯ. Это отфильтрует "правую" таблицу до соединения. Если же мы оставим их в ГДЕ после соединения, то строки, у которых по правой таблице будут NULL (т.е. нет совпадений), будут отфильтрованы, и ЛЕВОЕ СОЕДИНЕНИЕ фактически превратится во ВНУТРЕННЕЕ.
ВЫБРАТЬ
РегистрСведенийПокупатель.Период,
РегистрСведенийПокупатель.ФИО,
РегистрСведенийПокупатель.НомерСкидКарты,
ВремТабРегистратор.Регистратор
ИЗ
ВремТабРегистратор КАК ВремТабРегистратор
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РегистрСведенийПокупатель КАК РегистрСведенийПокупатель
ПО ВремТабРегистратор.Продавец = РегистрСведенийПокупатель.Продавец
И ВремТабРегистратор.Период = РегистрСведенийПокупатель.Период
// Дополнительные условия для фильтрации покупателей можно добавить сюда,
// если они относятся к связи или фильтрации правой таблицы
И РегистрСведенийПокупатель.Продавец = &ИмяПользователя
И РегистрСведенийПокупатель.Период = &ТекущаяДата
;
Обратите внимание: условия И РегистрСведенийПокупатель.Продавец = &ИмяПользователя И РегистрСведенийПокупатель.Период = &ТекущаяДата могут быть опущены в секции ГДЕ основного запроса, так как они уже либо фильтруют ВремТабРегистратор, либо являются частью условия соединения в ПО.
Даже после правильного использования ЛЕВОГО СОЕДИНЕНИЯ с корректными условиями связи, дубликаты могут появляться, если существует отношение "один ко многим" между соединяемыми данными. Например, если для одной пары Продавец / Период в РегистрСведенийПокупатель существует несколько записей с разными ФИО или НомерСкидКарты, то каждый Регистратор, соответствующий этой паре, будет дублироваться для каждой такой записи о покупателе. В этом случае мы можем рассмотреть следующие подходы:
Оператор РАЗЛИЧНЫЕ (DISTINCT): Если нам нужна уникальная комбинация всех полей в результате запроса, мы можем использовать оператор РАЗЛИЧНЫЕ после ВЫБРАТЬ.
ВЫБРАТЬ РАЗЛИЧНЫЕ
РегистрСведенийПокупатель.Период,
РегистрСведенийПокупатель.ФИО,
РегистрСведенийПокупатель.НомерСкидКарты,
ВремТабРегистратор.Регистратор
ИЗ
// ... остальная часть запроса ...
Предостережение: РАЗЛИЧНЫЕ будет удалять строки только в том случае, если все поля в строке полностью совпадают. Если Регистратор отличается, но ФИО и НомерСкидКарты одинаковы, или наоборот, строки будут считаться разными.
Оператор СГРУППИРОВАТЬ ПО (GROUP BY): Если наша цель не просто убрать дубликаты, а получить агрегированные данные (например, первый попавшийся ФИО или количество покупателей для каждого регистратора), мы можем использовать СГРУППИРОВАТЬ ПО. Все поля, не участвующие в агрегации, должны быть перечислены в секции СГРУППИРОВАТЬ ПО.
ВЫБРАТЬ
ВремТабРегистратор.Регистратор,
МАКСИМУМ(РегистрСведенийПокупатель.ФИО) КАК ФИОПокупателя,
МАКСИМУМ(РегистрСведенийПокупатель.НомерСкидКарты) КАК НомерКарты
ИЗ
ВремТабРегистратор КАК ВремТабРегистратор
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РегистрСведенийПокупатель КАК РегистрСведенийПокупатель
ПО ВремТабРегистратор.Продавец = РегистрСведенийПокупатель.Продавец
И ВремТабРегистратор.Период = РегистрСведенийПокупатель.Период
СГРУППИРОВАТЬ ПО
ВремТабРегистратор.Регистратор
Этот подход гарантирует, что на каждый уникальный Регистратор будет выведена только одна строка с агрегированными данными о покупателе (например, максимальное ФИО или номер карты, если это приемлемо для бизнес-логики).
Заключение: Ключ к решению проблемы с дубликатами и "пустым регистратором" лежит в глубоком понимании типов соединений, правильном формировании условий связи (в секции ПО) и адекватном использовании временных таблиц, которые содержат все необходимые поля для эффективного соединения. Всегда начинайте с четкого понимания того, как именно должны быть связаны ваши данные, и какой результат вы ожидаете получить.