Un moyen simple de transposer des colonnes et des lignes en SQL?

110

Comment changer simplement de colonne avec des lignes en SQL? Y a-t-il une commande simple à transposer?

ie tourner ce résultat:

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

dans ceci:

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

PIVOT semble trop complexe pour ce scénario.

Edezzie
la source
1
stackoverflow.com/questions/10699997/… est une question quelque peu similaire.
Tim Dearborn

Réponses:

145

Il existe plusieurs manières de transformer ces données. Dans votre article d'origine, vous avez déclaré que cela PIVOTsemble trop complexe pour ce scénario, mais cela peut être appliqué très facilement à l'aide des fonctions UNPIVOTetPIVOT de SQL Server.

Cependant, si vous n'avez pas accès à ces fonctions, cela peut être répliqué à l'aide de UNION ALLto UNPIVOT, puis d'une fonction d'agrégation avec une CASEinstruction to PIVOT:

Créer une table:

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

Version Union All, Agrégat et 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

Voir SQL Fiddle avec démo

Le UNION ALLexécute la UNPIVOTdes données en transformant les colonnes Paul, John, Tim, Ericen lignes séparées. Ensuite, vous appliquez la fonction d'agrégation sum()avec l' caseinstruction pour obtenir les nouvelles colonnes pour chacun color.

Unpivot et la version statique du pivot:

Les fonctions UNPIVOTet PIVOTdu serveur SQL rendent cette transformation beaucoup plus facile. Si vous connaissez toutes les valeurs que vous souhaitez transformer, vous pouvez les coder en dur dans une version statique pour obtenir le résultat:

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

Voir SQL Fiddle avec démo

La requête interne avec le UNPIVOTexécute la même fonction que le UNION ALL. Il prend la liste des colonnes et la transforme en lignes, PIVOTpuis effectue la transformation finale en colonnes.

Version du pivot dynamique:

Si vous avez un nombre inconnu de colonnes ( Paul, John, Tim, Ericdans votre exemple), puis un nombre inconnu de couleurs à transformer, vous pouvez utiliser SQL dynamique pour générer la liste vers UNPIVOTet ensuite 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)

Voir SQL Fiddle avec démo

La version dynamique interroge les deux yourtable, puis la sys.columnstable pour générer la liste des éléments vers UNPIVOTet PIVOT. Ceci est ensuite ajouté à une chaîne de requête à exécuter. Le plus de la version dynamique est si vous avez une liste changeante de colorset / ou namescela générera la liste au moment de l'exécution.

Les trois requêtes produiront le même résultat:

| NAME | RED | GREEN | BLUE |
-----------------------------
| Eric |   3 |     5 |    1 |
| John |   5 |     4 |    2 |
| Paul |   1 |     8 |    2 |
|  Tim |   1 |     3 |    9 |
Taryn
la source
J'aurais dû souligner que je souhaite une méthode pour transposer les résultats d'une requête sur une base de données. Une commande de type transposition simple qui peut être exécutée sur l'ensemble de résultats d'une requête sans avoir à rien savoir sur la requête elle-même, ou sur les colonnes, les tables, etc. dont elle extrait les informations. Quelque chose comme: (TRANSPOSE (SELECT ......)) Dans mon exemple donné ci-dessus, imaginez que la première table est un jeu de résultats généré à partir d'une requête très complexe impliquant plusieurs tables, unions, pivots etc. Pourtant, le résultat est un tableau simple. Je veux juste retourner les colonnes avec des lignes dans ce résultat.
edezzie
Je passe le résultat à ac # DataTable. Je peux créer une méthode simple 'Transpose (Datatable dt)' pour le faire là-bas, mais y a-t-il quelque chose dans SQL pour le faire.
edezzie
@edezzie il n'y a pas de moyen simple en SQL de le faire. Vous pourriez probablement modifier la version dynamique pour passer le nom de la table et extraire les informations des tables système.
Taryn
Y a-t-il une raison pour laquelle vous n'avez pas signalé cette brillante réponse comme RÉPONSE? Donnez une médaille à @bluefeet!
Fandango68
Cela fonctionne bien select * from Table unpivot (value for name in ([Paul], [John], [Tim], [Eric])) up pivot (max (value) for color in ([Red], [Green]) , [Bleu])) p mais est-il possible d'écrire cette sélection * à partir du tableau unpivot (valeur du nom dans ([Paul], [John], [Tim], [Eric])) pivot vers le haut (max (valeur) pour color in (SELECT COLOR FROM TABLENAME)) p au lieu de saisir une valeur entre guillemets, ne peut pas récupérer les valeurs directement à l'aide de la requête de sélection.
Mogli
20

