SQL Server: colonnes en lignes

129

Vous recherchez une solution élégante (ou toute autre) pour convertir des colonnes en lignes.

Voici un exemple: j'ai une table avec le schéma suivant:

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

Voici ce que je souhaite obtenir comme résultat:

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

Et les valeurs de résultat seront:

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'

Etc..

Est-ce que ça a du sens? Avez-vous des suggestions sur où chercher et comment le faire dans T-SQL?

Sergueï
la source
2
Avez-vous déjà examiné Pivot / Unpivot ?
Josh Jay
À la fin, c'est allé avec la solution du Bluefeet. Élégant et fonctionnel. Merci beaucoup à tous.
Sergei

Réponses:

248

Vous pouvez utiliser la fonction UNPIVOT pour convertir les colonnes en lignes:

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

Notez que les types de données des colonnes que vous annulez doivent être les mêmes, vous devrez peut-être convertir les types de données avant d'appliquer le pivotement.

Vous pouvez également utiliser CROSS APPLYavec UNION ALL pour convertir les colonnes:

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

Selon votre version de SQL Server, vous pouvez même utiliser CROSS APPLY avec la clause VALUES:

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

Enfin, si vous avez 150 colonnes à décompresser et que vous ne souhaitez pas coder en dur la requête entière, vous pouvez générer l'instruction SQL à l'aide de SQL dynamique:

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
la source
4
Pour ceux qui veulent plus d'écrous et de boulons sur UNPIVOTet / vs. APPLY, ce billet de blog 2010 de Brad Schulz (et la suite ) est (sont) magnifique.
ruffin
2
Msg 8167, niveau 16, état 1, ligne 147 Le type de colonne "blabla" entre en conflit avec le type des autres colonnes spécifiées dans la liste UNPIVOT.
JDPeckham
@JDPeckham Si vous avez différents types de données, vous devez les convertir pour qu'ils soient du même type et de la même longueur avant d'effectuer le décompresseur. Voici plus d'informations à ce sujet .
Taryn
la méthode xml a un défaut car elle ne parvient pas à échapper aux codes xml comme & gt ;, & lt; et & amp;. De plus, les performances peuvent être considérablement améliorées en réécrivant comme suit: sélectionnez @colsUnpivot = stuff ((select ',' + quotename (C.column_name) as [text ()] from information_schema.columns as C où C.table_name = 'yourtable' et C.column_name comme 'Indicator%' pour xml path (''), type) .value ('text () [1]', 'nvarchar (max)'), 1, 1, '')
rrozema
24

Eh bien, si vous avez 150 colonnes, je pense que UNPIVOT n'est pas une option. Vous pouvez donc utiliser une astuce 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

Vous pouvez également écrire du SQL dynamique, mais j'aime davantage le XML - pour le SQL dynamique, vous devez avoir les autorisations pour sélectionner des données directement à partir de la table et ce n'est pas toujours une option.

MISE
À JOUR Comme il y a une grande flamme dans les commentaires, je pense que je vais ajouter quelques avantages et inconvénients de xml / SQL dynamique. J'essaierai d'être aussi objectif que possible et de ne pas mentionner l'élégance et la laideur. Si vous avez d'autres avantages et inconvénients, modifiez la réponse ou écrivez dans les commentaires

