Gabungkan kolom dari beberapa baris menjadi satu baris


14

Saya punya beberapa customer_comments pembagian menjadi beberapa baris karena desain basis data, dan untuk laporan saya perlu menggabungkan commentsmasing-masing idbaris menjadi satu baris. Saya sebelumnya mencoba sesuatu yang bekerja dengan daftar terbatas ini dari klausa SELECT dan trik COALESCE tapi saya tidak dapat mengingatnya dan tidak boleh menyimpannya. Sepertinya saya tidak bisa membuatnya berfungsi dalam kasus ini, hanya tampaknya bekerja pada satu baris.

Data terlihat seperti ini:

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

Hasil saya harus terlihat seperti ini:

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

Jadi untuk masing-masing row_numhanya ada satu baris hasil; komentar harus dikombinasikan dalam urutanrow_num . SELECTTrik yang ditautkan di atas berfungsi untuk mendapatkan semua nilai untuk kueri tertentu sebagai satu baris, tapi saya tidak tahu cara membuatnya berfungsi sebagai bagian dari SELECTpernyataan yang memuntahkan semua baris ini.

Permintaan saya harus melalui seluruh tabel sendiri dan menampilkan baris ini. Saya tidak menggabungkannya menjadi beberapa kolom, satu untuk setiap baris, jadi PIVOTsepertinya tidak berlaku.

Jawaban:


18

Ini relatif sepele untuk dilakukan dengan subquery berkorelasi. Anda tidak dapat menggunakan metode COALESCE yang disorot dalam posting blog yang Anda sebutkan kecuali Anda mengekstraknya ke fungsi yang ditentukan pengguna (atau kecuali Anda hanya ingin mengembalikan satu baris sekaligus). Inilah cara saya biasanya melakukan ini:

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;

Jika Anda memiliki kasus di mana data dalam komentar bisa berisi tidak aman-untuk-XML karakter ( >, <, &), Anda harus mengubah ini:

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

Untuk pendekatan yang lebih rumit ini:

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

(Pastikan untuk menggunakan tipe data tujuan yang tepat, varcharatau nvarchar, dan panjang yang tepat, dan awali semua string literal dengan Njika menggunakan nvarchar.)


3
+1 Saya membuat biola untuk itu untuk melihat cepat sqlfiddle.com/#!3/e4ee5/2
MarlonRibunal

3
Yap, ini bekerja seperti pesona. @MarlonRibunal SQL Fiddle benar-benar terbentuk!
Ben Brocka

@NickChammas - Saya akan menjulurkan leher dan mengatakan bahwa pesanan dijamin menggunakan order bydalam permintaan sub. Ini adalah membangun XML menggunakan for xmldan itu adalah yang cara untuk membangun XML menggunakan TSQL. Urutan elemen dalam file XML adalah hal yang penting dan dapat diandalkan. Jadi jika teknik ini tidak menjamin pesanan maka dukungan XML di TSQL rusak parah.
Mikael Eriksson

2
Saya telah memvalidasi bahwa kueri akan mengembalikan hasil dalam urutan yang benar terlepas dari indeks berkerumun di tabel yang mendasari (bahkan indeks berkerumun pada row_num descharus mematuhi order byseperti yang disarankan Mikael). Saya akan menghapus komentar yang menyarankan sebaliknya sekarang bahwa kueri berisi hak order bydan berharap bahwa @JonSeigel menganggap melakukan hal yang sama.
Aaron Bertrand

6

Jika Anda diizinkan menggunakan CLR di lingkungan Anda, ini adalah kasus yang dibuat khusus untuk agregat yang ditentukan pengguna.

Secara khusus, ini mungkin cara untuk pergi jika sumber data non-sepele besar dan / atau Anda perlu melakukan hal semacam ini banyak dalam aplikasi Anda. Saya sangat curiga rencana permintaan untuk solusi Aaron tidak akan menskala dengan baik karena ukuran input bertambah. (Saya mencoba menambahkan indeks ke tabel temp, tetapi itu tidak membantu.)

Solusi ini, seperti banyak hal lainnya, merupakan kompromi:

  • Politik / kebijakan untuk bahkan menggunakan Integrasi CLR di lingkungan Anda, atau klien Anda.
  • Fungsi CLR kemungkinan lebih cepat, dan akan menskala dengan lebih baik mengingat sekumpulan data nyata.
  • Fungsi CLR akan dapat digunakan kembali dalam permintaan lain, dan Anda tidak perlu menduplikasi (dan men-debug) subquery kompleks setiap kali Anda perlu melakukan hal semacam ini.
  • Straight T-SQL lebih sederhana daripada menulis dan mengelola sepotong kode eksternal.
  • Mungkin Anda tidak tahu cara memprogram dalam C # atau VB.
  • dll.

EDIT: Ya, saya pergi untuk mencoba melihat apakah ini sebenarnya lebih baik, dan ternyata persyaratan bahwa komentar dalam urutan tertentu saat ini tidak mungkin dipenuhi dengan menggunakan fungsi agregat. :(

Lihat SqlUserDefinedAggregateAttribute.IsInvariantToOrder . Pada dasarnya, apa yang perlu Anda lakukan adalah OVER(PARTITION BY customer_code ORDER BY row_num)tetapi ORDER BYtidak didukung dalam OVERklausa ketika menjumlahkan. Saya berasumsi menambahkan fungsi ini ke SQL Server membuka sekaleng cacing, karena apa yang perlu diubah dalam rencana eksekusi itu sepele. Tautan yang disebutkan di atas mengatakan ini dicadangkan untuk penggunaan di masa mendatang, jadi ini dapat diterapkan di masa mendatang (pada 2005 Anda mungkin kurang beruntung).

Ini bisa masih bisa dicapai dengan kemasan dan parsing row_numnilai ke string dikumpulkan, dan kemudian melakukan semacam dalam objek CLR ... yang tampaknya cukup hackish.

Dalam hal apa pun, di bawah ini adalah kode yang saya gunakan seandainya ada orang lain yang menganggap ini berguna walaupun dengan batasan. Saya akan meninggalkan bagian peretasan sebagai latihan untuk pembaca. Perhatikan bahwa saya menggunakan AdventureWorks (2005) untuk data uji.

Perakitan agregat:

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 untuk pengujian ( CREATE ASSEMBLY, dan sp_configureuntuk mengaktifkan CLR dihilangkan):

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

Inilah solusi berbasis kursor yang menjamin urutan komentar oleh row_num. (Lihat jawaban saya yang lain untuk bagaimana [dbo].[Comments]tabel itu diisi.)

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
Anda tidak menghindari kursor. Anda baru saja memanggil kursor Anda sebagai pengulangan sementara.
Aaron Bertrand
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.