Cela nécessite normalement que vous connaissiez au préalable TOUTES les étiquettes de colonne ET de ligne. Comme vous pouvez le voir dans la requête ci-dessous, les étiquettes sont toutes répertoriées dans leur intégralité dans les opérations UNPIVOT et (re) PIVOT.

Configuration du schéma 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;

Requête 1 :

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

Résultats :

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

Notes complémentaires:

  1. Étant donné un nom de table, vous pouvez déterminer tous les noms de colonne à partir de sys.columns ou de la supercherie FOR XML en utilisant local-name () .
  2. Vous pouvez également créer la liste des couleurs distinctes (ou des valeurs pour une colonne) à l'aide de FOR XML.
  3. Ce qui précède peut être combiné dans un lot SQL dynamique pour gérer n'importe quelle table.
RichardTheKiwi
la source
11

Je voudrais souligner quelques autres solutions pour transposer des colonnes et des lignes en SQL.

Le premier est - en utilisant CURSOR. Bien que le consensus général dans la communauté professionnelle soit de rester à l'écart des curseurs SQL Server, il existe encore des cas dans lesquels l'utilisation de curseurs est recommandée. Quoi qu'il en soit, les curseurs nous présentent une autre option pour transposer les lignes en colonnes.

  • Expansion verticale

    Semblable au PIVOT, le curseur a la capacité dynamique d'ajouter plus de lignes à mesure que votre ensemble de données se développe pour inclure plus de numéros de stratégie.

  • Expansion horizontale

    Contrairement au PIVOT, le curseur excelle dans ce domaine car il est capable de s'étendre pour inclure le document nouvellement ajouté, sans modifier le script.

  • Répartition des performances

    La principale limitation de la transposition de lignes en colonnes à l'aide de CURSOR est un inconvénient lié à l'utilisation des curseurs en général - ils ont un coût de performance significatif. Cela est dû au fait que le curseur génère une requête distincte pour chaque opération FETCH NEXT.

Une autre solution de transposition de lignes en colonnes consiste à utiliser XML.

La solution XML pour transposer des lignes en colonnes est fondamentalement une version optimale du PIVOT en ce qu'elle résout la limitation dynamique des colonnes.

La version XML du script résout cette limitation en utilisant une combinaison de chemin XML, de T-SQL dynamique et de certaines fonctions intégrées (c'est-à-dire STUFF, QUOTENAME).

  • Expansion verticale

    À l'instar du PIVOT et du curseur, les stratégies nouvellement ajoutées peuvent être récupérées dans la version XML du script sans modifier le script d'origine.

  • Expansion horizontale

    Contrairement au PIVOT, les documents nouvellement ajoutés peuvent être affichés sans modifier le script.

  • Répartition des performances

    En termes d'E / S, les statistiques de la version XML du script sont presque similaires à celles du PIVOT - la seule différence est que le XML a un deuxième scan de la table dtTranspose mais cette fois à partir d'un cache logique de lecture de données.

Vous pouvez trouver plus d'informations sur ces solutions (y compris des exemples réels de T-SQL) dans cet article: https://www.sqlshack.com/multiple-options-to-transposing-rows-into-columns/

MikeS
la source
8

Sur la base de cette solution de bluefeet, voici une procédure stockée qui utilise SQL dynamique pour générer la table transposée. Il faut que tous les champs soient numériques à l'exception de la colonne transposée (la colonne qui sera l'en-tête dans le tableau résultant):

