Как сгруппировать несколько строк в одну строку с разделителем в 1С?

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

При разработке отчетов и обработок в среде 1С программисты часто сталкиваются с классической задачей: необходимо сгруппировать строки таблицы и объединить текстовые или числовые значения подчиненных записей в одну строку через запятую, пробел или любой другой разделитель. В стандартных СУБД для этого существуют встроенные функции, такие как GROUP_CONCAT в MySQL или STRING_AGG в MS SQL Server. Однако в языке запросов 1С прямого аналога этих функций нет.

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


Решение №1. Использование Системы компоновки данных (СКД) и функции СоединитьСтроки()

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

Рассмотрим по шагам, как настроить склейку строк в СКД:

  1. Создайте схему компоновки данных и добавьте набор данных - запрос. Наш запрос может быть самым простым, выводящим плоскую таблицу без предварительной группировки. Например, выберем пользователей и приборы, которые за ними закреплены.
  2. Перейдите на вкладку «Ресурсы».
  3. Добавьте поле, значения которого нужно объединить (например, Прибор), в список ресурсов.
  4. В колонке «Выражение» напишите формулу объединения. Базовый синтаксис функции выглядит так:

    СоединитьСтроки(Прибор, ", ")

    Здесь первым параметром передается поле, значения которого мы соединяем, а вторым — строка-разделитель.

  5. Укажите, рассчитывать этот ресурс только по нужным группировкам (например, по группировке Юзер), чтобы на уровне детальных записей данные не дублировались.

Рассмотрим более сложную ситуацию. Что делать, если приборы могут повторяться, а нам нужно вывести только уникальные приборы через запятую? Для этого мы можем использовать функцию получения массива уникальных значений внутри ресурса. Изменим выражение ресурса на следующее:

СоединитьСтроки(ВычислитьВыражениеСГруппировкойМассив("Различные Прибор"), ", ")

Или воспользуемся альтернативным синтаксисом работы с таблицей значений внутри выражений СКД:

СоединитьСтроки(ТаблицаЗначений(Различные Прибор), ", ")

Использование СКД избавляет разработчика от необходимости писать сложный программный код и перекладывает всю рутину по форматированию и агрегации данных на платформу 1С. Это решение работает быстро и надежно.


Решение №2. Реализация задачи в «чистом» запросе (без СКД)

Иногда возникают ситуации, когда использовать СКД невозможно или нецелесообразно — например, если запрос выполняется внутри транзакции проведения документа или в фоновом задании, где на выходе нужно получить готовую строку. Хотя универсального механизма склейки неограниченного числа строк в запросе нет, мы можем применить обходные пути (так называемые «хаки»), если максимальное количество объединяемых строк заранее известно и невелико (например, не более 3–5 значений).

Вариант А. Левое соединение по порядковому номеру (транспонирование таблицы)

Идея метода заключается в том, чтобы пронумеровать строки внутри каждой группы, а затем соединить таблицу саму с собой левым соединением столько раз, сколько максимум строк может потребоваться объединить. Начиная с версии платформы 1С 8.3.20, мы можем использовать оконные функции для нумерации строк прямо в запросе.

Проанализируем пример запроса, где мы нумеруем приборы для каждого пользователя и склеиваем их:


ВЫБРАТЬ
    ИсходныеДанные.Юзер КАК Юзер,
    ИсходныеДанные.Прибор КАК Прибор,
    АВТОНОМЕРЗАПИСИ() КАК НомерСтроки
ПОМЕСТИТЬ ВТ_Исходная
ИЗ
    Справочник.ПриборыПользователей КАК ИсходныеДанные;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Т1.Юзер КАК Юзер,
    Т1.Прибор КАК Прибор1,
    ВЫБОР КОГДА Т2.Прибор ЕСТЬ НЕ NULL ТОГДА Т2.Прибор ИНАЧЕ "" КОНЕЦ КАК Прибор2,
    ВЫБОР КОГДА Т3.Прибор ЕСТЬ НЕ NULL ТОГДА Т3.Прибор ИНАЧЕ "" КОНЕЦ КАК Прибор3
