รวมคอลัมน์จากหลายแถวเป็นแถวเดียว


14

ฉันได้customer_commentsแบ่งออกเป็นหลายแถวเนื่องจากการออกแบบฐานข้อมูลและสำหรับรายงานที่ฉันต้องรวมcommentsจากแต่ละที่ไม่ซ้ำกันidเป็นหนึ่งแถว ก่อนหน้านี้ฉันเคยลองบางสิ่งบางอย่างที่ทำงานกับรายการที่มีการคั่นจาก SELECT clause และเคล็ดลับ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 กำลังก่อตัวขึ้นจริงๆ!
Ben Brocka

@ NickChammas - ฉันจะเอาคอของฉันออกไปและบอกว่าคำสั่งนั้นรับรองโดยใช้order byในแบบสอบถามย่อย นี่คือการสร้าง XML ใช้for xmlและที่เป็นวิธีการสร้าง XML ใช้ TSQL ลำดับขององค์ประกอบในไฟล์ XML เป็นเรื่องสำคัญและสามารถไว้วางใจได้ ดังนั้นหากเทคนิคนี้ไม่รับประกันการสั่งซื้อดังนั้นการสนับสนุน XML ใน TSQL จะเสียหายอย่างรุนแรง
Mikael Eriksson

2
ฉันได้ตรวจสอบแล้วว่าแบบสอบถามจะส่งคืนผลลัพธ์ในลำดับที่ถูกต้องโดยไม่คำนึงถึงดัชนีคลัสเตอร์บนตารางพื้นฐาน (แม้แต่ดัชนีคลัสเตอร์ที่row_num descต้องปฏิบัติตามคำorder byแนะนำของ Mikael) ฉันจะลบความคิดเห็นที่แนะนำมิฉะนั้นตอนนี้แบบสอบถามมีสิทธิ์order byและหวังว่า @JonSeigel พิจารณาว่าทำแบบเดียวกัน
Aaron Bertrand

6

หากคุณได้รับอนุญาตให้ใช้ CLR ในสภาพแวดล้อมของคุณนี่เป็นกรณีเฉพาะสำหรับการรวมที่ผู้ใช้กำหนด

โดยเฉพาะอย่างยิ่งนี่อาจเป็นวิธีที่จะไปหากแหล่งข้อมูลมีขนาดใหญ่และ / หรือคุณต้องทำสิ่งนี้มากในแอปพลิเคชันของคุณ ฉันสงสัยอย่างยิ่งว่าแผนคิวรีสำหรับโซลูชันของแอรอนจะไม่ขยายขนาดเมื่อขนาดอินพุตเพิ่มขึ้น (ฉันพยายามเพิ่มดัชนีไปยังตารางชั่วคราว แต่นั่นก็ไม่ได้ช่วยอะไร)

วิธีการแก้ปัญหานี้เหมือนกับสิ่งอื่น ๆ เป็นข้อเสีย:

  • การเมือง / นโยบายสำหรับการใช้ CLR Integration ในสภาพแวดล้อมของคุณหรือของลูกค้า
  • ฟังก์ชั่น CLR นั้นมีแนวโน้มที่จะเร็วกว่าและจะเพิ่มขนาดให้ดีขึ้นเมื่อได้รับชุดข้อมูลจริง
  • ฟังก์ชัน CLR จะสามารถนำมาใช้ซ้ำได้ในการค้นหาอื่น ๆ และคุณจะไม่ต้องทำซ้ำ (และดีบัก) แบบสอบถามย่อยที่ซับซ้อนทุกครั้งที่คุณต้องทำสิ่งนี้
  • Straight T-SQL นั้นง่ายกว่าการเขียนและจัดการโค้ดภายนอก
  • บางทีคุณอาจไม่รู้วิธีเขียนโปรแกรมใน C # หรือ VB
  • เป็นต้น

แก้ไข:ดีฉันไปลองดูว่าจริง ๆ แล้วดีกว่าและปรากฎว่าข้อกำหนดที่ข้อคิดเห็นอยู่ในลำดับที่เฉพาะเจาะจงในปัจจุบันไม่สามารถตอบสนองการใช้ฟังก์ชันรวมได้ :(

ดูSqlUserDefinedAggregateAttribute.IsInvariantToOrder โดยทั่วไปสิ่งที่คุณต้องทำคือOVER(PARTITION BY customer_code ORDER BY row_num)แต่ORDER BYไม่ได้รับการสนับสนุนในOVERข้อเมื่อรวม ฉันสมมติว่าการเพิ่มฟังก์ชันนี้ใน SQL Server เปิดกระป๋องเวิร์มเพราะสิ่งที่จะต้องมีการเปลี่ยนแปลงในแผนการดำเนินการเป็นเรื่องเล็กน้อย ลิงก์ข้างต้นกล่าวว่าสิ่งนี้สงวนไว้สำหรับการใช้งานในอนาคตดังนั้นสิ่งนี้อาจถูกนำไปใช้ในอนาคต (ในปี 2548 คุณอาจโชคไม่ดี)

สิ่งนี้สามารถทำได้โดยการบรรจุและแยก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
คุณไม่ได้หลีกเลี่ยงเคอร์เซอร์ คุณเพิ่งเรียกเคอร์เซอร์ของคุณมาสักพักหนึ่งแทน
Aaron Bertrand
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.