Połącz kolumnę z wielu wierszy w pojedynczy wiersz

14

Niektóre zostały customer_commentspodzielone na wiele wierszy ze względu na projekt bazy danych, a dla raportu muszę połączyć commentskażdy z unikalnych idw jeden wiersz. Wcześniej próbowałem czegoś, co działa z tą ograniczoną listą z klauzuli SELECT i sztuczki COALESCE, ale nie mogę sobie tego przypomnieć i nie mogłem tego zapisać. Wydaje mi się, że nie mogę go uruchomić w tym przypadku, wydaje się, że działa tylko w jednym rzędzie.

Dane wyglądają następująco:

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

Moje wyniki muszą wyglądać następująco:

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

Tak więc dla każdego row_numjest naprawdę tylko jeden wiersz wyników; komentarze powinny być łączone w kolejności row_num. Powyższa połączona SELECTsztuczka działa, aby uzyskać wszystkie wartości dla określonego zapytania w jednym wierszu, ale nie mogę wymyślić, jak to zrobić, jako część SELECTinstrukcji, która wyrzuca wszystkie te wiersze.

Moje zapytanie musi samodzielnie przejść przez całą tabelę i wyprowadzić te wiersze. Nie łączę ich w wiele kolumn, po jednej dla każdego wiersza, więc PIVOTwydaje się , że nie ma zastosowania.

Ben Brocka
źródło

Odpowiedzi:

18

Jest to stosunkowo trywialne w przypadku skorelowanego podkwerendy. Nie możesz użyć metody COALESCE wyróżnionej w poście na blogu, o którym wspominasz, chyba że wyodrębnisz ją do funkcji zdefiniowanej przez użytkownika (lub chyba, że ​​chcesz zwrócić tylko jeden wiersz na raz). Oto jak zazwyczaj to robię:

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;

Jeśli masz przypadku, gdy dane w komentarzach mogą zawierać niebezpieczne-dla-XML znaków ( >, <, &), należy zmienić:

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

Do tego bardziej złożonego podejścia:

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

(Pamiętaj, aby użyć właściwego docelowego typu danych varcharlub nvarcharodpowiedniej długości i poprzedzić ją literałami ciągów, Njeśli są używane nvarchar).

Aaron Bertrand
źródło
3
+1 Stworzyłem do tego skrzypce do szybkiego obejrzenia sqlfiddle.com/#!3/e4ee5/2
MarlonRibunal
3
Tak, to działa jak urok. @MarlonRibunal SQL Fiddle naprawdę się kształtuje!
Ben Brocka
@NickChammas - Mam zamiar wystawić szyję i powiedzieć, że zamówienie jest gwarantowane przy użyciu order byzapytania podrzędnego. To jest XML budynku za for xmli to sposób na budowanie XML przy użyciu TSQL. Kolejność elementów w plikach XML jest ważną kwestią i można na niej polegać. Jeśli więc ta technika nie gwarantuje porządku, obsługa XML w TSQL jest poważnie zepsuta.
Mikael Eriksson
2
Zweryfikowałem, że zapytanie zwróci wyniki we właściwej kolejności, niezależnie od indeksu klastrowego w tabeli bazowej (nawet indeks klastrowany row_num descmusi być zgodny z order bysugerowanym przez Mikaela). Mam zamiar usunąć komentarze sugerujące inaczej, ponieważ zapytanie zawiera prawo order byi mam nadzieję, że @JonSeigel rozważa zrobienie tego samego.
Aaron Bertrand
6

Jeśli możesz używać CLR w swoim środowisku, jest to specjalnie dostosowany przypadek agregacji zdefiniowanej przez użytkownika.

W szczególności jest to prawdopodobnie właściwy sposób, jeśli dane źródłowe nie są trywialnie duże i / lub musisz często wykonywać tego typu czynności w swojej aplikacji. Podejrzewam, że plan zapytań dla rozwiązania Aarona nie będzie dobrze skalowany wraz ze wzrostem wielkości wejściowej. (Próbowałem dodać indeks do tabeli tymczasowej, ale to nie pomogło).

To rozwiązanie, podobnie jak wiele innych rzeczy, stanowi kompromis:

  • Polityka / polityka dotycząca nawet korzystania z integracji CLR w środowisku twojego lub twojego klienta.
  • Funkcja CLR jest prawdopodobnie szybsza i lepiej się skaluje, biorąc pod uwagę prawdziwy zestaw danych.
  • Funkcja CLR będzie nadawać się do ponownego użycia w innych zapytaniach i nie będziesz musiał duplikować (i debugować) złożonego podzapytania za każdym razem, gdy będziesz tego potrzebować.
  • Prosty T-SQL jest prostszy niż pisanie zewnętrznego kodu i zarządzanie nim.
  • Być może nie wiesz, jak programować w C # lub VB.
  • itp.

EDYCJA: Cóż, poszedłem sprawdzić, czy to rzeczywiście było lepsze, i okazuje się, że wymaganie, aby komentarze były w określonej kolejności, nie jest obecnie możliwe do spełnienia przy użyciu funkcji agregującej. :(

Zobacz SqlUserDefinedAggregateAttribute.IsInvariantToOrder . Zasadniczo, co trzeba zrobić, to OVER(PARTITION BY customer_code ORDER BY row_num)jednak ORDER BYnie jest obsługiwana w OVERklauzuli podczas agregowania. Zakładam, że dodanie tej funkcji do programu SQL Server otwiera puszkę robaków, ponieważ to, co należałoby zmienić w planie wykonania, jest banalne. Wyżej wspomniany link mówi, że jest on zarezerwowany do wykorzystania w przyszłości, więc można go zaimplementować w przyszłości (w 2005 r. Prawdopodobnie nie masz szczęścia).

Można to nadal osiągnąć poprzez spakowanie i parsowanie row_numwartości w zagregowanym ciągu, a następnie wykonanie sortowania w obiekcie CLR ... co wydaje się dość hackerskie.

W każdym razie poniżej znajduje się kod, którego użyłem na wypadek, gdyby ktokolwiek uznał to za przydatne, nawet z ograniczeniami. Część hakerską zostawię jako ćwiczenie dla czytelnika. Zauważ, że użyłem AdventureWorks (2005) do danych testowych.

Montaż agregatu:

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 do testowania ( CREATE ASSEMBLYi sp_configureaby pominąć 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
Jon Seigel
źródło
1

Oto rozwiązanie oparte na kursorach, które gwarantuje kolejność komentarzy według row_num. (Zobacz moją drugą odpowiedź dotyczącą sposobu [dbo].[Comments]zapełniania tabeli).

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
Jon Seigel
źródło
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
źródło
2
Nie uniknąłeś kursora. Zamiast tego właśnie wywołałeś pętlę while.
Aaron Bertrand