ПОМЕСТИТЬ ВТ_Пронумерованная
ИЗ
    ВТ_Исходная КАК Т1
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Исходная КАК Т2 ПО Т1.Юзер = Т2.Юзер И Т2.НомерСтроки = Т1.НомерСтроки + 1
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Исходная КАК Т3 ПО Т1.Юзер = Т3.Юзер И Т3.НомерСтроки = Т1.НомерСтроки + 2
ГДЕ
    Т1.НомерСтроки = 1;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Т.Юзер КАК Юзер,
    Т.Прибор1 
    + ВЫБОР КОГДА Т.Прибор2 = "" ТОГДА "" ИНАЧЕ ", " + Т.Прибор2 КОНЕЦ 
    + ВЫБОР КОГДА Т.Прибор3 = "" ТОГДА "" ИНАЧЕ ", " + Т.Прибор3 КОНЕЦ КАК ВсеПриборы
ИЗ
    ВТ_Пронумерованная КАК Т

Этот метод полностью автономен и выполняется на стороне СУБД. Однако у него есть существенный минус: если количество подчиненных строк окажется больше, чем количество прописанных левых соединений в запросе, часть данных будет безвозвратно утеряна.

Вариант Б. Группировка с использованием функции МАКСИМУМ и ВЫБОР

Этот вариант подходит в тех случаях, когда у нас есть четко определенная структура данных или жесткий лимит на количество строк. Мы группируем таблицу по ключевому полю и с помощью агрегатной функции МАКСИМУМ вытаскиваем значения для каждого индекса строки:


ВЫБРАТЬ
    Т.Юзер КАК Юзер,
    МАКСИМУМ(ВЫБОР КОГДА Т.НомерСтроки = 1 ТОГДА Т.Прибор ИНАЧЕ "" КОНЕЦ) КАК Прибор1,
    МАКСИМУМ(ВЫБОР КОГДА Т.НомерСтроки = 2 ТОГДА Т.Прибор ИНАЧЕ "" КОНЕЦ) КАК Прибор2
ПОМЕСТИТЬ ВТ_Группировка
ИЗ
    ВТ_Исходная КАК Т
СГРУППИРОВАТЬ ПО
    Т.Юзер;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Г.Юзер КАК Юзер,
    Г.Прибор1 + " " + Г.Прибор2 КАК ВсеПриборы
ИЗ
    ВТ_Группировка КАК Г

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


Решение №3. Программная постобработка результатов запроса

Если количество объединяемых строк заранее неизвестно, а отчетные механизмы СКД применить нельзя, оптимальным и наиболее производительным решением является программная обработка выборки на встроенном языке 1С. Рассмотрим два эффективных алгоритма.

Вариант А. Обход выборки по группировкам (ИТОГИ ПО)

Этот способ является наиболее красивым и правильным с точки зрения платформенных стандартов 1С. Мы строим запрос с использованием предложения ИТОГИ ПО, а затем обходим выборку по двухуровневой схеме: сначала по группировочным записям, а затем внутри каждой группировки склеиваем детальные записи в цикле.

Посмотрим на пример реализации этого алгоритма:


// Шаг 1. Формируем запрос с итогами
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	Приборы.Юзер КАК Юзер,
    |	Приборы.Прибор КАК Прибор
    |ИЗ
    |	Справочник.ПриборыПользователей КАК Приборы
    |ИТОГИ ПО
    |	Юзер";

РезультатЗапроса = Запрос.Выполнить();

// Шаг 2. Обходим группировку верхнего уровня (Юзер)
ВыборкаЮзер = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Юзер");
Пока ВыборкаЮзер.Следующий() Цикл
    
    СписокПриборов = Новый СписокЗначений;
    
    // Шаг 3. Выбираем подчиненные детальные записи для текущего Юзера
    ВыборкаДетали = ВыборкаЮзер.Выбрать();
    Пока ВыборкаДетали.Следующий() Цикл
        // Добавляем прибор в список, проверяя на уникальность (при необходимости)
        Если СписокПриборов.НайтиПоЗначению(ВыборкаДетали.Прибор) = Неопределено Тогда
            СписокПриборов.Добавить(ВыборкаДетали.Прибор);
        КонецЕсли;
    КонецЦикла;
    
    // Шаг 4. Превращаем список в одну строку с разделителем
    СтрокаПриборов = СписокПриборов.ВыгрузитьЗначения();
    РезультатСтрокой = СтрСоединить(СтрокаПриборов, ", ");
    
    // Теперь переменная РезультатСтрокой содержит объединенные данные для конкретного пользователя
    Сообщить("Пользователь: " + ВыборкаЮзер.Юзер + " -> Приборы: " + РезультатСтрокой);
    
