Об’єднайте стовпчик з декількох рядків в один ряд


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 у вашому, або в клієнтовому середовищі.
  • Функція CLR, швидше за все, швидше, і вона буде масштабуватися краще з урахуванням реального набору даних.
  • Функція CLR буде повторно використана в інших запитах, і вам не доведеться дублювати (і налагоджувати) складний підзапит кожен раз, коли вам потрібно робити цей тип речей.
  • Прямий T-SQL простіший, ніж написання та керування фрагментом зовнішнього коду.
  • Можливо, ви не знаєте, як програмувати на C # або VB.
  • тощо.

EDIT: Ну, я спробував перевірити, чи справді це краще, і виявляється, що вимогу, щоб коментарі були у визначеному порядку, наразі неможливо задовольнити за допомогою сукупної функції. :(

Див. 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

2
Ви не уникали курсору. Ви замість цього щойно назвали курсор циклом.
Аарон Бертран
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.