¿Una forma sencilla de transponer columnas y filas en SQL?

110

¿Cómo cambio simplemente columnas con filas en SQL? ¿Existe algún comando simple para transponer?

es decir, convierte este resultado:

        Paul  | John  | Tim  |  Eric
Red     1       5       1       3
Green   8       4       3       5
Blue    2       2       9       1

dentro de esto:

        Red  | Green | Blue
Paul    1       8       2
John    5       4       2
Tim     1       3       9
Eric    3       5       1

PIVOT parece demasiado complejo para este escenario.

edezzie
fuente
1
stackoverflow.com/questions/10699997/… es una pregunta algo similar.
Tim Dearborn

Respuestas:

145

Hay varias formas de transformar estos datos. En su publicación original, dijo que PIVOTparece demasiado complejo para este escenario, pero se puede aplicar muy fácilmente usando las funciones UNPIVOTyPIVOT en SQL Server.

Sin embargo, si usted no tiene acceso a estas funciones se pueden replicar esta usando UNION ALLa UNPIVOTy luego una función de agregado con una CASEdeclaración a PIVOT:

Crear mesa:

CREATE TABLE yourTable([color] varchar(5), [Paul] int, [John] int, [Tim] int, [Eric] int);

INSERT INTO yourTable
    ([color], [Paul], [John], [Tim], [Eric])
VALUES
    ('Red', 1, 5, 1, 3),
    ('Green', 8, 4, 3, 5),
    ('Blue', 2, 2, 9, 1);

Versión Union All, Agregada y CASE:

select name,
  sum(case when color = 'Red' then value else 0 end) Red,
  sum(case when color = 'Green' then value else 0 end) Green,
  sum(case when color = 'Blue' then value else 0 end) Blue
from
(
  select color, Paul value, 'Paul' name
  from yourTable
  union all
  select color, John value, 'John' name
  from yourTable
  union all
  select color, Tim value, 'Tim' name
  from yourTable
  union all
  select color, Eric value, 'Eric' name
  from yourTable
) src
group by name

Ver SQL Fiddle con demostración

Los UNION ALLrealiza las UNPIVOTde los datos mediante la transformación de las columnas Paul, John, Tim, Ericen filas separadas. Luego aplica la función agregada sum()con la casedeclaración para obtener las nuevas columnas para cada una color.

Versión estática sin pivote y pivote:

Tanto las funciones UNPIVOTy PIVOTen el servidor SQL facilitan mucho esta transformación. Si conoce todos los valores que desea transformar, puede codificarlos en una versión estática para obtener el resultado:

select name, [Red], [Green], [Blue]
from
(
  select color, name, value
  from yourtable
  unpivot
  (
    value for name in (Paul, John, Tim, Eric)
  ) unpiv
) src
pivot
(
  sum(value)
  for color in ([Red], [Green], [Blue])
) piv

Ver SQL Fiddle con demostración

La consulta interna con UNPIVOTrealiza la misma función que UNION ALL. Toma la lista de columnas y la convierte en filas, PIVOTluego realiza la transformación final en columnas.

Versión dinámica de pivote:

Si tiene un número desconocido de columnas ( Paul, John, Tim, Ericen su ejemplo) y luego un número desconocido de colores para transformar, puede usar sql dinámico para generar la lista UNPIVOTy luego PIVOT:

DECLARE @colsUnpivot AS NVARCHAR(MAX),
    @query  AS NVARCHAR(MAX),
    @colsPivot as  NVARCHAR(MAX)

select @colsUnpivot = stuff((select ','+quotename(C.name)
         from sys.columns as C
         where C.object_id = object_id('yourtable') and
               C.name <> 'color'
         for xml path('')), 1, 1, '')

select @colsPivot = STUFF((SELECT  ',' 
                      + quotename(color)
                    from yourtable t
            FOR XML PATH(''), TYPE
            ).value('.', 'NVARCHAR(MAX)') 
        ,1,1,'')


