SQL Server: columnas a filas

129

Buscando una solución elegante (o cualquier) para convertir columnas en filas.

Aquí hay un ejemplo: tengo una tabla con el siguiente esquema:

[ID] [EntityID] [Indicator1] [Indicator2] [Indicator3] ... [Indicator150]

Esto es lo que quiero obtener como resultado:

[ID] [EntityId] [IndicatorName] [IndicatorValue]

Y los valores del resultado serán:

1 1 'Indicator1' 'Value of Indicator 1 for entity 1'
2 1 'Indicator2' 'Value of Indicator 2 for entity 1'
3 1 'Indicator3' 'Value of Indicator 3 for entity 1'
4 2 'Indicator1' 'Value of Indicator 1 for entity 2'

Y así..

¿Esto tiene sentido? ¿Tiene alguna sugerencia sobre dónde buscar y cómo hacerlo en T-SQL?

Sergei
fuente
2
¿ Ya has examinado Pivot / Unpivot ?
Josh Jay
Al final se fue con la solución de bluefeet. Elegante y funcional. Muchas gracias a todos.
Sergei

Respuestas:

248

Puede usar la función UNPIVOT para convertir las columnas en filas:

select id, entityId,
  indicatorname,
  indicatorvalue
from yourtable
unpivot
(
  indicatorvalue
  for indicatorname in (Indicator1, Indicator2, Indicator3)
) unpiv;

Tenga en cuenta que los tipos de datos de las columnas que está desconectando deben ser los mismos, por lo que es posible que tenga que convertir los tipos de datos antes de aplicar la desconexión.

También puede usar CROSS APPLYcon UNION ALL para convertir las columnas:

select id, entityid,
  indicatorname,
  indicatorvalue
from yourtable
cross apply
(
  select 'Indicator1', Indicator1 union all
  select 'Indicator2', Indicator2 union all
  select 'Indicator3', Indicator3 union all
  select 'Indicator4', Indicator4 
) c (indicatorname, indicatorvalue);

Dependiendo de su versión de SQL Server, incluso podría usar CROSS APPLY con la cláusula VALUES:

select id, entityid,
  indicatorname,
  indicatorvalue
from yourtable
cross apply
(
  values
  ('Indicator1', Indicator1),
  ('Indicator2', Indicator2),
  ('Indicator3', Indicator3),
  ('Indicator4', Indicator4)
) c (indicatorname, indicatorvalue);

Finalmente, si tiene 150 columnas para desconectar y no desea codificar la consulta completa, puede generar la instrucción sql usando SQL dinámico:

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

select @colsUnpivot 
  = stuff((select ','+quotename(C.column_name)
           from information_schema.columns as C
           where C.table_name = 'yourtable' and
                 C.column_name like 'Indicator%'
           for xml path('')), 1, 1, '')

set @query 
  = 'select id, entityId,
        indicatorname,
        indicatorvalue
     from yourtable
     unpivot
     (
        indicatorvalue
        for indicatorname in ('+ @colsunpivot +')
     ) u'

exec sp_executesql @query;
Taryn
fuente
44
Para aquellos que quieren más detalles sobre UNPIVOTy / vs. APPLY, esta publicación de blog de 2010 de Brad Schulz (y la continuación ) es (son) hermosas.
ruffin
2
Mensaje 8167, Nivel 16, Estado 1, Línea 147 El tipo de columna "blahblah" entra en conflicto con el tipo de otras columnas especificadas en la lista UNPIVOT.
JDPeckham
@JDPeckham Si tiene diferentes tipos de datos, entonces debe convertirlos para que sean del mismo tipo y longitud antes de realizar la eliminación dinámica. Aquí hay más información sobre eso .
Taryn
el método xml tiene una falla porque no puede escapar de los códigos xml como & gt ;, & lt; y & amp ;. Además, el rendimiento puede mejorarse significativamente reescribiendo de la siguiente manera: select @colsUnpivot = stuff ((select ',' + quotename (C.column_name) as [text ()] from information_schema.columns as C where C.table_name = 'yourtable' y C.column_name como 'Indicator%' para la ruta xml (''), tipo) .value ('text () [1]', 'nvarchar (max)'), 1, 1, '')
rrozema
24

bueno, si tienes 150 columnas, creo que UNPIVOT no es una opción. Entonces podrías usar el truco xml

