Объединить столбец из нескольких строк в одну строку

14

У меня есть некоторые customer_commentsразбиты на несколько строк из-за дизайна базы данных, и для отчета мне нужно объединить commentsиз каждого уникального idв одну строку. Ранее я пытался что-то работать с этим разделенным списком из предложения SELECT и трюка COALESCE, но я не могу вспомнить это и не должен был его сохранить. Я не могу заставить его работать в этом случае, кажется, работает только в одной строке.

Данные выглядят так:

id  row_num  customer_code comments
-----------------------------------
1   1        Dilbert        Hard
1   2        Dilbert        Worker
2   1        Wally          Lazy

Мои результаты должны выглядеть так:

id  customer_code comments
------------------------------
1   Dilbert        Hard Worker
2   Wally          Lazy

Таким образом, для каждого row_numесть только один ряд результатов; комментарии должны быть объединены в порядке row_num. Приведенный выше SELECTтрюк работает, чтобы получить все значения для определенного запроса в виде одной строки, но я не могу понять, как заставить его работать как часть SELECTоператора, который выплевывает все эти строки.

Мой запрос должен пройти всю таблицу самостоятельно и вывести эти строки. Я не объединяю их в несколько столбцов, по одному для каждой строки, поэтому PIVOTне представляется возможным.

Бен Брока
источник

Ответы:

18

Это относительно тривиально сделать с коррелированным подзапросом. Вы не можете использовать метод COALESCE, выделенный в упомянутом сообщении в блоге, если вы не извлекаете его в пользовательскую функцию (или если вы не хотите возвращать только одну строку за раз). Вот как я обычно делаю это:

DECLARE @x TABLE 
(
  id INT, 
  row_num INT, 
  customer_code VARCHAR(32), 
  comments VARCHAR(32)
);

INSERT @x SELECT 1,1,'Dilbert','Hard'
UNION ALL SELECT 1,2,'Dilbert','Worker'
UNION ALL SELECT 2,1,'Wally','Lazy';

SELECT id, customer_code, comments = STUFF((SELECT ' ' + comments 
    FROM @x AS x2 WHERE id = x.id
     ORDER BY row_num
     FOR XML PATH('')), 1, 1, '')
FROM @x AS x
GROUP BY id, customer_code
ORDER BY id;

Если у Вас есть случай , когда данные в комментариях могут содержать небезопасном для XML-символов ( >, <, &), вы должны изменить это:

     FOR XML PATH('')), 1, 1, '')

К этому более сложному подходу:

     FOR XML PATH(''), TYPE).value(N'(./text())[1]', N'varchar(max)'), 1, 1, '')

(Обязательно используйте правильный тип данных назначения, varcharили nvarchar, и правильную длину, а также префикс всех строковых литералов Nпри использовании nvarchar.)

Аарон Бертран
источник
3
+1 Для этого я создал скрипку для быстрого просмотра sqlfiddle.com/#!3/e4ee5/2
MarlonRibunal
3
Да, это работает как шарм. @MarlonRibunal SQL Fiddle действительно складывается!
Бен Брокка
@NickChammas - я собираюсь высунуть свою шею и сказать, что заказ гарантирован, используя order byв подзапросе. Это построение XML с использованием , for xmlи это способ построения XML с использованием TSQL. Порядок элементов в файлах XML является важным вопросом и на него можно положиться. Так что если этот метод не гарантирует порядок, то поддержка XML в TSQL серьезно нарушена.
Микаэль Эрикссон
2
Я подтвердил, что запрос будет возвращать результаты в правильном порядке, независимо от кластеризованного индекса в базовой таблице (даже кластеризованный индекс row_num descдолжен подчиняться, order byкак предложил Микаэль). Я собираюсь удалить комментарии, предлагающие обратное, теперь, когда запрос содержит право, order byи надеюсь, что @JonSeigel подумает о том же.
Аарон Бертран
6

Если вам разрешено использовать CLR в вашей среде, это индивидуальный подход для пользовательского агрегата.

