Consulta dinámica de SQL Server PIVOT?

203

Se me ha encomendado la tarea de encontrar un medio para traducir los siguientes datos:

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

en lo siguiente:

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

Los espacios en blanco pueden ser NULL o espacios en blanco, o bien está bien, y las categorías deberían ser dinámicas. Otra posible advertencia a esto es que ejecutaremos la consulta en una capacidad limitada, lo que significa que las tablas temporales están fuera. He intentado investigar y he llegado a tierra, PIVOTpero como nunca lo he usado antes, realmente no lo entiendo, a pesar de mis mejores esfuerzos para resolverlo. ¿Alguien puede señalarme en la dirección correcta?

Sean Cunningham
fuente
3
¿Qué versión de SQL Server por favor?
Aaron Bertrand
1
posible duplicado de Write Advanced SQL Select
RichardTheKiwi

Respuestas:

251

SQL PIVOT dinámico:

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

Resultados:

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
fuente
Entonces, \ @cols debe estar concatenado con cadenas, ¿verdad? ¿No podemos usar sp_executesql y el enlace de parámetros para interpolar \ @cols allí? Aunque construimos \ @cols nosotros mismos, ¿qué pasaría si de alguna manera contuviera SQL malicioso? ¿Algún paso de mitigación adicional que pueda tomar antes de concatenarlo y ejecutarlo?
The Red Pea
¿Cómo ordenarías las filas y columnas en esto?
Patrick Schomburg el
@PatrickSchomburg Hay una variedad de maneras - si desea ordenar la @colsentonces se podría eliminar el DISTINCTuso GROUP BYy ORDER BYcuando llegue a la lista de @cols.
Taryn
Lo intentaré ¿Qué hay de las filas? Estoy usando una fecha también, y no sale en orden.
Patrick Schomburg el
1
No importaba que estaba poniendo el pedido en el lugar equivocado.
Patrick Schomburg el
27

PIVOTE SQL dinámico

Enfoque diferente para crear cadenas de columnas

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

Resultado

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
fuente
13

Sé que esta pregunta es más antigua, pero estaba buscando las respuestas y pensé que podría ampliar la parte "dinámica" del problema y posiblemente ayudar a alguien.

En primer lugar, construí esta solución para resolver un problema que un par de compañeros de trabajo estaban teniendo con conjuntos de datos grandes e inconstantes que debían pivotar rápidamente.

Esta solución requiere la creación de un procedimiento almacenado, por lo que si eso no se ajusta a sus necesidades, deje de leer ahora.

Este procedimiento tomará las variables clave de una declaración dinámica para crear dinámicamente declaraciones dinámicas para diferentes tablas, nombres de columnas y agregados. La columna estática se usa como la columna de grupo por / identidad para el pivote (esto puede eliminarse del código si no es necesario, pero es bastante común en las declaraciones de pivote y fue necesario para resolver el problema original), la columna pivote es donde se generarán los nombres de columna resultantes finales, y la columna de valor es a lo que se aplicará el agregado. El parámetro Table es el nombre de la tabla, incluido el esquema (schema.tablename), esta parte del código podría usar algo de amor porque no es tan limpio como me gustaría que fuera. Funcionó para mí porque mi uso no era público y la inyección de sql no era una preocupación.

Comencemos con el código para crear el procedimiento almacenado. Este código debería funcionar en todas las versiones de SSMS 2005 y superiores, pero no lo he probado en 2005 o 2016, pero no puedo ver por qué no funcionaría.

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

A continuación, prepararemos nuestros datos para el ejemplo. Tomé el ejemplo de datos de la respuesta aceptada con la adición de un par de elementos de datos para usar en esta prueba de concepto para mostrar los resultados variados del cambio agregado.

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)

Los siguientes ejemplos muestran las declaraciones de ejecución variadas que muestran los agregados variados como un ejemplo simple. No opté por cambiar las columnas estáticas, dinámicas y de valor para mantener el ejemplo simple. Debería poder simplemente copiar y pegar el código para comenzar a jugarlo usted mismo

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'

Esta ejecución devuelve los siguientes conjuntos de datos respectivamente.

ingrese la descripción de la imagen aquí

SFrejofsky
fuente
¡Buen trabajo! ¿Puede hacer una opción de TVF en lugar del procedimiento almacenado? Sería conveniente seleccionar de tal TVF.
Przemyslaw Remin
3
Lamentablemente no, que yo sepa, porque no se puede tener una estructura dinámica para un TVF. Debe tener un conjunto estático de columnas en un TVF.
SFrejofsky
8

Versión actualizada para SQL Server 2017 usando la función STRING_AGG para construir la lista de columnas dinámicas:

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
fuente
6

Puede lograr esto usando TSQL dinámico (recuerde usar QUOTENAME para evitar ataques de inyección SQL):

Pivotes con columnas dinámicas en SQL Server 2005

SQL Server - Tabla dinámica PIVOT - Inyección SQL

Referencia obligatoria a The Curse and Blessings of Dynamic SQL

davids
fuente
11
FWIW QUOTENAMEsolo ayuda a los ataques de inyección SQL si acepta @tableName como parámetro de un usuario y lo agrega a una consulta como SET @sql = 'SELECT * FROM ' + @tableName;. Puede crear muchas cadenas SQL dinámicas vulnerables y QUOTENAMEno hará un esfuerzo para ayudarlo.
Aaron Bertrand
2
@davids Consulte esta meta discusión . Si elimina los hipervínculos, su respuesta está incompleta.
Kermit
@Kermit, estoy de acuerdo en que mostrar el código es más útil, pero ¿estás diciendo que es obligatorio para que sea una respuesta? Sin los enlaces, mi respuesta es "Puede lograr esto usando TSQL dinámico". La respuesta seleccionada sugiere la misma ruta, con el beneficio adicional de mostrar también cómo hacerlo, razón por la cual se seleccionó como respuesta.
David
2
He votado a favor la respuesta seleccionada (antes de ser seleccionada) porque tenía un ejemplo y ayudará mejor a alguien nuevo. Sin embargo, creo que alguien nuevo también debería leer los enlaces que proporcioné, por lo que no los eliminé.
davids
3

Ahí está mi solución limpiando los valores nulos innecesarios

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
fuente
2

El siguiente código proporciona los resultados que reemplazan NULL a cero en la salida.

Creación de tablas e inserción de datos:

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)

Consulta para generar los resultados exactos que también reemplazan NULL con ceros:

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

SALIDA

ingrese la descripción de la imagen aquí

Arockia Nirmal
fuente