КонецЦикла;

Использование метода СтрСоединить() (доступно в платформе 8.3) значительно ускоряет конкатенацию строк по сравнению со стандартным сложением строк через оператор + в цикле, так как платформа оптимизирует выделение памяти под результирующий массив символов.

Вариант Б. Выгрузка в Таблицу Значений и программная свертка

Если вам привычнее работать с коллекциями в памяти, можно выгрузить результат запроса в ТаблицуЗначений, добавить новую колонку для итоговой строки, рассчитать её значения в цикле, а затем свернуть таблицу по ключевым полям.

Проанализируем этот алгоритм на примере:


// Шаг 1. Получаем сырые данные из запроса
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	Приборы.Юзер КАК Юзер,
    |	Приборы.Прибор КАК Прибор
    |ИЗ
    |	Справочник.ПриборыПользователей КАК Приборы
    |УПОРЯДОЧИТЬ ПО
    |	Юзер";

ТаблицаДанных = Запрос.Выполнить().Выгрузить();

// Шаг 2. Создаем результирующую таблицу для свернутых данных
ТаблицаРезультат = Новый ТаблицаЗначений;
ТаблицаРезультат.Колонки.Добавить("Юзер", ТаблицаДанных.Колонки.Юзер.ТипЗначения);
ТаблицаРезультат.Колонки.Добавить("ВсеПриборы", Новый ОписаниеТипов("Строка"));

// Шаг 3. Логика ручной свертки и склеивания данных
ТекущийЮзер = Неопределено;
ТекущиеПриборы = Новый Массив;

Для Каждого СтрокаДанных Из ТаблицаДанных Цикл
    
    Если СтрокаДанных.Юзер <> ТекущийЮзер Тогда
        
        // Записываем накопленные данные предыдущего пользователя
        Если ТекущийЮзер <> Неопределено Тогда
            НоваяСтрока = ТаблицаРезультат.Добавить();
            НоваяСтрока.Юзер = ТекущийЮзер;
            НоваяСтрока.ВсеПриборы = СтрСоединить(ТекущиеПриборы, ", ");
        КонецЕсли;
        
        // Сбрасываем буфер для нового пользователя
        ТекущийЮзер = СтрокаДанных.Юзер;
        ТекущиеПриборы.Очистить();
        
    КонецЕсли;
    
    // Добавляем значение в массив, если его там еще нет
    Если ТекущиеПриборы.Найти(СтрокаДанных.Прибор) = Неопределено Тогда
        ТекущиеПриборы.Добавить(СтрокаДанных.Прибор);
    КонецЕсли;
    
КонецЦикла;

// Не забываем записать данные последнего пользователя из выборки
Если ТекущийЮзер <> Неопределено Тогда
    НоваяСтрока = ТаблицаРезультат.Добавить();
    НоваяСтрока.Юзер = ТекущийЮзер;
    НоваяСтрока.ВсеПриборы = СтрСоединить(ТекущиеПриборы, ", ");
EndIf;

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


Сравнительный анализ решений

Давайте сопоставим все разобранные методы, чтобы вы могли быстро сориентироваться при решении реальных задач:

  1. Ресурс СКД СоединитьСтроки(): Отличное быстродействие. Идеально подходит для любых отчетов. Минимальные трудозатраты разработчика. Ограничение — требует использования СКД.
  2. Обход выборки запроса (ИТОГИ ПО): Высокое быстродействие. Отлично подходит для программных алгоритмов и проведения документов. Требует написания кода обхода выборки.
  3. Левое соединение в запросе: Среднее быстродействие. Подходит для жестко ограниченных структур данных (например, не более 3 элементов). Сложно поддерживать при увеличении объема данных.

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

← На главную