Convierta filas a columnas usando 'Pivot' en SQL Server

279

He leído las cosas en las tablas dinámicas de MS y todavía tengo problemas para hacer esto correctamente.

Tengo una tabla temporal que se está creando, diremos que la columna 1 es un número de Tienda, y la columna 2 es un número de semana y, por último, la columna 3 es un total de algún tipo. Además, los números de la semana son dinámicos, los números de la tienda son estáticos.

Store      Week     xCount
-------    ----     ------
102        1        96
101        1        138
105        1        37
109        1        59
101        2        282
102        2        212
105        2        78
109        2        97
105        3        60
102        3        123
101        3        220
109        3        87

Me gustaría que salga como una tabla dinámica, así:

Store        1          2          3        4        5        6....
----- 
101        138        282        220
102         96        212        123
105         37        
109

Almacene números al costado y semanas en la parte superior.

Lynn
fuente
1
posible duplicado de la consulta PIVOT dinámica
RichardTheKiwi

Respuestas:

356

Si está utilizando SQL Server 2005+, puede usar la PIVOTfunción para transformar los datos de filas en columnas.

Parece que necesitará usar sql dinámico si las semanas son desconocidas, pero es más fácil ver el código correcto usando una versión codificada inicialmente.

Primero, aquí hay algunas definiciones de tablas rápidas y datos para su uso:

CREATE TABLE #yt 
(
  [Store] int, 
  [Week] int, 
  [xCount] int
);

INSERT INTO #yt
(
  [Store], 
  [Week], [xCount]
)
VALUES
    (102, 1, 96),
    (101, 1, 138),
    (105, 1, 37),
    (109, 1, 59),
    (101, 2, 282),
    (102, 2, 212),
    (105, 2, 78),
    (109, 2, 97),
    (105, 3, 60),
    (102, 3, 123),
    (101, 3, 220),
    (109, 3, 87);

Si conoce sus valores, codificará la consulta:

select *
from 
(
  select store, week, xCount
  from yt
) src
pivot
(
  sum(xcount)
  for week in ([1], [2], [3])
) piv;

Ver demostración de SQL

Entonces, si necesita generar el número de semana dinámicamente, su código será:

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

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

set @query = 'SELECT store,' + @cols + ' from 
             (
                select store, week, xCount
                from yt
            ) x
            pivot 
            (
                sum(xCount)
                for week in (' + @cols + ')
            ) p '

execute(@query);

Ver demostración de SQL .

La versión dinámica, genera la lista de week números que deben convertirse en columnas. Ambos dan el mismo resultado:

| STORE |   1 |   2 |   3 |
---------------------------
|   101 | 138 | 282 | 220 |
|   102 |  96 | 212 | 123 |
|   105 |  37 |  78 |  60 |
|   109 |  59 |  97 |  87 |
Taryn
fuente
44
¡Muy agradable! Pero, ¿cómo eliminar la columna cuando todos los valores de esa columna son NULL?
ZooZ
1
@ZooZ Vea la respuesta a continuación . No lo he probado literalmente, pero el concepto es sólido.
ruffin
1
+1 "Parece que necesitará usar sql dinámico si las semanas son desconocidas, pero es más fácil ver el código correcto usando una versión con código inicial inicialmente". A diferencia de la función genérica Qlikview ( community.qlik.com/blogs/qlikviewdesignblog/2014/03/31/generic ) que permite no requiere que explícitamente se nombre el diferente "FOR ____ IN (...)"
The Red Pea
1
Si está creando una tabla dinámica con un cte anteriormente. cte3 AS (select ... )entonces tiene la lógica definida anteriormente con @colsy @query... hay un error. Nombre de objeto inválido 'cte3'. ¿Cómo lo soluciona? -
Elizabeth
3
Esto es fantástico, agradable @bluefeet. Nunca lo había usado STUFF(...)antes (o XML PATHtampoco). Para el beneficio de otros lectores, todo lo que está haciendo es unir los nombres de las columnas y cortar la coma principal. Tenga en cuenta que creo que lo siguiente es un poco más simple: seleccione @cols = (SELECCIONE EL COTIZO DISTINTO (Semana) + ',' del orden de yt por 1 PARA RUTA XML ('')) set @cols = SUBSTRING (@cols, 1, LEN ( @cols) - 1) ... reemplazando group byby distincty order by 1cortando manualmente una coma con sufijo !
DarthPablo
26

Esto es por un número dinámico de semanas.

Ejemplo completo aquí: SQL Dynamic Pivot

DECLARE @DynamicPivotQuery AS NVARCHAR(MAX)
DECLARE @ColumnName AS NVARCHAR(MAX)

