Kết hợp cột từ nhiều hàng thành một hàng


14

Tôi đã customer_commentschia ra thành nhiều hàng do thiết kế cơ sở dữ liệu và để báo cáo tôi cần kết hợp commentstừ mỗi hàng idthành một hàng. Trước đây tôi đã thử một cái gì đó làm việc với danh sách được phân tách này từ mệnh đề SELECT và thủ thuật COALESCE nhưng tôi không thể nhớ lại và không được lưu nó. Tôi dường như không thể làm cho nó hoạt động trong trường hợp này, dường như chỉ hoạt động trên một hàng duy nhất.

Dữ liệu trông như thế này:

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

Kết quả của tôi cần phải như thế này:

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

Vì vậy, đối với mỗi row_numngười thực sự chỉ có một hàng kết quả; các ý kiến ​​nên được kết hợp theo thứ tự row_num. Thủ SELECTthuật được liên kết ở trên hoạt động để lấy tất cả các giá trị cho một truy vấn cụ thể dưới dạng một hàng, nhưng tôi không thể tìm ra cách làm cho nó hoạt động như một phần của SELECTcâu lệnh phun ra tất cả các hàng này.

Truy vấn của tôi phải tự đi qua toàn bộ bảng và xuất các hàng này. Tôi không kết hợp chúng thành nhiều cột, mỗi cột cho mỗi hàng, vì vậy PIVOTdường như không thể áp dụng được.

Câu trả lời:


18

Điều này là tương đối tầm thường để làm với một truy vấn con tương quan. Bạn không thể sử dụng phương thức COALESCE được tô sáng trong bài đăng trên blog mà bạn đề cập trừ khi bạn trích xuất hàm đó cho hàm do người dùng xác định (hoặc trừ khi bạn chỉ muốn trả về một hàng mỗi lần). Đây là cách tôi thường làm điều này:

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;

Nếu bạn có một trường hợp dữ liệu trong bình có thể chứa không an toàn-cho-XML ký tự ( >, <, &), bạn nên thay đổi điều này:

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

Để tiếp cận công phu hơn này:

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

(Đảm bảo sử dụng đúng loại dữ liệu đích, varcharhoặc nvarchar, và độ dài phù hợp và tiền tố tất cả các chuỗi ký tự bằng Nnếu sử dụng nvarchar.)


3
+1 Tôi đã tạo ra một câu đố cho điều đó để có cái nhìn nhanh về sqlfiddle.com/#!3/e4ee5/2
MarlonRibunal

3
Đúng, điều này hoạt động như một nét duyên dáng. @MarlonRibunal SQL Fiddle thực sự định hình!
Ben Brocka

@NickChammas - Tôi sẽ thò cổ ra và nói rằng thứ tự được đảm bảo bằng cách sử dụng order bytruy vấn phụ. Này được xây dựng XML sử dụng for xmlvà đó là những cách để xây dựng XML sử dụng TSQL. Thứ tự các phần tử trong tệp XML là một vấn đề quan trọng và có thể dựa vào. Vì vậy, nếu kỹ thuật này không đảm bảo trật tự thì hỗ trợ XML trong TSQL bị phá vỡ nghiêm trọng.
Mikael Eriksson

2
Tôi đã xác thực rằng truy vấn sẽ trả về kết quả theo đúng thứ tự bất kể chỉ mục được nhóm trên bảng bên dưới (ngay cả một chỉ mục được nhóm row_num descphải tuân theo order bynhư Mikael đề xuất). Bây giờ tôi sẽ xóa các nhận xét đề xuất rằng truy vấn có quyền order byvà hy vọng rằng @JonSeigel xem xét thực hiện tương tự.
Aaron Bertrand

6

Nếu bạn được phép sử dụng CLR trong môi trường của mình, đây là trường hợp được thiết kế riêng cho tổng hợp do người dùng xác định.

Đặc biệt, đây có lẽ là cách để đi nếu dữ liệu nguồn không quá lớn và / hoặc bạn cần thực hiện loại điều này rất nhiều trong ứng dụng của mình. Tôi hoàn toàn nghi ngờ kế hoạch truy vấn cho giải pháp của Aaron sẽ không mở rộng được khi kích thước đầu vào tăng lên. (Tôi đã thử thêm một chỉ mục vào bảng tạm thời, nhưng điều đó không có ích.)

Giải pháp này, giống như nhiều thứ khác, là một sự đánh đổi:

  • Chính trị / chính sách thậm chí sử dụng Tích hợp CLR trong môi trường của bạn hoặc của khách hàng của bạn.
  • Chức năng CLR có khả năng nhanh hơn và sẽ mở rộng tốt hơn khi có một bộ dữ liệu thực sự.
  • Hàm CLR sẽ có thể được sử dụng lại trong các truy vấn khác và bạn sẽ không phải sao chép (và gỡ lỗi) một truy vấn con phức tạp mỗi khi bạn cần thực hiện loại điều này.
  • Straight T-SQL đơn giản hơn viết và quản lý một đoạn mã bên ngoài.
  • Có lẽ bạn không biết cách lập trình trong C # hoặc VB.
  • Vân vân.

EDIT: Chà, tôi đã cố gắng xem liệu điều này có thực sự tốt hơn không, và hóa ra yêu cầu rằng các bình luận theo một thứ tự cụ thể hiện không thể đáp ứng bằng cách sử dụng hàm tổng hợp. :(

Xem SqlUserDefinedAggregateAttribution.IsInvariantToOrder . Về cơ bản, những gì bạn cần làm là OVER(PARTITION BY customer_code ORDER BY row_num)nhưng ORDER BYkhông được hỗ trợ trong OVERmệnh đề khi tổng hợp. Tôi giả sử việc thêm chức năng này vào SQL Server sẽ mở ra một hộp giun, bởi vì những gì cần phải thay đổi trong kế hoạch thực hiện là không đáng kể. Liên kết nói trên cho biết điều này được dành riêng cho sử dụng trong tương lai, vì vậy điều này có thể được thực hiện trong tương lai (mặc dù vào năm 2005, bạn có thể không gặp may).

Điều này vẫn có thể được thực hiện bằng cách đóng gói và phân tích cú pháprow_num giá trị vào chuỗi tổng hợp, và sau đó thực hiện sắp xếp trong đối tượng CLR ... có vẻ khá hackish.

Trong mọi trường hợp, dưới đây là mã tôi đã sử dụng trong trường hợp bất kỳ ai khác thấy điều này hữu ích ngay cả với giới hạn. Tôi sẽ để lại phần hack như một bài tập cho người đọc. Lưu ý rằng tôi đã sử dụng AdventureWorks (2005) cho dữ liệu thử nghiệm.

Lắp ráp tổng hợp:

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 để kiểm tra ( CREATE ASSEMBLYsp_configuređể bật CLR bị bỏ qua):

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

Đây là một giải pháp dựa trên con trỏ đảm bảo thứ tự các bình luận bằng cách row_num. (Xem câu trả lời khác của tôi để biết cách [dbo].[Comments]bảng được điền.)

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
Bạn đã không tránh được một con trỏ. Thay vào đó, bạn chỉ gọi con trỏ của mình một vòng lặp while.
Aaron Bertrand
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.