Как выбрать конкретное значение из табличной части документа в запросе 1С без дублирования строк?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке в среде 1С:Предприятие одной из самых распространенных задач является извлечение данных из табличных частей документов. Очень часто начинающие разработчики пытаются решить эту задачу «в лоб» — с помощью использования механизмов вложенных таблиц прямо в основном тексте запроса. Однако такой подход признан устаревшим, непроизводительным и крайне неудобным для дальнейшей обработки результатов.

В этой статье мы подробно разберем, почему не стоит использовать вложенные таблицы в запросах, как правильно связать основную таблицу документа с его табличной частью с помощью механизма связей, а также рассмотрим, как избежать типичной ошибки компилятора запросов 1С — неоднозначности полей. Дополнительно мы проанализируем смежные вопросы: проблему дублирования записей при объединении таблиц и вывод нескольких значений из табличной части в одну строку.

В чем проблема использования вложенных таблиц?

Давайте проанализируем исходный запрос, который часто пишут новички. В нем поле табличной части выбирается как подчиненный объект:


Запрос = Новый Запрос("ВЫБРАТЬ
|	КонтрольныеТочки.Ссылка КАК КонтрольнаяТочка,
|	КонтрольныеТочки.ПользователиКТ.(
|		Ответственный
|	)
|ИЗ
|	Документ.CRM_КонтрольнаяТочкаПроекта КАК КонтрольныеТочки");

На первый взгляд это кажется логичным: мы получаем документ, а внутри него — его табличную часть. Но на уровне СУБД такой запрос обрабатывается крайне неэффективно. Платформа делает выборку данных, формируя для каждой строки документа отдельный массив (вложенную таблицу) в оперативной памяти. Это существенно увеличивает нагрузку на сервер и усложняет чтение кода. Работать с результатом такого запроса неудобно — приходится обходить строки в двойном цикле.

Для эффективного извлечения данных в 1С всегда следует использовать соединения таблиц (в большинстве случаев — левое соединение). Это классический подход из теории реляционных баз данных, который позволяет СУБД оптимально построить план выполнения запроса.

Переходим на ЛЕВОЕ СОЕДИНЕНИЕ и разбираем ошибку неоднозначности

Попробуем связать основную таблицу документа и его табличную часть через левое соединение. Связующим звеном здесь выступает уникальный идентификатор документа — поле Ссылка.

Представим, что мы написали следующий запрос, чтобы выбрать только того пользователя из списка табличной части, напротив которого установлен флаг Ответственный:


Запрос = Новый Запрос("ВЫБРАТЬ
|	КонтрольныеТочки.Ссылка КАК КонтрольнаяТочка,
|	ПользователиКТ.Пользователь КАК ОтветственныйЗаКТ
|ИЗ
|	Документ.CRM_КонтрольнаяТочкаПроекта КАК КонтрольныеТочки
|		ЛЕВОЕ СОЕДИНЕНИЕ Документ.CRM_КонтрольнаяТочкаПроекта.ПользователиКТ КАК ПользователиКТ
|		ПО КонтрольныеТочки.Ссылка = ПользователиКТ.Ссылка 
|			И ПользователиКТ.Ответственный = ИСТИНА");

При попытке запустить данный запрос платформа выдаст ошибку: «Неоднозначное поле "ПользователиКТ.Ссылка"». Почему так произошло?

Причина кроется в именах таблиц и их синонимах. В тексте запроса физическое имя табличной части документа записано как Документ.CRM_КонтрольнаяТочкаПроекта.ПользователиКТ. При этом в качестве псевдонима (алиаса) этой таблицы после ключевого слова КАК было указано точно такое же имя — ПользователиКТ. Платформа запуталась, к какой именно таблице (к исходной табличной части или к результирующему псевдониму соединения) мы обращаемся в условии связи ПО.

Чтобы раз и навсегда решить эту проблему, достаточно дать табличной части уникальный, отличающийся от ее физического имени псевдоним — например, ПользователиТЧ. Давайте посмотрим на правильный вариант запроса:


Запрос = Новый Запрос("ВЫБРАТЬ
|	КонтрольныеТочки.Ссылка КАК КонтрольнаяТочка,
|	КонтрольныеТочки.НаВесьДень КАК НаВесьДень,
|	КонтрольныеТочки.Тема КАК Тема,
|	КонтрольныеТочки.Дата КАК Дата,
|	КонтрольныеТочки.ДатаЗакрытия КАК ДатаЗакрытия,
|	КонтрольныеТочки.Этап КАК Этап,
|	КонтрольныеТочки.Этап.Ответственный КАК ОтветственныйЗаЭтап,
|	ЕСТЬNULL(ПользователиТЧ.Пользователь, ЗНАЧЕНИЕ(Справочник.Пользователи.ПустаяСсылка)) КАК ОтветственныйЗаКТ
|ИЗ
|	Документ.CRM_КонтрольнаяТочкаПроекта КАК КонтрольныеТочки
|		ЛЕВОЕ СОЕДИНЕНИЕ Документ.CRM_КонтрольнаяТочкаПроекта.ПользователиКТ КАК ПользователиТЧ
|		ПО КонтрольныеТочки.Ссылка = ПользователиТЧ.Ссылка 
|			И ПользователиТЧ.Ответственный = ИСТИНА
|ГДЕ
|	КонтрольныеТочки.Проект = &Проект
|	И НЕ КонтрольныеТочки.ПометкаУдаления
|	И КонтрольныеТочки.Статус = ЗНАЧЕНИЕ(Перечисление.CRM_СтатусыКонтрольныхТочек.Запланирована)");

В этом запросе мы использовали функцию ЕСТЬNULL. Она необходима для того, чтобы в случае отсутствия ответственного в табличной части запроса (или если сам документ пустой) система вывела не значение Null, а пустую ссылку на справочник пользователей. Это гарантирует стабильную работу последующего кода и исключает ошибки типов данных.

Опасность дублирования строк при ЛЕВОМ СОЕДИНЕНИИ

Использование левого соединения — отличный и быстрый инструмент, но при работе с табличными частями он таит в себе скрытую опасность. Мы должны всегда помнить о логике данных.

Представим ситуацию: пользователь в режиме предприятия случайно или намеренно установил флаг Ответственный сразу у двух сотрудников в одной и той же табличной части документа. Что произойдет при выполнении нашего запроса?

СУБД честно выполнит условие связи и вернет для одного этого документа две строки в результате запроса. В итоге в вашем отчете, списке или обработке один и тот же документ задвоится. Если на основании этого запроса рассчитываются суммы или выводятся количественные показатели, итоги будут искажены.

Как мы можем гарантированно защитить себя от дублирования записей? Существует два основных способа решения этой проблемы.

Способ 1. Использование группировки в запросе

Мы можем принудительно сгруппировать результат запроса по полям документа. Для поля ответственного в таком случае применяется агрегатная функция (например, МАКСИМУМ или МИНИМУМ). Таким образом, даже если ответственных будет несколько, запрос вернет ровно одну строку по документу с одним из ответственных.


Запрос = Новый Запрос("ВЫБРАТЬ
|	КонтрольныеТочки.Ссылка КАК КонтрольнаяТочка,
|	КонтрольныеТочки.Тема КАК Тема,
|	МАКСИМУМ(ЕСТЬNULL(ПользователиТЧ.Пользователь, ЗНАЧЕНИЕ(Справочник.Пользователи.ПустаяСсылка))) КАК ОтветственныйЗаКТ
|ИЗ
|	Документ.CRM_КонтрольнаяТочкаПроекта КАК КонтрольныеТочки
|		ЛЕВОЕ СОЕДИНЕНИЕ Документ.CRM_КонтрольнаяТочкаПроекта.ПользователиКТ КАК ПользователиТЧ
|		ПО КонтрольныеТочки.Ссылка = ПользователиТЧ.Ссылка 
|			И ПользователиТЧ.Ответственный = ИСТИНА
|СГРУППИРОВАТЬ ПО
|	КонтрольныеТочки.Ссылка,
|	КонтрольныеТочки.Тема");

Способ 2. Предварительная фильтрация во Временной Таблице (ВТ)

Более профессиональным и быстродействующим методом (особенно на больших базах данных) является предварительный отбор уникальных значений во временную таблицу. Мы сначала собираем уникальные пары Ссылка — Ответственный в отдельный пакет запроса, избавляясь от дублей с помощью группировки, а затем соединяем наши документы уже с этой чистой временной таблицей.


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|	ПользователиТЧ.Ссылка КАК Ссылка,
|	МАКСИМУМ(ПользователиТЧ.Пользователь) КАК Пользователь
|ПОМЕСТИТЬ ВТ_Ответственные
|ИЗ
|	Документ.CRM_КонтрольнаяТочкаПроекта.ПользователиКТ КАК ПользователиТЧ
|ГДЕ
|	ПользователиТЧ.Ответственный = ИСТИНА
|СГРУППИРОВАТЬ ПО
|	ПользователиТЧ.Ссылка
|;
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	КонтрольныеТочки.Ссылка КАК КонтрольнаяТочка,
|	КонтрольныеТочки.Тема КАК Тема,
|	ЕСТЬNULL(ВТ_Ответственные.Пользователь, ЗНАЧЕНИЕ(Справочник.Пользователи.ПустаяСсылка)) КАК ОтветственныйЗаКТ
|ИЗ
|	Документ.CRM_КонтрольнаяТочкаПроекта КАК КонтрольныеТочки
|		ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Ответственные КАК ВТ_Ответственные
|		ПО КонтрольныеТочки.Ссылка = ВТ_Ответственные.Ссылка";

Как вывести всех пользователей табличной части в одну строку через запятую?

Теперь обратимся к более сложной задаче, которая созвучна с названием темы обсуждения. Что делать, если нам нужно вывести в одну ячейку (строку) вообще всех пользователей, перечисленных в табличной части, разделив их запятыми?

В отличие от классического SQL, где присутствуют встроенные функции агрегации строк (такие как STRING_AGG или GROUP_CONCAT), в чистом языке запросов 1С объединить строки «на лету» невозможно. Для реализации этой задачи у нас есть три проверенных пути.

Вариант А. Использование возможностей СКД (Системы компоновки данных)

Если ваша задача — построить отчет, то СКД предоставляет великолепную встроенную функцию СоединитьСтроки(). Она позволяет автоматически свернуть детальные записи в одну строку с разделителем.

Для настройки этого поведения выполните следующие шаги:

  1. В схеме компоновки данных на вкладке «Ресурсы» добавьте поле, содержащее список пользователей (например, ОтветственныйЗаКТ).
  2. В колонке «Выражение» напишите формулу свертки: СоединитьСтроки(ОтветственныйЗаКТ, ", ").
  3. В настройках отчета укажите группировку по шапке документа. Платформа самостоятельно соберет все строки из подчиненной ТЧ, склеит их через запятую и выведет красивый результат в одну ячейку отчета.

Вариант Б. Программный обход выборки в коде 1С

Если результат запроса обрабатывается программно (например, для программного заполнения печатной формы макета или реквизитов на форме), свертку строк осуществляют вручную при обходе выборки.

И вот здесь нам как раз могут сослужить хорошую службу вложенные таблицы! Хотя выше мы советовали отказаться от них ради оптимизации, в случае, когда нам обязательно нужно программно обойти все подчиненные строки каждого документа, структура вложенной таблицы позволяет написать лаконичный и понятный код:


ВыборкаДокументов = РезультатЗапроса.Выбрать();
Пока ВыборкаДокументов.Следующий() Цикл
	
	// Выводим данные шапки документа
	Сообщить("Документ: " + ВыборкаДокументов.КонтрольнаяТочка);
	
	// Обходим вложенную таблицу пользователей для данного документа
	ВыборкаТЧ = ВыборкаДокументов.ПользователиКТ.Выбрать();
	СтрокаПользователей = "";
	
	Пока ВыборкаТЧ.Следующий() Цикл
		СтрокаПользователей = СтрокаПользователей 
			+ ?(СтрокаПользователей = "", "", ", ") 
			+ СокрЛП(ВыборкаТЧ.Ответственный);
	КонецЦикла;
	
	// Теперь у нас есть собранная строка: "Иванов И.И., Петров П.П."
	Сообщить("Список ответственных: " + СтрокаПользователей);
	
КонецЦикла;

Подведем итоги

Мы подробно разобрали принципы работы с табличными частями в запросах 1С. Подводя итог, сформулируем основные правила:

  1. Для получения данных из табличных частей в 90% случаев используйте ЛЕВОЕ СОЕДИНЕНИЕ, а не вложенные таблицы.
  2. Всегда давайте табличной части в соединении уникальный синоним (например, ПользователиТЧ вместо ПользователиКТ), чтобы избежать ошибки неоднозначности полей.
  3. Помните о риске размножения строк при связях типа "один ко многим". Используйте группировки и функции агрегации (МАКСИМУМ), либо предварительно очищайте данные во временных таблицах.
  4. Если вам необходимо объединить несколько строк в одну текстовую строку через запятую, делайте это либо средствами СКД с помощью функции СоединитьСтроки(), либо путем программной постобработки выборки в коде.

Соблюдение этих несложных правил позволит вам писать быстрый, легко читаемый и надежный код, который будет работать стабильно независимо от объема вашей базы данных.

← На главную