Часто перед разработчиком стоит задача собрать данные из разных источников — например, обороты по денежным средствам, продажам и закупкам — в одну таблицу. Первая мысль, которая возникает у многих, — использовать ЛЕВОЕ СОЕДИНЕНИЕ (LEFT JOIN), выбрав один из регистров в качестве основного. Однако этот путь часто ведет к логическим ошибкам и потере данных. Рассмотрим подробнее, почему подход с соединениями может не работать и как реализовать задачу правильно.
Разберем ситуацию, описанную автором темы. В его запросе основной таблицей выступает РегистрНакопления.Касса.Обороты. К ней последовательно присоединяются другие регистры по полю Павилион. Проблема заключается в самой природе левого соединения: если в левой (ведущей) таблице нет записей по конкретному павильону за выбранный период, то даже если в правых (присоединяемых) таблицах есть огромные обороты, запрос вернет пустой результат.
Проанализируем пример: если по павильону «А» были продажи и покупки, но не было движений по кассе, то основная таблица (Касса) не вернет строк для этого павильона. Следовательно, соединять будет просто не с чем. Использование условия ГДЕ КассаОбороты.Павилион = &Павилион только усугубляет ситуацию, отсекая все, что не попало в первичную выборку по кассе.
В экосистеме 1С «золотым стандартом» для объединения данных из разных регистров считается использование конструкции ОБЪЕДИНИТЬ ВСЕ с последующей группировкой. Рассмотрим, как это работает на практике. Мы создаем несколько независимых выборок (по одной для каждого регистра), приводим их к единой структуре колонок, а затем «схлопываем» результат.
Выполним задачу по шагам:
Посмотрим на пример правильно сконструированного запроса:
ВЫБРАТЬ
Т.Павилион КАК Павилион,
СУММА(Т.Касса) КАК Касса,
СУММА(Т.Покупки) КАК Покупки,
СУММА(Т.Продажи) КАК Продажи,
СУММА(Т.ОбменВозврат) КАК ОбменВозврат
ИЗ
(ВЫБРАТЬ
КассаОбороты.Павилион КАК Павилион,
КассаОбороты.СуммаОборот КАК Касса,
0 КАК Покупки,
0 КАК Продажи,
0 КАК ОбменВозврат
ИЗ
РегистрНакопления.Касса.Обороты(&НачалоПериода, &КонецПериода, , Павилион = &Павилион) КАК КассаОбороты
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ПродажиОбороты.Павилион,
0,
0,
ПродажиОбороты.СуммаОборот,
0
ИЗ
РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, , Павилион = &Павилион) КАК ПродажиОбороты
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ПокупкиОбороты.Павилион,
0,
ПокупкиОбороты.СуммаОборот,
0,
0
ИЗ
РегистрНакопления.Покупки.Обороты(&НачалоПериода, &КонецПериода, , Павилион = &Павилион) КАК ПокупкиОбороты
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
УчетОбмена.Павилион,
0,
0,
0,
УчетОбмена.СуммаОборот
ИЗ
РегистрНакопления.УчетОбменаТоваров.Обороты(&НачалоПериода, &КонецПериода, , Павилион = &Павилион) КАК УчетОбмена) КАК Т
СГРУППИРОВАТЬ ПО
Т.Павилион
Во-первых, обратите внимание на параметры виртуальных таблиц Обороты. Мы передаем условие по павильону непосредственно в четвертый параметр виртуальной таблицы: Павилион = &Павилион. Это критически важно для производительности. Если использовать секцию ГДЕ во внешнем запросе, система сначала попытается рассчитать обороты по всем павильонам из всех регистров, и только потом отфильтрует нужный. Передача параметра в виртуальную таблицу заставляет СУБД выбирать только релевантные данные на самом раннем этапе.
Во-вторых, использование ОБЪЕДИНИТЬ ВСЕ (вместо простого ОБЪЕДИНИТЬ) избавляет сервер базы данных от необходимости проверять строки на уникальность и удалять дубликаты. Поскольку мы все равно будем делать группировку позже, нам не важна уникальность строк на этапе объединения. Это работает значительно быстрее на больших объемах данных.
В-третьих, мы решили проблему NULL. При использовании соединений нам приходилось использовать конструкцию ЕСТЬNULL(Поле, 0) или ВЫБОР КОГДА ... ЕСТЬ NULL. В случае с объединением мы сами явно проставляем «0» там, где данных в конкретной части запроса нет. Агрегатная функция СУММА() при финальной группировке корректно сложит эти нули с реальными числами.
Если регистров много, или логика получения данных из каждого регистра сложнее, чем просто выборка оборотов, мы рекомендуем использовать Временные Таблицы. Это сделает запрос более читаемым и упростит его отладку.
Рассмотрим структуру такого запроса:
ВТ_Касса.ВТ_Продажи.ОБЪЕДИНИТЬ ВСЕ из всех временных таблиц и группируем результат.Проанализируем пример схемы с временными таблицами:
ВЫБРАТЬ
КассаОбороты.Павилион КАК Павилион,
КассаОбороты.СуммаОборот КАК Сумма
ПОМЕСТИТЬ ВТ_Данные
ИЗ
РегистрНакопления.Касса.Обороты(&НачалоПериода, &КонецПериода, , Павилион = &Павилион) КАК КассаОбороты
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ПродажиОбороты.Павилион,
ПродажиОбороты.СуммаОборот
ИЗ
РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, , Павилион = &Павилион) КАК ПродажиОбороты
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Т.Павилион КАК Павилион,
СУММА(Т.Сумма) КАК ИтоговаяСумма
ИЗ
ВТ_Данные КАК Т
СГРУППИРОВАТЬ ПО
Т.Павилион
Если по каким-то причинам вы все же хотите использовать ЛЕВОЕ СОЕДИНЕНИЕ, вам необходимо сменить архитектуру запроса. Чтобы не терять данные, в качестве левой (основной) таблицы должен выступать справочник (в данном случае — Справочник.Павилионы).
Алгоритм будет следующим:
ЕСТЬNULL().Однако помните: такой подход менее эффективен. При объединении четырех и более регистров через соединения, планировщик запросов SQL-сервера может построить неоптимальный план, что приведет к замедлению работы системы. Метод через ОБЪЕДИНИТЬ ВСЕ является универсальным, более быстрым и легче поддается масштабированию (добавить пятый регистр в объединение гораздо проще, чем добавлять еще одно соединение в сложную структуру).
Для получения оборотов из нескольких независимых регистров накопления всегда отдавайте предпочтение объединению запросов. Это гарантирует полноту данных, даже если в одном из регистров за период не было записей. Не забывайте передавать условия фильтрации (например, по конкретному объекту или периоду) в параметры виртуальных таблиц, чтобы ваш код работал максимально быстро.