set @query 
  = 'select name, '+@colsPivot+'
      from
      (
        select color, name, value
        from yourtable
        unpivot
        (
          value for name in ('+@colsUnpivot+')
        ) unpiv
      ) src
      pivot
      (
        sum(value)
        for color in ('+@colsPivot+')
      ) piv'

exec(@query)

Ver SQL Fiddle con demostración

La versión dinámica consulta ambos yourtabley luego la sys.columnstabla para generar la lista de elementos para UNPIVOTy PIVOT. Esto luego se agrega a una cadena de consulta para su ejecución. La ventaja de la versión dinámica es si tiene una lista cambiante colorsy / o namesesto generará la lista en tiempo de ejecución.

Las tres consultas producirán el mismo resultado:

| NAME | RED | GREEN | BLUE |
-----------------------------
| Eric |   3 |     5 |    1 |
| John |   5 |     4 |    2 |
| Paul |   1 |     8 |    2 |
|  Tim |   1 |     3 |    9 |
Taryn
fuente
Debería haber enfatizado que quiero un método para transponer los resultados de una consulta en una base de datos. Un comando de tipo de transposición simple que se puede ejecutar en el conjunto de resultados de una consulta sin tener que saber nada sobre la consulta en sí, o las columnas, tablas, etc. de donde está extrayendo información. Algo como: (TRANSPOSE (SELECT ......)) En mi ejemplo anterior, imagine que la primera tabla es un conjunto de resultados generado a partir de una consulta muy compleja que involucra múltiples tablas, uniones, pivotes, etc. Sin embargo, el resultado es un mesa sencilla. Solo quiero voltear las columnas con filas en este resultado.
edezzie
Estoy pasando el resultado a ac # DataTable. Puedo construir un método simple 'Transpose (Datatable dt)' para hacerlo allí, pero ¿hay algo en SQL para hacer esto?
edezzie
@edezzie no hay una forma simple en SQL de hacer esto. Probablemente podría modificar la versión dinámica para pasar el nombre de la tabla y extraer la información de las tablas del sistema.
Taryn
¿Hay alguna razón por la que no haya marcado esta brillante respuesta como RESPUESTA? ¡Dale una medalla a @bluefeet!
Fandango68
Esto está funcionando bien, seleccione * de Tabla sin divisiones (valor para el nombre en ([Paul], [John], [Tim], [Eric])) pivote hacia arriba (max (valor) para el color en ([Red], [Green] , [Azul])) p pero ¿es posible escribir esta selección * de Tabla sin divisiones (valor para el nombre en ([Paul], [John], [Tim], [Eric])) pivote hacia arriba (max (valor) para color en (SELECT COLOR FROM TABLENAME)) p en lugar de ingresar el valor entre comillas, no se pueden recuperar los valores directamente usando la consulta de selección.
Mogli
20

Esto normalmente requiere que conozca TODAS las etiquetas de columna Y fila de antemano. Como puede ver en la consulta a continuación, todas las etiquetas se enumeran en su totalidad en las operaciones UNPIVOT y (re) PIVOT.

Configuración del esquema de MS SQL Server 2012 :

create table tbl (
    color varchar(10), Paul int, John int, Tim int, Eric int);
insert tbl select 
    'Red' ,1 ,5 ,1 ,3 union all select
    'Green' ,8 ,4 ,3 ,5 union all select
    'Blue' ,2 ,2 ,9 ,1;

Consulta 1 :

select *
from tbl
unpivot (value for name in ([Paul],[John],[Tim],[Eric])) up
pivot (max(value) for color in ([Red],[Green],[Blue])) p

Resultados :

| NAME | RED | GREEN | BLUE |
-----------------------------
| Eric |   3 |     5 |    1 |
| John |   5 |     4 |    2 |
| Paul |   1 |     8 |    2 |
|  Tim |   1 |     3 |    9 |

