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