les inconvénients

  • ce n'est pas aussi rapide que le SQL dynamique, des tests approximatifs m'ont montré que xml est environ 2,5 fois plus lent que dynamique (c'était une requête sur une table d'environ 250000 lignes, donc cette estimation n'est pas exacte). Vous pouvez le comparer vous-même si vous le souhaitez, voici l' exemple de sqlfiddle , sur 100000 lignes, c'était 29s (xml) contre 14s (dynamique);
  • peut-être que cela pourrait être plus difficile à comprendre pour les personnes qui ne sont pas familières avec xpath;

avantages

  • c'est la même portée que vos autres requêtes, et cela pourrait être très pratique. Quelques exemples me viennent à l'esprit
    • vous pouvez interroger insertedet deletedtables à l'intérieur de votre déclencheur (pas possible du tout avec dynamique);
    • l'utilisateur n'a pas besoin d'avoir des autorisations sur la sélection directe dans la table. Ce que je veux dire, c'est que si vous avez une couche de procédures stockées et que l'utilisateur a les autorisations pour exécuter sp, mais que vous n'avez pas les autorisations pour interroger les tables directement, vous pouvez toujours utiliser cette requête dans la procédure stockée;
    • vous pouvez interroger la variable de table que vous avez remplie dans votre portée (pour la transmettre à l'intérieur du SQL dynamique, vous devez soit en faire une table temporaire à la place, soit créer un type et le passer en tant que paramètre dans le SQL dynamique;
  • vous pouvez faire cette requête dans la fonction (scalaire ou table). Il n'est pas possible d'utiliser du SQL dynamique dans les fonctions;
Roman Pekar
la source
2
Quelles données sélectionnez-vous avec XML qui ne nécessitent pas la sélection de données dans le tableau?
Aaron Bertrand
1
Par exemple, vous pouvez décider de ne pas donner aux utilisateurs l'autorisation de sélectionner des données dans des tables, mais uniquement sur des procédures stockées travaillant avec des tables, afin que je puisse sélectionner pour xml dans la procédure, mais je dois utiliser certaines solutions de contournement si je veux utiliser SQL dynamique
Roman Pekar
3
Si vous voulez que vos utilisateurs puissent exécuter le code, vous devez leur donner l'accès dont ils ont besoin pour exécuter le code. N'inventez pas d'exigences qui n'existent pas pour que votre réponse soit meilleure (vous n'avez pas non plus à commenter les réponses concurrentes pour regarder votre réponse - s'ils ont trouvé cette réponse, ils peuvent aussi trouver la vôtre).
Aaron Bertrand
2
De plus, si votre justification de l'utilisation de XML est que vous pouvez le placer dans une procédure stockée pour éviter de donner un accès direct à la table, peut-être que votre exemple devrait montrer comment le mettre dans une procédure stockée et comment accorder des droits à un utilisateur afin qu'il peut l'exécuter sans avoir accès en lecture à la table sous-jacente. Pour moi, c'est un fluage de la portée, car la plupart des personnes qui écrivent des requêtes sur une table ont un accès en lecture à la table.
Aaron Bertrand
2
Je dirais qu'une différence de durée 10x importe, oui. Et environ 8 000 lignes ne sont pas de «grandes quantités de données» - devrions-nous voir ce qui se passe pour 800 000 lignes?
Aaron Bertrand
7

Juste pour aider les nouveaux lecteurs, j'ai créé un exemple pour mieux comprendre la réponse de @ bluefeet à propos d'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
la source
3

J'avais besoin d'une solution pour convertir des colonnes en lignes dans Microsoft SQL Server, sans connaître les noms de colonne (utilisés dans le déclencheur) et sans SQL dynamique (SQL dynamique est trop lent pour être utilisé dans un déclencheur).

J'ai finalement trouvé cette solution, qui fonctionne très 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)

Comme vous pouvez le voir, je convertis la ligne en XML (sous-requête sélectionnez i, * pour xml raw, cela convertit toutes les colonnes en une colonne xml)

Ensuite, j'APPLIQUE une fonction à chaque attribut XML de cette colonne, de sorte que j'obtienne une ligne par attribut.

Dans l'ensemble, cela convertit les colonnes en lignes, sans connaître les noms de colonne et sans utiliser SQL dynamique. C'est assez rapide pour mon objectif.

(Edit: je viens de voir la réponse de Roman Pekar ci-dessus, qui fait la même chose. J'ai d'abord utilisé le déclencheur SQL dynamique avec les curseurs, qui était 10 à 100 fois plus lent que cette solution, mais peut-être que cela a été causé par le curseur, pas par le SQL dynamique. Quoi qu'il en soit, cette solution est très simple et universelle, donc c'est définitivement une option).

Je laisse ce commentaire à cet endroit, car je souhaite faire référence à cette explication dans mon article sur le déclencheur d'audit complet, que vous pouvez trouver ici: https://stackoverflow.com/a/43800286/4160788

flack
la source
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

Vous pouvez obtenir la liste des tables dont le nombre de lignes est supérieur à 350. Vous pouvez voir la liste des solutions de la table sous forme de ligne.

cunay
la source
2

Juste parce que je ne l'ai pas vu mentionné.

Si 2016+, voici encore une autre option pour décompresser dynamiquement les données sans réellement utiliser Dynamic SQL.

Exemple

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

Retour

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