Notas adicionales:

  1. Dado un nombre de tabla, puede determinar todos los nombres de columna de sys.columns o el engaño de FOR XML utilizando local-name () .
  2. También puede crear la lista de colores distintos (o valores para una columna) usando FOR XML.
  3. Lo anterior se puede combinar en un lote SQL dinámico para manejar cualquier tabla.
RichardTheKiwi
fuente
11

Me gustaría señalar algunas soluciones más para transponer columnas y filas en SQL.

El primero es usar CURSOR. Aunque el consenso general en la comunidad profesional es mantenerse alejado de los cursores de SQL Server, todavía hay casos en los que se recomienda el uso de cursores. De todos modos, los cursores nos presentan otra opción para transponer filas en columnas.

  • Expansión vertical

    De manera similar al PIVOT, el cursor tiene la capacidad dinámica de agregar más filas a medida que su conjunto de datos se expande para incluir más números de póliza.

  • Expansión horizontal

    A diferencia del PIVOT, el cursor sobresale en esta área, ya que puede expandirse para incluir el documento recién agregado, sin alterar el script.

  • Desglose del rendimiento

    La principal limitación de la transposición de filas a columnas usando CURSOR es una desventaja que está relacionada con el uso de cursores en general: tienen un costo de rendimiento significativo. Esto se debe a que el Cursor genera una consulta separada para cada operación FETCH NEXT.

Otra solución para transponer filas a columnas es mediante XML.

La solución XML para transponer filas en columnas es básicamente una versión óptima de PIVOT ya que aborda la limitación dinámica de columnas.

La versión XML del script aborda esta limitación mediante el uso de una combinación de XML Path, T-SQL dinámico y algunas funciones integradas (es decir, STUFF, QUOTENAME).

  • Expansión vertical

    Al igual que el PIVOT y el Cursor, las políticas recién agregadas se pueden recuperar en la versión XML del script sin alterar el script original.

  • Expansión horizontal

    A diferencia de PIVOT, los documentos recién agregados se pueden mostrar sin alterar el script.

  • Desglose del rendimiento

    En términos de IO, las estadísticas de la versión XML del script son casi similares a las de PIVOT; la única diferencia es que el XML tiene un segundo escaneo de la tabla dtTranspose, pero esta vez desde una caché de datos de lectura lógica.

Puede encontrar más sobre estas soluciones (incluidos algunos ejemplos reales de T-SQL) en este artículo: https://www.sqlshack.com/multiple-options-to-transposing-rows-into-columns/

MikeS
fuente
8

Basado en esta solución de bluefeet, aquí hay un procedimiento almacenado que usa sql dinámico para generar la tabla transpuesta. Requiere que todos los campos sean numéricos excepto la columna transpuesta (la columna que será el encabezado en la tabla resultante):