;with CTE1 as (
    select ID, EntityID, (select t.* for xml raw('row'), type) as Data
    from temp1 as t
), CTE2 as (
    select
         C.id, C.EntityID,
         F.C.value('local-name(.)', 'nvarchar(128)') as IndicatorName,
         F.C.value('.', 'nvarchar(max)') as IndicatorValue
    from CTE1 as c
        outer apply c.Data.nodes('row/@*') as F(C)
)
select * from CTE2 where IndicatorName like 'Indicator%'

sql fiddle demo

También podría escribir SQL dinámico, pero me gusta más xml: para el SQL dinámico debe tener permisos para seleccionar datos directamente de la tabla y eso no siempre es una opción.

ACTUALIZACIÓN
Como hay una gran llama en los comentarios, creo que agregaré algunos pros y contras de xml / dynamic SQL. Intentaré ser lo más objetivo posible y no mencionar la elegancia y la fealdad. Si tienes otros pros y contras, edita la respuesta o escribe comentarios

contras

  • no es tan rápido como el SQL dinámico, las pruebas aproximadas me dieron que xml es aproximadamente 2.5 veces más lento que el dinámico (era una consulta en una tabla de ~ 250000 filas, por lo que esta estimación no es exacta). Puede compararlo usted mismo si lo desea, aquí está el ejemplo de sqlfiddle , en 100000 filas era 29s (xml) frente a 14s (dinámico);
  • puede ser más difícil de entender para personas que no están familiarizadas con xpath;

