Динамічний запит 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
можливий дублікат Write Advanced SQL Select
RichardTheKiwi

Відповіді:


250

Динамічний SQL PIVOT:

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. Якісь додаткові пом’якшувальні дії, які я міг би зробити, перш ніж об'єднати його та виконати?
Червоний горох

Як би ви сортували рядки та стовпці на цьому?
Патрік Шомбург

@PatrickSchomburg Існує безліч способів - якщо ви хочете сортувати, @colsто ви можете видалити DISTINCTта використати, GROUP BYі ORDER BYколи ви отримаєте список @cols.
Тарін

Я спробую це. А як щодо рядків? Я також використовую побачення, і воно не виходить в порядку.
Патрік Шомбург

1
Не маю на увазі, що я робив наказ не в тому місці.
Патрік Шомбург

27

Динамічний SQL PIVOT

Інший підхід для створення рядків стовпців

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

Я знаю, що це питання старше, але я шукав відповіді і думав, що мені вдасться розширити "динамічну" частину проблеми і, можливо, допомогти комусь.

Перш за все, я створив це рішення, щоб вирішити проблему, яку мали декілька колег з непостійними та великими наборами даних, які потребують швидкого повороту.

Це рішення вимагає створення збереженої процедури, тому якщо це не викликає сумнівів для ваших потреб, будь ласка, перестаньте читати зараз.

У цій процедурі будуть взяті ключові змінні оператора зведення, щоб динамічно створювати зведені оператори для різних таблиць, назв стовпців та агрегатів. Стовпець Статичний використовується як стовпець групи по / ідентичності для повороту (це може бути викреслено з коду, якщо не потрібно, але досить поширене у зведених висловлюваннях і було необхідне для вирішення початкової проблеми), стовпчастий стовпчик - це місце імена кінцевих результатів будуть створені з, а стовпець значення - це те, до чого буде застосовано сукупність. Параметр Таблиця - це назва таблиці, включаючи схему (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 замість збереженої процедури. Було б зручно вибрати з такого ТВФ.
Перемислав Ремін

3
На жаль, ні, наскільки мені відомо, тому що ти не можеш мати динамічну структуру для TVF. Ви повинні мати статичний набір стовпців у TVF.
SFrejofsky

8

Оновлена ​​версія для SQL Server 2017 за допомогою функції STRING_AGG для побудови списку стовпців:

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):

Стрижні з динамічними стовпцями у SQL Server 2005

SQL Server - динамічна таблиця PIVOT - ін'єкція SQL

Обов’язкове посилання на прокляття та благословення динамічного SQL


11
FWIW QUOTENAMEдопомагає атакам ін'єкцій SQL лише у тому випадку, якщо ви приймаєте @tableName як параметр від користувача та додаєте його до подібного запиту SET @sql = 'SELECT * FROM ' + @tableName;. Ви можете створити безліч вразливих динамічних рядків SQL і QUOTENAMEне будете лизати, щоб допомогти вам.
Аарон Бертран

2
@davids Будь ласка, зверніться до цієї мета-дискусії . Якщо ви видалите гіперпосилання, ваша відповідь неповна.
Керміт

@ Керміт, я погоджуюся, що показ коду корисніше, але ти кажеш, що він потрібен для того, щоб він був відповіддю? Без посилань, моя відповідь - "Ви можете досягти цього за допомогою динамічного 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

Наведений нижче код надає результати, які замінюють NULL на нуль у висновку.

Створення таблиці та вставка даних:

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

ВИХІД:

введіть тут опис зображення

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.