แบบสอบถาม PIVOT แบบไดนามิกของ SQL Server


202

ฉันได้รับมอบหมายให้คิดวิธีแปลข้อมูลต่อไปนี้:

date        category        amount
1/1/2012    ABC             1000.00
2/1/2012    DEF             500.00
2/1/2012    GHI             800.00
2/10/2012   DEF             700.00
3/1/2012    ABC             1100.00

ลงในต่อไปนี้:

date        ABC             DEF             GHI
1/1/2012    1000.00
2/1/2012                    500.00
2/1/2012                                    800.00
2/10/2012                   700.00
3/1/2012    1100.00

จุดว่างสามารถเป็น NULL หรือช่องว่างก็ได้และหมวดหมู่จะต้องเป็นแบบไดนามิก ข้อแม้ที่เป็นไปได้อีกประการหนึ่งสำหรับเรื่องนี้คือเราจะเรียกใช้แบบสอบถามในความจุที่ จำกัด ซึ่งหมายความว่าตารางชั่วคราวหมด ฉันพยายามค้นคว้าและลงจอดPIVOTแต่เนื่องจากฉันไม่เคยใช้มันมาก่อนที่ฉันจะไม่เข้าใจจริงๆแม้ว่าฉันจะพยายามหาวิธีที่ดีที่สุดก็ตาม ใครช่วยชี้ฉันในทิศทางที่ถูกต้องได้ไหม


3
กรุณาใช้ SQL Server รุ่นใด
แอรอนเบอร์ทรานด์ด์

1
อาจเป็นไปได้ซ้ำกับการเขียนเลือก SQL ขั้นสูง
RichardTheKiwi

คำตอบ:


250

PIVOT SQL แบบไดนามิก:

create table temp
(
    date datetime,
    category varchar(3),
    amount money
)

insert into temp values ('1/1/2012', 'ABC', 1000.00)
insert into temp values ('2/1/2012', 'DEF', 500.00)
insert into temp values ('2/1/2012', 'GHI', 800.00)
insert into temp values ('2/10/2012', 'DEF', 700.00)
insert into temp values ('3/1/2012', 'ABC', 1100.00)


DECLARE @cols AS NVARCHAR(MAX),
    @query  AS NVARCHAR(MAX);

SET @cols = STUFF((SELECT distinct ',' + QUOTENAME(c.category) 
            FROM temp c
            FOR XML PATH(''), TYPE
            ).value('.', 'NVARCHAR(MAX)') 
        ,1,1,'')

set @query = 'SELECT date, ' + @cols + ' from 
            (
                select date
                    , amount
                    , category
                from temp
           ) x
            pivot 
            (
                 max(amount)
                for category in (' + @cols + ')
            ) p '


execute(@query)

drop table temp

ผล:

Date                        ABC         DEF    GHI
2012-01-01 00:00:00.000     1000.00     NULL    NULL
2012-02-01 00:00:00.000     NULL        500.00  800.00
2012-02-10 00:00:00.000     NULL        700.00  NULL
2012-03-01 00:00:00.000     1100.00     NULL    NULL

ดังนั้น \ @cols จะต้องต่อกันเป็นสตริงใช่ไหม เราไม่สามารถใช้ sp_executesql และการผูกพารามิเตอร์เพื่อแทรก \ @cols ในนั้นได้หรือไม่ แม้ว่าเราจะสร้าง \ @cols เราเองจะเกิดอะไรขึ้นถ้ามันมี SQL ที่เป็นอันตรายอยู่ มีขั้นตอนการบรรเทาอื่นใดบ้างที่ฉันสามารถทำได้ก่อนทำการเชื่อมต่อและเรียกใช้งานได้?
ถั่วแดง

คุณจะเรียงลำดับแถวและคอลัมน์ในสิ่งนี้ได้อย่างไร
Patrick Schomburg