pros

  • es el mismo alcance que sus otras consultas, y eso podría ser muy útil. Algunos ejemplos me vienen a la mente
    • podría consultar insertedy deletedtablas dentro de su activador (no es posible con dinámico en absoluto);
    • el usuario no tiene que tener permisos en la selección directa de la tabla. Lo que quiero decir es que si tiene una capa de procedimientos almacenados y el usuario tiene permisos para ejecutar sp, pero no tiene permisos para consultar tablas directamente, aún podría usar esta consulta dentro del procedimiento almacenado;
    • puede consultar la variable de tabla que ha rellenado en su ámbito (para pasarla dentro del SQL dinámico, debe convertirla en una tabla temporal en su lugar o crear un tipo y pasarla como parámetro al SQL dinámico;
  • puede hacer esta consulta dentro de la función (escalar o con valores de tabla). No es posible usar SQL dinámico dentro de las funciones;
Roman Pekar
fuente
2
¿Qué datos está seleccionando con XML que no requiere seleccionar datos de la tabla?
Aaron Bertrand
1
Por ejemplo, podría decidir no otorgar a los usuarios permisos para seleccionar datos de tablas, sino solo en procedimientos almacenados que funcionan con tablas, por lo que podría seleccionar xml dentro del procedimiento, pero tengo que usar algunas soluciones alternativas si quiero usar SQL dinámico
Roman Pekar
3
Si desea que sus usuarios puedan ejecutar el código, debe darles el acceso que necesiten para ejecutar el código. No invente requisitos que no existen para que su respuesta suene mejor (tampoco tiene que comentar las respuestas de la competencia para ver su respuesta; si encuentran esa respuesta, también pueden encontrar la suya).
Aaron Bertrand
2
Además, si su justificación para usar XML es que puede ponerlo en un procedimiento almacenado para evitar dar acceso directo a la tabla, tal vez su ejemplo debería mostrar cómo colocarlo en un procedimiento almacenado y cómo otorgar derechos a un usuario para que pueda puede ejecutarlo sin tener acceso de lectura a la tabla subyacente. Para mí, eso es un cambio de alcance, porque la mayoría de las personas que escriben consultas en una tabla tienen acceso de lectura a la tabla.
Aaron Bertrand
2
Yo diría que sí importa una diferencia de 10 veces en la duración. Y ~ 8,000 filas no son "grandes cantidades de datos". ¿Deberíamos ver qué sucede con 800,000 filas?
Aaron Bertrand
7

Solo para ayudar a los nuevos lectores, he creado un ejemplo para comprender mejor la respuesta de @ bluefeet sobre UNPIVOT.

 SELECT id
        ,entityId
        ,indicatorname
        ,indicatorvalue
  FROM (VALUES
        (1, 1, 'Value of Indicator 1 for entity 1', 'Value of Indicator 2 for entity 1', 'Value of Indicator 3 for entity 1'),
        (2, 1, 'Value of Indicator 1 for entity 2', 'Value of Indicator 2 for entity 2', 'Value of Indicator 3 for entity 2'),
        (3, 1, 'Value of Indicator 1 for entity 3', 'Value of Indicator 2 for entity 3', 'Value of Indicator 3 for entity 3'),
        (4, 2, 'Value of Indicator 1 for entity 4', 'Value of Indicator 2 for entity 4', 'Value of Indicator 3 for entity 4')
       ) AS Category(ID, EntityId, Indicator1, Indicator2, Indicator3)
UNPIVOT
(
    indicatorvalue
    FOR indicatorname IN (Indicator1, Indicator2, Indicator3)
) UNPIV;
Dmyan
fuente
3

Necesitaba una solución para convertir columnas en filas en Microsoft SQL Server, sin conocer los nombres de columna (utilizados en el desencadenador) y sin sql dinámico (sql dinámico es demasiado lento para usarlo en un activador).

Finalmente encontré esta solución, que funciona bien:

SELECT
    insRowTbl.PK,
    insRowTbl.Username,
    attr.insRow.value('local-name(.)', 'nvarchar(128)') as FieldName,
    attr.insRow.value('.', 'nvarchar(max)') as FieldValue 
FROM ( Select      
          i.ID as PK,
          i.LastModifiedBy as Username,
          convert(xml, (select i.* for xml raw)) as insRowCol
       FROM inserted as i
     ) as insRowTbl
CROSS APPLY insRowTbl.insRowCol.nodes('/row/@*') as attr(insRow)

Como puede ver, convierto la fila a XML (Subconsulta, seleccione i, * para xml raw, esto convierte todas las columnas en una columna xml)

Luego CRUZ APLICO una función a cada atributo XML de esta columna, de modo que obtengo una fila por atributo.

En general, esto convierte las columnas en filas, sin conocer los nombres de las columnas y sin utilizar sql dinámico. Es lo suficientemente rápido para mi propósito.

(Editar: acabo de ver a Roman Pekar responder arriba, quién está haciendo lo mismo. Primero utilicé el gatillo sql dinámico con cursores, que era de 10 a 100 veces más lento que esta solución, pero tal vez fue causado por el cursor, no por el sql dinámico. De todos modos, esta solución es muy simple y universal, por lo que definitivamente es una opción).

Dejo este comentario en este lugar, porque quiero hacer referencia a esta explicación en mi publicación sobre el activador de auditoría completo, que puede encontrar aquí: https://stackoverflow.com/a/43800286/4160788

flack
fuente
3
DECLARE @TableName varchar(max)=NULL
SELECT @TableName=COALESCE(@TableName+',','')+t.TABLE_CATALOG+'.'+ t.TABLE_SCHEMA+'.'+o.Name
  FROM sysindexes AS i
  INNER JOIN sysobjects AS o ON i.id = o.id
  INNER JOIN INFORMATION_SCHEMA.TABLES T ON T.TABLE_NAME=o.name
 WHERE i.indid < 2
  AND OBJECTPROPERTY(o.id,'IsMSShipped') = 0
  AND i.rowcnt >350
  AND o.xtype !='TF'
 ORDER BY o.name ASC

 print @tablename

Puede obtener una lista de tablas que tiene recuentos de filas> 350. Puede ver en la lista de soluciones de la tabla como fila.

cunay
fuente
2

Solo porque no lo vi mencionado.

Si 2016+, aquí hay otra opción para desconectar dinámicamente los datos sin usar Dynamic SQL.

Ejemplo

Declare @YourTable Table ([ID] varchar(50),[Col1] varchar(50),[Col2] varchar(50))
Insert Into @YourTable Values 
 (1,'A','B')
,(2,'R','C')
,(3,'X','D')

Select A.[ID]
      ,Item  = B.[Key]
      ,Value = B.[Value]
 From  @YourTable A
 Cross Apply ( Select * 
                From  OpenJson((Select A.* For JSON Path,Without_Array_Wrapper )) 
                Where [Key] not in ('ID','Other','Columns','ToExclude')
             ) B

Devoluciones

ID  Item    Value
1   Col1    A
1   Col2    B
2   Col1    R
2   Col2    C
3   Col1    X
3   Col2    D
John Cappelletti
fuente