SQL Server dynamic PIVOT query?

203

Saya telah diberi tugas untuk menerjemahkan data berikut:

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

menjadi sebagai berikut:

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

Bintik kosong dapat berupa NULL atau kosong, baik-baik saja, dan kategorinya harus dinamis. Peringatan lain yang mungkin untuk ini adalah bahwa kami akan menjalankan kueri dalam kapasitas terbatas, yang berarti tabel temp tidak tersedia. Saya sudah mencoba untuk meneliti dan telah mendarat PIVOTtetapi karena saya belum pernah menggunakannya sebelumnya saya benar-benar tidak memahaminya, meskipun upaya terbaik saya untuk mengetahuinya. Adakah yang bisa mengarahkan saya ke arah yang benar?

Sean Cunningham
sumber
3
Tolong apa versi SQL Server?
Aaron Bertrand
1
kemungkinan rangkap dari Tulis lanjutan SQL Select
RichardTheKiwi

Jawaban:

251

PIVOT SQL dinamis:

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

Hasil:

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
Taryn
sumber
Jadi \ @cols harus dipatenkan, bukan? Kami tidak dapat menggunakan sp_executesql dan parameter-binding untuk menginterpolasi \ @cols di sana? Meskipun kita membangun \ @cols sendiri, bagaimana jika entah bagaimana itu mengandung SQL berbahaya. Adakah langkah mitigasi tambahan yang bisa saya ambil sebelum menyatukan dan melaksanakannya?
Kacang Merah
Bagaimana Anda mengurutkan baris dan kolom pada ini?
Patrick Schomburg
@ PatrickSchomburg Ada berbagai cara - jika Anda ingin menyortirnya @colsmaka Anda dapat menghapus DISTINCTdan menggunakan GROUP BYdan ORDER BYketika Anda mendapatkan daftar @cols.
Taryn
Saya akan mencobanya. Bagaimana dengan barisnya? Saya menggunakan kencan juga, dan itu tidak keluar secara berurutan.
Patrick Schomburg
1
Nevermind saya menempatkan pesanan di tempat yang salah.
Patrick Schomburg
27

PIVOT SQL dinamis

Pendekatan berbeda untuk membuat string kolom

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

Hasil

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
mkdave99
sumber
13

Saya tahu pertanyaan ini lebih lama tetapi saya mencari jawaban dan berpikir bahwa saya mungkin dapat memperluas bagian "dinamis" dari masalah dan mungkin membantu seseorang.

Pertama dan terpenting saya membangun solusi ini untuk memecahkan masalah yang dialami oleh beberapa rekan kerja dengan set data yang tidak konstan dan besar yang perlu diputar cepat.

Solusi ini memerlukan pembuatan prosedur tersimpan sehingga jika itu tidak sesuai dengan kebutuhan Anda, silakan berhenti membaca sekarang.

Prosedur ini akan mengambil variabel kunci dari pernyataan pivot untuk secara dinamis membuat pernyataan pivot untuk berbagai tabel, nama kolom, dan agregat. Kolom statis digunakan sebagai grup oleh / kolom identitas untuk pivot (ini dapat dihapus dari kode jika tidak diperlukan tetapi cukup umum dalam pernyataan pivot dan diperlukan untuk menyelesaikan masalah asli), kolom pivot adalah tempat nama kolom hasil akhir akan dihasilkan dari, dan kolom nilai adalah apa yang akan diterapkan pada agregat. Parameter Tabel adalah nama tabel termasuk skema (schema.tablename), bagian kode ini dapat menggunakan beberapa cinta karena tidak sebersih yang saya inginkan. Ini bekerja untuk saya karena penggunaan saya tidak menghadap ke publik dan injeksi sql bukan masalah.

Mari kita mulai dengan kode untuk membuat prosedur tersimpan. Kode ini harus bekerja di semua versi SSMS 2005 dan di atas tetapi saya belum mengujinya pada 2005 atau 2016 tetapi saya tidak bisa melihat mengapa itu tidak berhasil.

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

Selanjutnya kita akan mendapatkan data kita siap untuk contoh. Saya telah mengambil contoh data dari jawaban yang diterima dengan penambahan beberapa elemen data untuk digunakan dalam bukti konsep ini untuk menunjukkan hasil yang bervariasi dari perubahan agregat.

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)

Contoh-contoh berikut menunjukkan pernyataan eksekusi bervariasi yang menunjukkan agregat bervariasi sebagai contoh sederhana. Saya tidak memilih untuk mengubah kolom statis, pivot, dan nilai untuk menjaga contoh sederhana. Anda dapat menyalin dan menempelkan kode untuk mulai mengacaukannya sendiri

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'

Eksekusi ini mengembalikan set data berikut masing-masing.

masukkan deskripsi gambar di sini

SFrejofsky
sumber
Kerja bagus! Bisakah Anda membuat pilihan TVF daripada prosedur tersimpan. Akan lebih mudah untuk memilih dari TVF tersebut.
Przemyslaw Remin
3
Sayangnya tidak, sejauh yang saya ketahui, karena Anda tidak dapat memiliki struktur dinamis untuk TVF. Anda harus memiliki kumpulan kolom statis di TVF.
SFrejofsky
8

Versi terbaru untuk SQL Server 2017 menggunakan fungsi STRING_AGG untuk membuat daftar kolom 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;
nvogel
sumber
6

Anda dapat mencapai ini menggunakan TSQL dinamis (ingat untuk menggunakan QUOTENAME untuk menghindari serangan injeksi SQL):

Pivot dengan Kolom Dinamis di SQL Server 2005

SQL Server - Tabel PIVOT Dinamis - SQL Injection

Referensi wajib untuk Kutukan dan Berkat dari Dynamic SQL

Davids
sumber
11
FWIW QUOTENAMEhanya membantu serangan injeksi SQL jika Anda menerima @tableName sebagai parameter dari pengguna, dan menambahkannya ke kueri seperti SET @sql = 'SELECT * FROM ' + @tableName;. Anda dapat membangun banyak string SQL dinamis yang rentan dan QUOTENAMEtidak akan melakukan apa pun untuk membantu Anda.
Aaron Bertrand
2
@davids Silakan merujuk ke diskusi meta ini . Jika Anda menghapus hyperlink, jawaban Anda tidak lengkap.
Kermit
@Ermit, saya setuju bahwa menunjukkan kode lebih bermanfaat, tetapi apakah Anda mengatakan itu diperlukan agar itu menjadi jawaban? Tanpa tautan, balasan saya adalah "Anda dapat mencapai ini menggunakan TSQL dinamis". Jawaban yang dipilih menunjukkan rute yang sama, dengan manfaat tambahan jika juga menunjukkan cara melakukannya, itulah sebabnya dipilih sebagai jawabannya.
David
2
Saya memilih ulang jawaban yang dipilih (sebelum dipilih) karena memiliki contoh dan akan lebih membantu seseorang yang baru. Namun, saya pikir seseorang yang baru juga harus membaca tautan yang saya berikan, itulah sebabnya saya tidak menghapusnya.
David
3

Ada solusi saya membersihkan nilai nol yang tidak perlu

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);
m0rg4n
sumber
2

Kode di bawah ini memberikan hasil yang menggantikan NULL ke nol dalam output.

Pembuatan tabel dan penyisipan data:

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)

Kueri untuk menghasilkan hasil yang tepat yang juga menggantikan NULL dengan nol:

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

OUTPUT:

masukkan deskripsi gambar di sini

Arockia Nirmal
sumber