@PatrickSchomburg มีความหลากหลายของวิธี - ถ้าคุณต้องการที่จะเรียงลำดับ@colsแล้วคุณสามารถลบDISTINCTและการใช้งานGROUP BYและเมื่อคุณได้รับรายชื่อของORDER BY @cols
Taryn

ฉันจะลองดู แล้วแถวล่ะ? ฉันใช้วันที่เช่นกันและมันไม่ออกมาตามลำดับ
Patrick Schomburg

1
ไม่เป็นไรฉันถูกสั่งโดยผิดจุด
Patrick Schomburg

27

PIVOT SQL แบบไดนามิก

วิธีการต่าง ๆ ในการสร้างสตริงคอลัมน์

create table #temp
(
    date datetime,
    category varchar(3),
    amount money
)

insert into #temp values ('1/1/2012', 'ABC', 1000.00)
insert into #temp values ('2/1/2012', 'DEF', 500.00)
insert into #temp values ('2/1/2012', 'GHI', 800.00)
insert into #temp values ('2/10/2012', 'DEF', 700.00)
insert into #temp values ('3/1/2012', 'ABC', 1100.00)

DECLARE @cols  AS NVARCHAR(MAX)='';
DECLARE @query AS NVARCHAR(MAX)='';

SELECT @cols = @cols + QUOTENAME(category) + ',' FROM (select distinct category from #temp ) as tmp
select @cols = substring(@cols, 0, len(@cols)) --trim "," at end

set @query = 
'SELECT * from 
(
    select date, amount, category from #temp
) src
pivot 
(
    max(amount) for category in (' + @cols + ')
) piv'

execute(@query)
drop table #temp

ผลลัพธ์

date                    ABC     DEF     GHI
2012-01-01 00:00:00.000 1000.00 NULL    NULL
2012-02-01 00:00:00.000 NULL    500.00  800.00
2012-02-10 00:00:00.000 NULL    700.00  NULL
2012-03-01 00:00:00.000 1100.00 NULL    NULL

13

ฉันรู้ว่าคำถามนี้เก่ากว่า แต่ฉันกำลังมองหาคำตอบและคิดว่าฉันอาจจะสามารถขยายในส่วนของ "ไดนามิก" ของปัญหาและอาจช่วยให้ใครบางคนออกมา

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

โซลูชันนี้ต้องการการสร้างโพรซีเดอร์ที่จัดเก็บไว้ดังนั้นหากไม่เป็นไปตามความต้องการของคุณโปรดหยุดอ่านเดี๋ยวนี้

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

ให้เริ่มต้นด้วยรหัสเพื่อสร้างกระบวนงานที่เก็บไว้ รหัสนี้ควรใช้ได้กับ SSMS 2005 ทุกรุ่นขึ้นไป แต่ฉันยังไม่ได้ทดสอบในปี 2005 หรือ 2016 แต่ฉันไม่สามารถดูได้ว่าทำไมมันถึงไม่ทำงาน

create PROCEDURE [dbo].[USP_DYNAMIC_PIVOT]
    (
        @STATIC_COLUMN VARCHAR(255),
        @PIVOT_COLUMN VARCHAR(255),
        @VALUE_COLUMN VARCHAR(255),
        @TABLE VARCHAR(255),
        @AGGREGATE VARCHAR(20) = null
    )

AS


BEGIN

SET NOCOUNT ON;
declare @AVAIABLE_TO_PIVOT NVARCHAR(MAX),
        @SQLSTRING NVARCHAR(MAX),
        @PIVOT_SQL_STRING NVARCHAR(MAX),
        @TEMPVARCOLUMNS NVARCHAR(MAX),
        @TABLESQL NVARCHAR(MAX)

if isnull(@AGGREGATE,'') = '' 
    begin
        SET @AGGREGATE = 'MAX'
    end


 SET @PIVOT_SQL_STRING =    'SELECT top 1 STUFF((SELECT distinct '', '' + CAST(''[''+CONVERT(VARCHAR,'+ @PIVOT_COLUMN+')+'']''  AS VARCHAR(50)) [text()]
                            FROM '+@TABLE+'
                            WHERE ISNULL('+@PIVOT_COLUMN+','''') <> ''''
                            FOR XML PATH(''''), TYPE)
                            .value(''.'',''NVARCHAR(MAX)''),1,2,'' '') as PIVOT_VALUES
                            from '+@TABLE+' ma
                            ORDER BY ' + @PIVOT_COLUMN + ''

declare @TAB AS TABLE(COL NVARCHAR(MAX) )

INSERT INTO @TAB EXEC SP_EXECUTESQL  @PIVOT_SQL_STRING, @AVAIABLE_TO_PIVOT 

SET @AVAIABLE_TO_PIVOT = (SELECT * FROM @TAB)


SET @TEMPVARCOLUMNS = (SELECT replace(@AVAIABLE_TO_PIVOT,',',' nvarchar(255) null,') + ' nvarchar(255) null')


SET @SQLSTRING = 'DECLARE @RETURN_TABLE TABLE ('+@STATIC_COLUMN+' NVARCHAR(255) NULL,'+@TEMPVARCOLUMNS+')  
                    INSERT INTO @RETURN_TABLE('+@STATIC_COLUMN+','+@AVAIABLE_TO_PIVOT+')

                    select * from (
                    SELECT ' + @STATIC_COLUMN + ' , ' + @PIVOT_COLUMN + ', ' + @VALUE_COLUMN + ' FROM '+@TABLE+' ) a

                    PIVOT
                    (
                    '+@AGGREGATE+'('+@VALUE_COLUMN+')
                    FOR '+@PIVOT_COLUMN+' IN ('+@AVAIABLE_TO_PIVOT+')
                    ) piv

                    SELECT * FROM @RETURN_TABLE'



EXEC SP_EXECUTESQL @SQLSTRING

END

ต่อไปเราจะทำให้ข้อมูลพร้อมสำหรับตัวอย่าง ฉันได้นำตัวอย่างข้อมูลจากคำตอบที่ยอมรับพร้อมกับการเพิ่มองค์ประกอบข้อมูลสองสามอย่างเพื่อใช้ในการพิสูจน์แนวคิดนี้เพื่อแสดงผลลัพธ์ที่หลากหลายของการเปลี่ยนแปลงโดยรวม

create table temp
(
    date datetime,
    category varchar(3),
    amount money
)

insert into temp values ('1/1/2012', 'ABC', 1000.00)
insert into temp values ('1/1/2012', 'ABC', 2000.00) -- added
insert into temp values ('2/1/2012', 'DEF', 500.00)
insert into temp values ('2/1/2012', 'DEF', 1500.00) -- added
insert into temp values ('2/1/2012', 'GHI', 800.00)
insert into temp values ('2/10/2012', 'DEF', 700.00)
insert into temp values ('2/10/2012', 'DEF', 800.00) -- addded
insert into temp values ('3/1/2012', 'ABC', 1100.00)

ตัวอย่างต่อไปนี้แสดงคำสั่งการดำเนินการที่หลากหลายซึ่งแสดงการรวมที่หลากหลายเป็นตัวอย่างง่าย ๆ ฉันไม่ได้เลือกที่จะเปลี่ยนคอลัมน์สแตติกเดือยและค่าเพื่อให้ตัวอย่างง่าย คุณควรจะสามารถคัดลอกและวางรหัสเพื่อเริ่มต้นทำมันด้วยตัวคุณเอง

exec [dbo].[USP_DYNAMIC_PIVOT] 'date','category','amount','dbo.temp','sum'
exec [dbo].[USP_DYNAMIC_PIVOT] 'date','category','amount','dbo.temp','max'
exec [dbo].[USP_DYNAMIC_PIVOT] 'date','category','amount','dbo.temp','avg'
exec [dbo].[USP_DYNAMIC_PIVOT] 'date','category','amount','dbo.temp','min'

การดำเนินการนี้จะส่งคืนชุดข้อมูลต่อไปนี้ตามลำดับ

ป้อนคำอธิบายรูปภาพที่นี่


เยี่ยมมาก! คุณช่วยเลือก TVF แทนวิธีการที่เก็บไว้ได้ไหม จะสะดวกในการเลือกจาก TVF เช่นนี้
Przemyslaw Remin

3
น่าเสียดายที่ไม่ใช่เพื่อความรู้ของฉันที่ดีที่สุดเพราะคุณไม่สามารถมีโครงสร้างแบบไดนามิกสำหรับ TVF คุณต้องมีชุดคอลัมน์คงที่ใน TVF
SFrejofsky

8

อัปเดตสำหรับ SQL Server 2017 โดยใช้ฟังก์ชัน STRING_AGG เพื่อสร้างรายการคอลัมน์ pivot:

create table temp
(
    date datetime,
    category varchar(3),
    amount money
);

insert into temp values ('20120101', 'ABC', 1000.00);
insert into temp values ('20120201', 'DEF', 500.00);
insert into temp values ('20120201', 'GHI', 800.00);
insert into temp values ('20120210', 'DEF', 700.00);
insert into temp values ('20120301', 'ABC', 1100.00);


DECLARE @cols AS NVARCHAR(MAX),
    @query  AS NVARCHAR(MAX);

SET @cols = (SELECT STRING_AGG(category,',') FROM (SELECT DISTINCT category FROM temp WHERE category IS NOT NULL)t);

set @query = 'SELECT date, ' + @cols + ' from 
            (
                select date
                    , amount
                    , category
                from temp
           ) x
            pivot 
            (
                 max(amount)
                for category in (' + @cols + ')
            ) p ';

execute(@query);

drop table temp;

6

คุณสามารถทำได้โดยใช้ TSQL แบบไดนามิก (อย่าลืมใช้ QUOTENAME เพื่อหลีกเลี่ยงการโจมตีของ SQL injection):

Pivots ด้วยคอลัมน์แบบไดนามิกใน SQL Server 2005

SQL Server - Dynamic PIVOT Table - การฉีด SQL

ภาระหน้าที่อ้างอิงถึงThe Curse and Blessings ของ Dynamic SQL


11
FWIW QUOTENAMEเพียง แต่จะช่วยโจมตีฉีด SQL ถ้าคุณยอมรับ @tableName SET @sql = 'SELECT * FROM ' + @tableName;เป็นพารามิเตอร์จากผู้ใช้และท้ายมันแบบสอบถามเหมือน คุณสามารถสร้างสตริง SQL แบบไดนามิกที่มีช่องโหว่ได้และQUOTENAMEจะไม่ช่วยให้คุณทำ
แอรอนเบอร์ทรานด์ด์

2
@davids โปรดอ้างถึงการสนทนาเมตานี้ หากคุณลบไฮเปอร์ลิงก์คำตอบของคุณไม่สมบูรณ์
มิต

@ Kermit ฉันเห็นด้วยว่าการแสดงรหัสมีประโยชน์มากกว่า แต่คุณกำลังบอกว่าจำเป็นต้องมีคำตอบหรือไม่ หากไม่มีลิงก์คำตอบของฉันคือ "คุณสามารถทำได้โดยใช้ TSQL แบบไดนามิก" คำตอบที่เลือกจะแนะนำเส้นทางเดียวกันพร้อมกับสิทธิประโยชน์เพิ่มเติมหากแสดงวิธีดำเนินการด้วยเช่นกันซึ่งเป็นสาเหตุที่เลือกเป็นคำตอบ
davids

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

3

มีวิธีแก้ปัญหาของฉันล้างค่าว่างที่ไม่จำเป็น

DECLARE @cols AS NVARCHAR(MAX),
@maxcols AS NVARCHAR(MAX),
@query  AS NVARCHAR(MAX)

select @cols = STUFF((SELECT ',' + QUOTENAME(CodigoFormaPago) 
                from PO_FormasPago
                order by CodigoFormaPago
        FOR XML PATH(''), TYPE
        ).value('.', 'NVARCHAR(MAX)') 
    ,1,1,'')

select @maxcols = STUFF((SELECT ',MAX(' + QUOTENAME(CodigoFormaPago) + ') as ' + QUOTENAME(CodigoFormaPago)
                from PO_FormasPago
                order by CodigoFormaPago
        FOR XML PATH(''), TYPE
        ).value('.', 'NVARCHAR(MAX)')
    ,1,1,'')

set @query = 'SELECT CodigoProducto, DenominacionProducto, ' + @maxcols + '
            FROM
            (
                SELECT 
                CodigoProducto, DenominacionProducto,
                ' + @cols + ' from 
                 (
                    SELECT 
                        p.CodigoProducto as CodigoProducto,
                        p.DenominacionProducto as DenominacionProducto,
                        fpp.CantidadCuotas as CantidadCuotas,
                        fpp.IdFormaPago as IdFormaPago,
                        fp.CodigoFormaPago as CodigoFormaPago
                    FROM
                        PR_Producto p
                        LEFT JOIN PR_FormasPagoProducto fpp
                            ON fpp.IdProducto = p.IdProducto
                        LEFT JOIN PO_FormasPago fp
                            ON fpp.IdFormaPago = fp.IdFormaPago
                ) xp
                pivot 
                (
                    MAX(CantidadCuotas)
                    for CodigoFormaPago in (' + @cols + ')
                ) p 
            )  xx 
            GROUP BY CodigoProducto, DenominacionProducto'

t @query;

execute(@query);

2

รหัสด้านล่างแสดงผลซึ่งแทนที่โมฆะเพื่อเป็นศูนย์ในการส่งออก

การสร้างตารางและการแทรกข้อมูล:

create table test_table
 (
 date nvarchar(10),
 category char(3),
 amount money
 )

 insert into test_table values ('1/1/2012','ABC',1000.00)
 insert into test_table values ('2/1/2012','DEF',500.00)
 insert into test_table values ('2/1/2012','GHI',800.00)
 insert into test_table values ('2/10/2012','DEF',700.00)
 insert into test_table values ('3/1/2012','ABC',1100.00)

แบบสอบถามเพื่อสร้างผลลัพธ์ที่แน่นอนซึ่งยังแทนที่ NULL ด้วยศูนย์:

DECLARE @DynamicPivotQuery AS NVARCHAR(MAX),
@PivotColumnNames AS NVARCHAR(MAX),
@PivotSelectColumnNames AS NVARCHAR(MAX)

--Get distinct values of the PIVOT Column
SELECT @PivotColumnNames= ISNULL(@PivotColumnNames + ',','')
+ QUOTENAME(category)
FROM (SELECT DISTINCT category FROM test_table) AS cat

--Get distinct values of the PIVOT Column with isnull
SELECT @PivotSelectColumnNames 
= ISNULL(@PivotSelectColumnNames + ',','')
+ 'ISNULL(' + QUOTENAME(category) + ', 0) AS '
+ QUOTENAME(category)
FROM (SELECT DISTINCT category FROM test_table) AS cat

--Prepare the PIVOT query using the dynamic 
SET @DynamicPivotQuery = 
N'SELECT date, ' + @PivotSelectColumnNames + '
FROM test_table
pivot(sum(amount) for category in (' + @PivotColumnNames + ')) as pvt';

--Execute the Dynamic Pivot Query
EXEC sp_executesql @DynamicPivotQuery

เอาท์พุท:

ป้อนคำอธิบายรูปภาพที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.