--Get distinct values of the PIVOT Column 
SELECT @ColumnName= ISNULL(@ColumnName + ',','') + QUOTENAME(Week)
FROM (SELECT DISTINCT Week FROM #StoreSales) AS Weeks

--Prepare the PIVOT query using the dynamic 
SET @DynamicPivotQuery = 
  N'SELECT Store, ' + @ColumnName + ' 
    FROM #StoreSales
    PIVOT(SUM(xCount) 
          FOR Week IN (' + @ColumnName + ')) AS PVTTable'
--Execute the Dynamic Pivot Query
EXEC sp_executesql @DynamicPivotQuery
Enkode
fuente
Oye, tengo un violín donde necesito pivotar las tablas dinámicamente. ¿Crees que puedes ayudarme con eso? dbfiddle.uk/…
Silly Volley
@SillyVolley aquí es uno, no especificó en qué quería pivotar. Además, no sé si puedes hacer esto en Postgres, así que lo hice en SQL Server: dbfiddle.uk/…
Enkode
16

He logrado lo mismo antes usando subconsultas. Entonces, si su tabla original se llamaba StoreCountsByWeek y tuviera una tabla separada que enumerara los ID de las tiendas, entonces se vería así:

SELECT StoreID, 
    Week1=(SELECT ISNULL(SUM(xCount),0) FROM StoreCountsByWeek WHERE StoreCountsByWeek.StoreID=Store.StoreID AND Week=1),
    Week2=(SELECT ISNULL(SUM(xCount),0) FROM StoreCountsByWeek WHERE StoreCountsByWeek.StoreID=Store.StoreID AND Week=2),
    Week3=(SELECT ISNULL(SUM(xCount),0) FROM StoreCountsByWeek WHERE StoreCountsByWeek.StoreID=Store.StoreID AND Week=3)
FROM Store
ORDER BY StoreID

Una ventaja de este método es que la sintaxis es más clara y hace que sea más fácil unirse a otras tablas para incluir otros campos en los resultados también.

Mis resultados anecdóticos son que ejecutar esta consulta en un par de miles de filas completadas en menos de un segundo, y en realidad tuve 7 subconsultas. Pero como se señaló en los comentarios, es más costoso desde el punto de vista informático hacerlo de esta manera, así que tenga cuidado al usar este método si espera que se ejecute en grandes cantidades de datos.

Eric Barr
fuente
8
es más fácil, pero es una operación muy costosa, esas subconsultas deben ejecutarse una vez por cada fila devuelta desde la tabla.
Greg
11

Esto es lo que puedes hacer:

SELECT * 
FROM yourTable
PIVOT (MAX(xCount) 
       FOR Week in ([1],[2],[3],[4],[5],[6],[7])) AS pvt

MANIFESTACIÓN

Praveen Nambiar
fuente
6

Estoy escribiendo un sp que podría ser útil para este propósito, básicamente este sp pivotea cualquier tabla y devuelve una nueva tabla pivoteada o devuelve solo el conjunto de datos, esta es la forma de ejecutarla:

Exec dbo.rs_pivot_table @schema=dbo,@table=table_name,@column=column_to_pivot,@agg='sum([column_to_agg]),avg([another_column_to_agg]),',
        @sel_cols='column_to_select1,column_to_select2,column_to_select1',@new_table=returned_table_pivoted;

tenga en cuenta que en el parámetro @agg los nombres de columna deben estar con '['y el parámetro debe terminar con una coma','

SP

Create Procedure [dbo].[rs_pivot_table]
    @schema sysname=dbo,
    @table sysname,
    @column sysname,
    @agg nvarchar(max),
    @sel_cols varchar(max),
    @new_table sysname,
    @add_to_col_name sysname=null
As
--Exec dbo.rs_pivot_table dbo,##TEMPORAL1,tip_liq,'sum([val_liq]),sum([can_liq]),','cod_emp,cod_con,tip_liq',##TEMPORAL1PVT,'hola';
Begin

    Declare @query varchar(max)='';
    Declare @aggDet varchar(100);
    Declare @opp_agg varchar(5);
    Declare @col_agg varchar(100);
    Declare @pivot_col sysname;
    Declare @query_col_pvt varchar(max)='';
    Declare @full_query_pivot varchar(max)='';
    Declare @ind_tmpTbl int; --Indicador de tabla temporal 1=tabla temporal global 0=Tabla fisica

    Create Table #pvt_column(
        pivot_col varchar(100)
    );

    Declare @column_agg table(
        opp_agg varchar(5),
        col_agg varchar(100)
    );

    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(@table) AND type in (N'U'))
        Set @ind_tmpTbl=0;
    ELSE IF OBJECT_ID('tempdb..'+ltrim(rtrim(@table))) IS NOT NULL
        Set @ind_tmpTbl=1;

    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(@new_table) AND type in (N'U')) OR 
        OBJECT_ID('tempdb..'+ltrim(rtrim(@new_table))) IS NOT NULL
    Begin
        Set @query='DROP TABLE '+@new_table+'';
        Exec (@query);
    End;

    Select @query='Select distinct '+@column+' From '+(case when @ind_tmpTbl=1 then 'tempdb.' else '' end)+@schema+'.'+@table+' where '+@column+' is not null;';
    Print @query;

    Insert into #pvt_column(pivot_col)
    Exec (@query)

    While charindex(',',@agg,1)>0
    Begin
        Select @aggDet=Substring(@agg,1,charindex(',',@agg,1)-1);

        Insert Into @column_agg(opp_agg,col_agg)
        Values(substring(@aggDet,1,charindex('(',@aggDet,1)-1),ltrim(rtrim(replace(substring(@aggDet,charindex('[',@aggDet,1),charindex(']',@aggDet,1)-4),')',''))));

        Set @agg=Substring(@agg,charindex(',',@agg,1)+1,len(@agg))

    End

    Declare cur_agg cursor read_only forward_only local static for
    Select 
        opp_agg,col_agg
    from @column_agg;

    Open cur_agg;

    Fetch Next From cur_agg
    Into @opp_agg,@col_agg;

    While @@fetch_status=0
    Begin

        Declare cur_col cursor read_only forward_only local static for
        Select 
            pivot_col 
        From #pvt_column;

        Open cur_col;

        Fetch Next From cur_col
        Into @pivot_col;

        While @@fetch_status=0
        Begin

            Select @query_col_pvt='isnull('+@opp_agg+'(case when '+@column+'='+quotename(@pivot_col,char(39))+' then '+@col_agg+
            ' else null end),0) as ['+lower(Replace(Replace(@opp_agg+'_'+convert(varchar(100),@pivot_col)+'_'+replace(replace(@col_agg,'[',''),']',''),' ',''),'&',''))+
                (case when @add_to_col_name is null then space(0) else '_'+isnull(ltrim(rtrim(@add_to_col_name)),'') end)+']'
            print @query_col_pvt
            Select @full_query_pivot=@full_query_pivot+@query_col_pvt+', '

            --print @full_query_pivot

            Fetch Next From cur_col
            Into @pivot_col;        

        End     

        Close cur_col;
        Deallocate cur_col;

        Fetch Next From cur_agg
        Into @opp_agg,@col_agg; 
    End

    Close cur_agg;
    Deallocate cur_agg;

    Select @full_query_pivot=substring(@full_query_pivot,1,len(@full_query_pivot)-1);

    Select @query='Select '+@sel_cols+','+@full_query_pivot+' into '+@new_table+' From '+(case when @ind_tmpTbl=1 then 'tempdb.' else '' end)+
    @schema+'.'+@table+' Group by '+@sel_cols+';';

    print @query;
    Exec (@query);