/****** Object:  StoredProcedure [dbo].[SQLTranspose]    Script Date: 11/10/2015 7:08:02 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:      Paco Zarate
-- Create date: 2015-11-10
-- Description: SQLTranspose dynamically changes a table to show rows as headers. It needs that all the values are numeric except for the field using for     transposing.
-- Parameters: @TableName - Table to transpose
--             @FieldNameTranspose - Column that will be the new headers
-- Usage: exec SQLTranspose <table>, <FieldToTranspose>
-- =============================================
ALTER PROCEDURE [dbo].[SQLTranspose] 
  -- Add the parameters for the stored procedure here
  @TableName NVarchar(MAX) = '', 
  @FieldNameTranspose NVarchar(MAX) = ''
AS
BEGIN
  -- SET NOCOUNT ON added to prevent extra result sets from
  -- interfering with SELECT statements.
  SET NOCOUNT ON;

  DECLARE @colsUnpivot AS NVARCHAR(MAX),
  @query  AS NVARCHAR(MAX),
  @queryPivot  AS NVARCHAR(MAX),
  @colsPivot as  NVARCHAR(MAX),
  @columnToPivot as NVARCHAR(MAX),
  @tableToPivot as NVARCHAR(MAX), 
  @colsResult as xml

  select @tableToPivot = @TableName;
  select @columnToPivot = @FieldNameTranspose


  select @colsUnpivot = stuff((select ','+quotename(C.name)
       from sys.columns as C
       where C.object_id = object_id(@tableToPivot) and
             C.name <> @columnToPivot 
       for xml path('')), 1, 1, '')

  set @queryPivot = 'SELECT @colsResult = (SELECT  '','' 
                    + quotename('+@columnToPivot+')
                  from '+@tableToPivot+' t
                  where '+@columnToPivot+' <> ''''
          FOR XML PATH(''''), TYPE)'

  exec sp_executesql @queryPivot, N'@colsResult xml out', @colsResult out

  select @colsPivot = STUFF(@colsResult.value('.', 'NVARCHAR(MAX)'),1,1,'')

  set @query 
    = 'select name, rowid, '+@colsPivot+'
        from
        (
          select '+@columnToPivot+' , name, value, ROW_NUMBER() over (partition by '+@columnToPivot+' order by '+@columnToPivot+') as rowid
          from '+@tableToPivot+'
          unpivot
          (
            value for name in ('+@colsUnpivot+')
          ) unpiv
        ) src
        pivot
        (
          sum(value)
          for '+@columnToPivot+' in ('+@colsPivot+')
        ) piv
        order by rowid'
  exec(@query)
END

Puede probarlo con la tabla proporcionada con este comando:

exec SQLTranspose 'yourTable', 'color'
Paco Zarate
fuente
argg. pero quiero uno donde todos los campos sean nvarchar (max)
hamish
1
Para todos los campos nvarchar (max) simplemente cambie la suma (valor) con max (valor) en la sexta línea desde el final
Paco Zarate
2

Estoy haciendo UnPivotprimero y almacenando los resultados CTEy usando el CTEen Pivotfuncionamiento.

Manifestación

with cte as
(
select 'Paul' as Name, color, Paul as Value 
 from yourTable
 union all
 select 'John' as Name, color, John as Value 
 from yourTable
 union all
 select 'Tim' as Name, color, Tim as Value 
 from yourTable
 union all
 select 'Eric' as Name, color, Eric as Value 
 from yourTable
 )

select Name, [Red], [Green], [Blue]
from
(
select *
from cte
) as src
pivot 
(
  max(Value)
  for color IN ([Red], [Green], [Blue])
) as Dtpivot;
Vishwanath Dalvi
fuente
2

Agregando a la excelente respuesta de @Paco Zarate anterior, si desea transponer una tabla que tiene múltiples tipos de columnas, agregue esto al final de la línea 39, para que solo transponga intcolumnas:

and C.system_type_id = 56   --56 = type int

Aquí está la consulta completa que se está cambiando:

select @colsUnpivot = stuff((select ','+quotename(C.name)
from sys.columns as C
where C.object_id = object_id(@tableToPivot) and
      C.name <> @columnToPivot and C.system_type_id = 56    --56 = type int
for xml path('')), 1, 1, '')

Para encontrar otros system_type_id, ejecute esto:

select name, system_type_id from sys.types order by name
Jonathan Harris
fuente
1

Me gusta compartir el código que estoy usando para transponer un texto dividido basado en la respuesta + bluefeet. En este enfoque estoy implementado como un procedimiento en MS SQL 2005

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:      ELD.
-- Create date: May, 5 2016.
-- Description: Transpose from rows to columns the user split function.
-- =============================================
CREATE PROCEDURE TransposeSplit @InputToSplit VARCHAR(8000)
    ,@Delimeter VARCHAR(8000) = ','
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @colsUnpivot AS NVARCHAR(MAX)
        ,@query AS NVARCHAR(MAX)
        ,@queryPivot AS NVARCHAR(MAX)
        ,@colsPivot AS NVARCHAR(MAX)
        ,@columnToPivot AS NVARCHAR(MAX)
        ,@tableToPivot AS NVARCHAR(MAX)
        ,@colsResult AS XML

    SELECT @tableToPivot = '#tempSplitedTable'

    SELECT @columnToPivot = 'col_number'

    CREATE TABLE #tempSplitedTable (
        col_number INT
        ,col_value VARCHAR(8000)
        )

    INSERT INTO #tempSplitedTable (
        col_number
        ,col_value
        )
    SELECT ROW_NUMBER() OVER (
            ORDER BY (
                    SELECT 100
                    )
            ) AS RowNumber
        ,item
    FROM [DB].[ESCHEME].[fnSplit](@InputToSplit, @Delimeter)

    SELECT @colsUnpivot = STUFF((
                SELECT ',' + quotename(C.NAME)
                FROM [tempdb].sys.columns AS C
                WHERE C.object_id = object_id('tempdb..' + @tableToPivot)
                    AND C.NAME <> @columnToPivot
                FOR XML path('')
                ), 1, 1, '')

    SET @queryPivot = 'SELECT @colsResult = (SELECT  '','' 
                    + quotename(' + @columnToPivot + ')
                  from ' + @tableToPivot + ' t
                  where ' + @columnToPivot + ' <> ''''
          FOR XML PATH(''''), TYPE)'

    EXEC sp_executesql @queryPivot
        ,N'@colsResult xml out'
        ,@colsResult OUT

    SELECT @colsPivot = STUFF(@colsResult.value('.', 'NVARCHAR(MAX)'), 1, 1, '')

    SET @query = 'select name, rowid, ' + @colsPivot + '
        from
        (
          select ' + @columnToPivot + ' , name, value, ROW_NUMBER() over (partition by ' + @columnToPivot + ' order by ' + @columnToPivot + ') as rowid
          from ' + @tableToPivot + '
          unpivot
          (
            value for name in (' + @colsUnpivot + ')
          ) unpiv
        ) src
        pivot
        (
          MAX(value)
          for ' + @columnToPivot + ' in (' + @colsPivot + ')
        ) piv
        order by rowid'

    EXEC (@query)

    DROP TABLE #tempSplitedTable
END
GO

Estoy mezclando esta solución con la información sobre cómo ordenar filas sin ordenar por ( SQLAuthority.com ) y la función de división en MSDN ( social.msdn.microsoft.com )

Cuando ejecuta el prodecure

DECLARE @RC int
DECLARE @InputToSplit varchar(MAX)
DECLARE @Delimeter varchar(1)

set @InputToSplit = 'hello|beautiful|world'
set @Delimeter = '|'

EXECUTE @RC = [TransposeSplit] 
   @InputToSplit
  ,@Delimeter
GO

obtienes el siguiente resultado

  name       rowid  1      2          3
  col_value  1      hello  beautiful  world
Elidio Marquina
fuente
1

De esta manera, convierta todos los datos de Filelds (columnas) en la tabla para registrar (fila).

Declare @TableName  [nvarchar](128)
Declare @ExecStr    nvarchar(max)
Declare @Where      nvarchar(max)
Set @TableName = 'myTableName'
--Enter Filtering If Exists
Set @Where = ''

--Set @ExecStr = N'Select * From '+quotename(@TableName)+@Where
--Exec(@ExecStr)

Drop Table If Exists #tmp_Col2Row

Create Table #tmp_Col2Row
(Field_Name nvarchar(128) Not Null
,Field_Value nvarchar(max) Null
)

Set @ExecStr = N' Insert Into #tmp_Col2Row (Field_Name , Field_Value) '
Select @ExecStr += (Select N'Select '''+C.name+''' ,Convert(nvarchar(max),'+quotename(C.name) + ') From ' + quotename(@TableName)+@Where+Char(10)+' Union All '
         from sys.columns as C
         where (C.object_id = object_id(@TableName)) 
         for xml path(''))
Select @ExecStr = Left(@ExecStr,Len(@ExecStr)-Len(' Union All '))
--Print @ExecStr
Exec (@ExecStr)

Select * From #tmp_Col2Row
Go
Mahmood Khezrian
fuente
0

Pude usar la solución de Paco Zarate y funciona a la perfección. Tuve que agregar una línea ("SET ANSI_WARNINGS ON"), pero eso puede ser algo único en la forma en que lo usé o lo llamé. Hay un problema con mi uso y espero que alguien pueda ayudarme con él:

La solución solo funciona con una tabla SQL real. Lo probé con una tabla temporal y también una tabla en memoria (declarada) pero no funciona con esas. Entonces, en mi código de llamada, creo una tabla en mi base de datos SQL y luego llamo a SQLTranspose. Nuevamente, funciona muy bien. Es solo lo que quiero. Este es mi problema:

Para que la solución general sea verdaderamente dinámica, necesito crear esa tabla donde almaceno temporalmente la información preparada que estoy enviando a SQLTranspose "sobre la marcha", y luego eliminar esa tabla una vez que se llama a SQLTranspose. La eliminación de la tabla presenta un problema con mi plan de implementación final. El código debe ejecutarse desde una aplicación de usuario final (un botón en un formulario / menú de Microsoft Access). Cuando utilizo este proceso SQL (creo una tabla SQL, llamo a SQLTranspose, elimino la tabla SQL), la aplicación del usuario final da un error porque la cuenta SQL utilizada no tiene los derechos para eliminar una tabla.

Entonces me imagino que hay algunas soluciones posibles:

  1. Encuentre una manera de hacer que SQLTranspose funcione con una tabla temporal o una variable de tabla declarada.

  2. Descubra otro método para la transposición de filas y columnas que no requiera una tabla SQL real.

  3. Averigüe un método apropiado para permitir que la cuenta SQL utilizada por mis usuarios finales elimine una tabla. Es una única cuenta SQL compartida codificada en mi aplicación de Access. Parece que el permiso es un privilegio de tipo dbo que no se puede conceder.

Reconozco que algo de esto puede justificar otro hilo y una pregunta separados. Sin embargo, dado que existe la posibilidad de que una solución sea simplemente una forma diferente de realizar la transposición de filas y columnas, haré mi primera publicación aquí en este hilo.

EDITAR: También reemplacé sum (valor) con max (valor) en la sexta línea desde el final, como sugirió Paco.

EDITAR:

Descubrí algo que funciona para mí. No sé si es la mejor respuesta o no.

Tengo una cuenta de usuario de solo lectura que se usa para ejecutar procedimientos registrados y, por lo tanto, generar informes de salida desde una base de datos. Dado que la función SQLTranspose que creé solo funcionará con una tabla "legítima" (no una tabla declarada ni una tabla temporal), tuve que encontrar una forma para que una cuenta de usuario de solo lectura creara (y luego eliminara) una tabla .

Razoné que para mis propósitos está bien que la cuenta de usuario pueda crear una tabla. Sin embargo, el usuario aún no pudo eliminar la tabla. Mi solución fue crear un esquema donde la cuenta de usuario está autorizada. Luego, cada vez que creo, uso o elimino esa tabla, la refiero con el esquema especificado.

Primero emití este comando desde una cuenta 'sa' o 'sysadmin': CREATE SCHEMA ro AUTHORIZATION

Cuando en cualquier momento me refiero a mi tabla "tmpoutput" lo especifico como este ejemplo:

drop table ro.tmpoutput

CraigD
fuente
Descubrí algo que funciona para mí. No sé si es la mejor respuesta o no.
CraigD