В частности, это, вероятно, тот путь, если исходные данные нетривиально велики, и / или вам нужно много чего делать в своем приложении. Я сильно подозреваю, что план запроса для решения Аарона не будет хорошо масштабироваться по мере увеличения размера ввода. (Я попытался добавить индекс во временную таблицу, но это не помогло.)

Это решение, как и многие другие, является компромиссом:

  • Политика / политика даже для использования CLR Integration в вашей среде или среде вашего клиента.
  • Функция CLR, скорее всего, быстрее и будет лучше масштабироваться с учетом реального набора данных.
  • Функция CLR будет многократно использоваться в других запросах, и вам не придется дублировать (и отлаживать) сложный подзапрос каждый раз, когда вам понадобится выполнить подобные действия.
  • Прямой T-SQL проще, чем написание и управление фрагментом внешнего кода.
  • Возможно, вы не знаете, как программировать на C # или VB.
  • и т.п.

РЕДАКТИРОВАТЬ: Ну, я пошел, чтобы попытаться увидеть, было ли это на самом деле лучше, и оказалось, что требование, чтобы комментарии были в определенном порядке, в настоящее время невозможно удовлетворить с помощью функции агрегирования. :(

См. SqlUserDefinedAggregateAttribute.IsInvariantToOrder . По сути, вам нужно, OVER(PARTITION BY customer_code ORDER BY row_num)но ORDER BYне поддерживается в OVERпредложении при агрегировании. Я предполагаю, что добавление этой функции в SQL Server открывает червя, потому что то, что нужно изменить в плане выполнения, тривиально. Вышеупомянутая ссылка говорит, что это зарезервировано для будущего использования, так что это может быть реализовано в будущем (хотя в 2005 году вам, вероятно, не повезло).

Это может еще быть достигнуто путем упаковки и разбора row_numзначения в агрегированной строку, а затем делает вид внутри объекта CLR ... который кажется довольно хаком.

В любом случае, ниже приведен код, который я использовал на тот случай, если кто-то сочтет это полезным, даже с ограничениями. Я оставлю хакерскую часть как упражнение для читателя. Обратите внимание, что я использовал AdventureWorks (2005) для тестовых данных.

Агрегатная сборка:

using System;
using System.IO;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

namespace MyCompany.SqlServer
{
    [Serializable]
    [SqlUserDefinedAggregate
    (
        Format.UserDefined,
        IsNullIfEmpty = false,
        IsInvariantToDuplicates = false,
        IsInvariantToNulls = true,
        IsInvariantToOrder = false,
        MaxByteSize = -1
    )]
    public class StringConcatAggregate : IBinarySerialize
    {
        private string _accum;
        private bool _isEmpty;

        public void Init()
        {
            _accum = string.Empty;
            _isEmpty = true;
        }

        public void Accumulate(SqlString value)
        {
            if (!value.IsNull)
            {
                if (!_isEmpty)
                    _accum += ' ';
                else
                    _isEmpty = false;

                _accum += value.Value;
            }
        }

        public void Merge(StringConcatAggregate value)
        {
            Accumulate(value.Terminate());
        }

        public SqlString Terminate()
        {
            return new SqlString(_accum);
        }

        public void Read(BinaryReader r)
        {
            this.Init();

            _accum = r.ReadString();
            _isEmpty = _accum.Length == 0;
        }

        public void Write(BinaryWriter w)
        {
            w.Write(_accum);
        }
    }
}

T-SQL для тестирования ( CREATE ASSEMBLYи sp_configureдля включения CLR опущено):

CREATE TABLE [dbo].[Comments]
(
    CustomerCode int NOT NULL,
    RowNum int NOT NULL,
    Comments nvarchar(25) NOT NULL
)

INSERT INTO [dbo].[Comments](CustomerCode, RowNum, Comments)
    SELECT
        DENSE_RANK() OVER(ORDER BY FirstName),
        ROW_NUMBER() OVER(PARTITION BY FirstName ORDER BY ContactID),
        Phone
        FROM [AdventureWorks].[Person].[Contact]
GO

CREATE AGGREGATE [dbo].[StringConcatAggregate]
(
    @input nvarchar(MAX)
)
RETURNS nvarchar(MAX)
EXTERNAL NAME StringConcatAggregate.[MyCompany.SqlServer.StringConcatAggregate]
GO


SELECT
    CustomerCode,
    [dbo].[StringConcatAggregate](Comments) AS AllComments
    FROM [dbo].[Comments]
    GROUP BY CustomerCode
Джон Сайгель
источник
1

Вот решение на основе курсора, которое гарантирует порядок комментариев по row_num. (См. Мой другой ответ о том, как [dbo].[Comments]таблица была заполнена.)

SET NOCOUNT ON

DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
    SELECT
        CustomerCode,
        Comments
        FROM [dbo].[Comments]
        ORDER BY
            CustomerCode,
            RowNum

DECLARE @curCustomerCode int
DECLARE @lastCustomerCode int
DECLARE @curComment nvarchar(25)
DECLARE @comments nvarchar(MAX)

DECLARE @results table
(
    CustomerCode int NOT NULL,
    AllComments nvarchar(MAX) NOT NULL
)


OPEN cur

FETCH NEXT FROM cur INTO
    @curCustomerCode, @curComment

SET @lastCustomerCode = @curCustomerCode


WHILE @@FETCH_STATUS = 0
BEGIN

    IF (@lastCustomerCode != @curCustomerCode)
    BEGIN
        INSERT INTO @results(CustomerCode, AllComments)
            VALUES(@lastCustomerCode, @comments)

        SET @lastCustomerCode = @curCustomerCode
        SET @comments = NULL
    END

    IF (@comments IS NULL)
        SET @comments = @curComment
    ELSE
        SET @comments = @comments + N' ' + @curComment

    FETCH NEXT FROM cur INTO
        @curCustomerCode, @curComment

END

IF (@comments IS NOT NULL)
BEGIN
    INSERT INTO @results(CustomerCode, AllComments)
        VALUES(@curCustomerCode, @comments)
END

CLOSE cur
DEALLOCATE cur


SELECT * FROM @results
Джон Сайгель
источник
0
-- solution avoiding the cursor ...

DECLARE @idMax INT
DECLARE @idCtr INT
DECLARE @comment VARCHAR(150)

SELECT @idMax = MAX(id)
FROM [dbo].[CustomerCodeWithSeparateComments]

IF @idMax = 0
    return
DECLARE @OriginalTable AS Table
(
    [id] [int] NOT NULL,
    [row_num] [int] NULL,
    [customer_code] [varchar](50) NULL,
    [comment] [varchar](120) NULL
)

DECLARE @FinalTable AS Table
(
    [id] [int] IDENTITY(1,1) NOT NULL,
    [customer_code] [varchar](50) NULL,
    [comment] [varchar](120) NULL
)

INSERT INTO @FinalTable 
([customer_code])
SELECT [customer_code]
FROM [dbo].[CustomerCodeWithSeparateComments]
GROUP BY [customer_code]

INSERT INTO @OriginalTable
           ([id]
           ,[row_num]
           ,[customer_code]
           ,[comment])
SELECT [id]
      ,[row_num]
      ,[customer_code]
      ,[comment]
FROM [dbo].[CustomerCodeWithSeparateComments]
ORDER BY id, row_num

SET @idCtr = 1
SET @comment = ''

WHILE @idCtr < @idMax
BEGIN

    SELECT @comment = @comment + ' ' + comment
    FROM @OriginalTable 
    WHERE id = @idCtr
    UPDATE @FinalTable
       SET [comment] = @comment
    WHERE [id] = @idCtr 
    SET @idCtr = @idCtr + 1
    SET @comment = ''

END 

SELECT @comment = @comment + ' ' + comment
        FROM @OriginalTable 
        WHERE id = @idCtr

UPDATE @FinalTable
   SET [comment] = @comment
WHERE [id] = @idCtr

SELECT *
FROM @FinalTable
Gary
источник
2
Вы не избежали курсора. Вы только что назвали свой курсор циклом while.
Аарон Бертран