End;
GO

Este es un ejemplo de ejecución:

Exec dbo.rs_pivot_table @schema=dbo,@table=##TEMPORAL1,@column=tip_liq,@agg='sum([val_liq]),avg([can_liq]),',@sel_cols='cod_emp,cod_con,tip_liq',@new_table=##TEMPORAL1PVT;

luego Select * From ##TEMPORAL1PVTvolvería:

ingrese la descripción de la imagen aquí

MelgoV
fuente
2
select * from (select name, ID from Empoyee) Visits
    pivot(sum(ID) for name
    in ([Emp1],
    [Emp2],
    [Emp3]
    ) ) as pivottable;
Muhammad Bilal
fuente
2

Aquí hay una revisión de la respuesta @Tayrn anterior que podría ayudarlo a comprender cómo pivotar un poco más fácilmente:

Puede que esta no sea la mejor manera de hacerlo, pero esto es lo que me ayudó a entender cómo pivotar las tablas.

ID = filas que desea pivotar

MY_KEY = la columna que está seleccionando de su tabla original que contiene los nombres de columna que desea pivotar.

VAL = el valor que desea devolver debajo de cada columna.

MAX (VAL) => Se puede reemplazar con otras funciones de agregación. SUMA (VAL), MIN (VAL), ETC ...

DECLARE @cols AS NVARCHAR(MAX),
@query  AS NVARCHAR(MAX)
select @cols = STUFF((SELECT ',' + QUOTENAME(MY_KEY) 
                from yt
                group by MY_KEY
                order by MY_KEY ASC
        FOR XML PATH(''), TYPE
        ).value('.', 'NVARCHAR(MAX)') 
    ,1,1,'')
set @query = 'SELECT ID,' + @cols + ' from 
         (
            select ID, MY_KEY, VAL 
            from yt
        ) x
        pivot 
        (
            sum(VAL)
            for MY_KEY in (' + @cols + ')
        ) p '

        execute(@query);
FarajDaoud
fuente
2

Solo darte una idea de cómo otras bases de datos resuelven este problema. DolphinDBtambién tiene soporte incorporado para pivotar y el sql se ve mucho más intuitivo y ordenado. Es tan simple como especificar la columna clave ( Store), la columna pivotante ( Week) y la métrica calculada ( sum(xCount)).

//prepare a 10-million-row table
n=10000000
t=table(rand(100, n) + 1 as Store, rand(54, n) + 1 as Week, rand(100, n) + 1 as xCount)

//use pivot clause to generate a pivoted table pivot_t
pivot_t = select sum(xCount) from t pivot by Store, Week

DolphinDB es una base de datos columnar de alto rendimiento. El cálculo en la demostración cuesta tan solo 546 ms en una computadora portátil dell xps (i7 cpu). Para obtener más detalles, consulte el manual en línea de DolphinDB https://www.dolphindb.com/help/index.html?pivotby.html

Davis Zhou
fuente