/****** 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

Vous pouvez le tester avec le tableau fourni avec cette commande:

exec SQLTranspose 'yourTable', 'color'
Paco Zarate
la source
argg. mais j'en veux un où tous les champs sont nvarchar (max)
hamish
1
Pour tous les champs nvarchar (max), il suffit de changer la somme (valeur) avec max (valeur) dans la 6ème ligne à partir de la fin
Paco Zarate
2

Je fais d' UnPivotabord et stocke les résultats dans CTEet utilise le CTEen Pivotfonctionnement.

Démo

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

En ajoutant à la réponse formidable de @Paco Zarate ci-dessus, si vous souhaitez transposer un tableau qui a plusieurs types de colonnes, ajoutez-le à la fin de la ligne 39, de sorte qu'il ne transpose que les intcolonnes:

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

Voici la requête complète en cours de modification:

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, '')

Pour en trouver d'autres system_type_id, exécutez ceci:

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

J'aime partager le code que j'utilise pour transposer un texte fractionné basé sur la réponse + bluefeet. Dans cette approche, je suis implémenté en tant que procédure dans 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

Je mélange cette solution avec les informations sur la façon de classer les lignes sans ordre par ( SQLAuthority.com ) et la fonction de fractionnement sur MSDN ( social.msdn.microsoft.com )

Lorsque vous exécutez la 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

vous obtenez le résultat suivant

  name       rowid  1      2          3
  col_value  1      hello  beautiful  world
Elidio Marquina
la source
1

De cette façon, convertissez toutes les données des fichiers (colonnes) dans le tableau en enregistrement (ligne).

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

J'ai pu utiliser la solution de Paco Zarate et cela fonctionne à merveille. J'ai dû ajouter une ligne ("SET ANSI_WARNINGS ON"), mais cela peut être quelque chose d'unique à la façon dont je l'ai utilisé ou appelé. Il y a un problème avec mon utilisation et j'espère que quelqu'un pourra m'aider:

La solution fonctionne uniquement avec une table SQL réelle. Je l'ai essayé avec une table temporaire et aussi une table en mémoire (déclarée) mais cela ne fonctionne pas avec ceux-ci. Donc, dans mon code d'appel, je crée une table sur ma base de données SQL, puis j'appelle SQLTranspose. Encore une fois, cela fonctionne très bien. C'est juste ce que je veux. Voici mon problème:

Pour que la solution globale soit vraiment dynamique, je dois créer cette table où je stocke temporairement les informations préparées que j'envoie à SQLTranspose "à la volée", puis supprimer cette table une fois que SQLTranspose est appelé. La suppression de la table pose un problème avec mon plan de mise en œuvre ultime. Le code doit s'exécuter à partir d'une application utilisateur final (un bouton sur un formulaire / menu Microsoft Access). Lorsque j'utilise ce processus SQL (créer une table SQL, appeler SQLTranspose, supprimer une table SQL), l'application utilisateur final rencontre une erreur car le compte SQL utilisé n'a pas le droit de supprimer une table.

Je pense donc qu'il y a quelques solutions possibles:

  1. Trouvez un moyen de faire fonctionner SQLTranspose avec une table temporaire ou une variable de table déclarée.

  2. Trouvez une autre méthode pour la transposition des lignes et des colonnes qui ne nécessite pas une table SQL réelle.

  3. Trouvez une méthode appropriée pour permettre au compte SQL utilisé par mes utilisateurs finaux de supprimer une table. C'est un seul compte SQL partagé codé dans mon application Access. Il semble que l'autorisation soit un privilège de type dbo qui ne peut pas être accordé.

Je reconnais que certains de ces éléments peuvent justifier un autre fil et une question distincts. Cependant, comme il y a une possibilité qu'une solution puisse être simplement une manière différente de faire la transposition des lignes et des colonnes, je ferai mon premier message ici dans ce fil.

EDIT: J'ai également remplacé sum (valeur) par max (valeur) dans la 6ème ligne à partir de la fin, comme Paco l'a suggéré.

ÉDITER:

J'ai trouvé quelque chose qui fonctionne pour moi. Je ne sais pas si c'est la meilleure réponse ou non.

J'ai un compte d'utilisateur en lecture seule qui est utilisé pour exécuter des procédures strictes et donc générer des résultats de rapport à partir d'une base de données. Étant donné que la fonction SQLTranspose que j'ai créée ne fonctionnera qu'avec une table "légitime" (pas une table déclarée ni une table temporaire), j'ai dû trouver un moyen pour un compte utilisateur en lecture seule de créer (puis de supprimer plus tard) une table .

J'ai pensé que pour mes besoins, il était normal que le compte utilisateur soit autorisé à créer une table. L'utilisateur n'a toujours pas pu supprimer la table. Ma solution était de créer un schéma où le compte utilisateur est autorisé. Ensuite, chaque fois que je crée, utilise ou supprime cette table, faites-la référence avec le schéma spécifié.

J'ai d'abord émis cette commande à partir d'un compte 'sa' ou 'sysadmin': CREATE SCHEMA ro AUTHORIZATION

À chaque fois que je me réfère à ma table "tmpoutput", je le spécifie comme cet exemple:

déposer la table ro.tmpoutput

CraigD
la source
J'ai trouvé quelque chose qui fonctionne pour moi. Je ne sais pas si c'est la meilleure réponse ou non.
CraigD