При разработке запросов в системе «1С:Предприятие» программисты часто сталкиваются с ситуацией, когда результат выборки неожиданно увеличивается в объеме, а строки начинают дублироваться. В СУБД это явление называется декартовым произведением (или Cross Join). Особенно ярко эта проблема проявляется при попытке соединить данные шапки документа или справочника с их табличными частями.
В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем типичные ошибки разработчиков и выработаем надежный, профессиональный алгоритм решения этой задачи средствами языка запросов 1С без использования «костылей» в виде постобработки в коде.
Для начала давайте выясним причину этого поведения СУБД. Когда мы выполняем оператор ЛЕВОЕ СОЕДИНЕНИЕ (или любое другое соединение), система пытается сопоставить строки из левой таблицы со строками из правой таблицы по указанному условию (ключу связи).
Если ключ связи не уникален хотя бы в одной из таблиц, СУБД соединяет каждую подходящую строку слева с каждой подходящей строкой справа. Рассмотрим простой пример:
Налив = "Нефть-1"."Нефть-1" содержится 4 строки (например, разные показатели или статусы).В результате их соединения СУБД выдаст ровно 4 строки. Данные из шапки документа продублируются четыре раза. Если же в левой таблице окажется 2 документа с одинаковым ключом связи, а в правой — 3 строки, на выходе мы получим уже 6 строк (произведение 2 * 3). Именно с этим столкнулся автор темы на форуме.
Давайте проанализируем подходы, к которым часто прибегают начинающие разработчики, и поймем, почему они не работают или считаются плохим тоном в программировании 1С.
Ошибочный подход №1: Использование программной очистки дублей (Сообщение 2)
Автор темы предложил оставить запрос «как есть» с дублями, а полученную таблицу значений свернуть или очистить в коде с помощью обхода выборки. Это крайне неэффективный подход. СУБД тратит колоссальные ресурсы на генерацию избыточных данных, передачу их по сети на сервер приложений 1С, а затем сервер тратит память и процессорное время на их обработку. При больших объемах данных это гарантированно приведет к зависанию системы.
Ошибочный подход №2: Использование конструкции РАЗЛИЧНЫЕ (DISTINCT) (Сообщение 3 и 17)
Конструкция РАЗЛИЧНЫЕ работает на уровне всей результирующей строки. Она убирает дубли только в том случае, если абсолютно все поля в строках идентичны. Если в запросе выбираются детальные записи из табличной части (например, разные статусы, уникальные идентификаторы строк или даты), то для СУБД эти строки будут считаться уникальными. Конструкция РАЗЛИЧНЫЕ в этом случае окажется абсолютно бесполезной.
Ошибочный подход №3: Использование вложенных запросов в соединениях
В первом сообщении темы автор соединяет два вложенных запроса напрямую. Это грубая ошибка оптимизации. Оптимизатор СУБД не всегда может построить эффективный план выполнения для вложенных запросов, участвующих в соединениях. Это часто приводит к сканированию таблиц вместо использования индексов.
Главный золотой стандарт разработки запросов в 1С гласит: соединять можно только те таблицы, в одной из которых гарантирована уникальность ключей связи!
Для того чтобы решить проблему размножения строк, нам необходимо предварительно подготовить данные. Давайте разберем этот процесс по шагам.
Вместо вложенных запросов мы должны использовать временные таблицы с помощью ключевого слова ПОМЕСТИТЬ. Это позволит СУБД построить оптимальный план выполнения и использовать индексацию.
Перед тем как соединять табличную часть справочника с документом, её необходимо сгруппировать по полям связи (например, по ссылке на документ-владелец и номенклатуре/грузу). При этом для детальных полей (таких как дата, статус, ответственный) нужно применить агрегатные функции, например, МАКСИМУМ или МИНИМУМ. Это позволит схлопнуть несколько строк табличной части в одну уникальную строку.
Для всех создаваемых временных таблиц обязательно нужно указывать инструкцию ИНДЕКСИРОВАТЬ ПО для тех полей, по которым в дальнейшем будет происходить соединение. Это значительно ускорит работу запроса.
В запросе автора присутствует условие ЖДНакладная.Налив.Ссылка = &СсылкаНаНалив. Обращение к реквизиту Ссылка через точку от ссылочного поля Налив заставляет систему делать лишнее неявное левое соединение. Правильно писать просто: ЖДНакладная.Налив = &СсылкаНаНалив.
Давайте объединим все правила и напишем красивый, оптимизированный запрос, который полностью исключает декартово произведение.
// ШАГ 1: Помещаем данные табличной части во временную таблицу и СГРУППИРОВЫВАЕМ ИХ.
// Это гарантирует, что для каждой связки "СсылкаДок + Груз" останется только ОДНА строка.
ВЫБРАТЬ
ПаспортаКачества.СсылкаДок КАК СсылкаДок,
ПаспортаКачестваТАбличнаяЧастьПК.Груз КАК Груз,
МАКСИМУМ(ПаспортаКачества.Ответственый) КАК Ответственый,
МАКСИМУМ(ПаспортаКачества.ДатаИВремя) КАК ДатаИВремя,
МАКСИМУМ(ПаспортаКачестваТАбличнаяЧастьПК.Отправка) КАК Отправка,
МАКСИМУМ(ПаспортаКачестваТАбличнаяЧастьПК.Статус) КАК Статус
ПОМЕСТИТЬ ВТ_ПаспортаСвернутые
ИЗ
Справочник.ПаспортаКачества.ТАбличнаяЧастьПК КАК ПаспортаКачестваТАбличнаяЧастьПК
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.ПаспортаКачества КАК ПаспортаКачества
ПО ПаспортаКачестваТАбличнаяЧастьПК.Ссылка = ПаспортаКачества.Ссылка
ГДЕ
ПаспортаКачества.СсылкаДок = &СсылкаНаНалив
СГРУППИРОВАТЬ ПО
ПаспортаКачества.СсылкаДок,
ПаспортаКачестваТАбличнаяЧастьПК.Груз
ИНДЕКСИРОВАТЬ ПО
СсылкаДок,
Груз
;
////////////////////////////////////////////////////////////////////////////////
// ШАГ 2: Получаем уникальные данные по ЖД накладным во вторую временную таблицу.
ВЫБРАТЬ
ЖДНакладная.ПрефиксНомераЖДНакладной + ЖДНакладная.НомерЖДНакладной КАК ПолныйНомерЖдНакладной,
ЖДНакладная.Получатель.АдресПаспортаКачества КАК АдресПолучателя,
ЖДНакладная.Налив КАК Налив,
ЖДНакладная.Груз КАК Груз
ПОМЕСТИТЬ ВТ_Накладные
ИЗ
Документ.ЖДНакладная КАК ЖДНакладная
ГДЕ
ЖДНакладная.Статус В (&МассивСтатусов)
И ЖДНакладная.Получатель.ОтправкаПоПочте = ИСТИНА
И ЖДНакладная.Налив = &СсылкаНаНалив // Оптимизировано, убрали обращение через точку ".Ссылка"
ИНДЕКСИРОВАТЬ ПО
Налив,
Груз
;
////////////////////////////////////////////////////////////////////////////////
// ШАГ 3: Соединяем две подготовленные временные таблицы.
// Теперь дублирование строк физически невозможно, так как ключи связи уникальны.
ВЫБРАТЬ
ВТ_Накладные.ПолныйНомерЖдНакладной,
ВТ_Накладные.АдресПолучателя,
ВТ_Накладные.Налив,
ВТ_Накладные.Груз,
ВТ_ПаспортаСвернутые.Ответственый,
ВТ_ПаспортаСвернутые.ДатаИВремя,
ВТ_ПаспортаСвернутые.Отправка,
ВТ_ПаспортаСвернутые.Статус
ИЗ
ВТ_Накладные КАК ВТ_Накладные
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПаспортаСвернутые КАК ВТ_ПаспортаСвернутые
ПО ВТ_Накладные.Налив = ВТ_ПаспортаСвернутые.СсылкаДок
И ВТ_Накладные.Груз = ВТ_ПаспортаСвернутые.Груз
Давайте подробно разберем логику работы этой схемы:
ВТ_ПаспортаСвернутые мы применили секцию СГРУППИРОВАТЬ ПО. СУБД свернула все строки табличной части до уникальных пар СсылкаДок + Груз. Прочие поля (такие как дата и статус) были агрегированы с помощью функции МАКСИМУМ. На выходе получилась «плоская» таблица без дублей.ВТ_Накладные мы собрали нужные документы. Мы также оптимизировали условие по статусу: вместо медленной конструкции ИЛИ (как было у автора в Сообщении 1) мы передаем список статусов массивом в один параметр &МассивСтатусов и используем быстрый оператор В.Налив = СсылкаДок и Груз = Груз. Поскольку в левой и правой таблицах записи уникальны по этим ключам связи, СУБД выполняет чистое сопоставление 1-к-1, полностью исключая декартово умножение строк.Используя данную методику, вы не только решите проблему с дублированием данных, но и обеспечите максимальную производительность вашей базы данных, что особенно критично для крупных корпоративных систем на платформе 1С.