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