web-dev-qa-db-fra.com

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

Comment changer simplement de colonne avec des lignes en SQL?

c'est à dire 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.

88
edezzie

Vous pouvez transformer ces données de plusieurs manières. Dans votre message d'origine, vous déclariez que PIVOT semblait trop complexe pour ce scénario, mais il peut être appliqué très facilement à l'aide des fonctions UNPIVOT ET PIVOT dans SQL Server. 

Toutefois, si vous n'avez pas accès à ces fonctions, vous pouvez le répliquer à l'aide de UNION ALL en UNPIVOT, puis d'une fonction d'agrégat avec une instruction CASE en PIVOT:

Créer un tableau:

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

Union All, version agrégée 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 Demo

Le UNION ALL exécute la UNPIVOT des données en transformant les colonnes Paul, John, Tim, Eric en lignes séparées. Ensuite, vous appliquez la fonction d'agrégation sum() à l'instruction case pour obtenir les nouvelles colonnes pour chaque color.

Version statique et pivots croisés:

Les fonctions UNPIVOT et PIVOT de SQL Server facilitent beaucoup cette transformation. 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 Demo

La requête interne avec UNPIVOT remplit la même fonction que UNION ALL. Il prend la liste des colonnes et la transforme en lignes, la PIVOT effectue ensuite la transformation finale en colonnes.

Version de pivot dynamique:

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

La version dynamique interroge à la fois yourtable et ensuite la table sys.columns pour générer la liste des éléments à UNPIVOT et PIVOT. Ceci est ensuite ajouté à une chaîne de requête à exécuter. Le plus de la version dynamique est que si vous avez une liste changeante de colors et/ou names, cela générera la liste au moment de l'exécution.

Les trois requêtes donneront 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 |
120
Taryn

Cela nécessite normalement de connaître TOUTES les étiquettes de colonne ET de ligne au préalable. Comme vous pouvez le voir dans la requête ci-dessous, les étiquettes sont toutes répertoriées dans leur intégralité à la fois 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 à l'aide de 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.
17
RichardTheKiwi

J'aimerais souligner quelques solutions supplémentaires pour la transposition de colonnes et de 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 est toujours recommandé d’utiliser des curseurs. Quoi qu'il en soit, les curseurs nous proposent une autre option pour transposer des lignes en colonnes.

  • Expansion verticale

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

  • Expansion horizontale

    Contrairement au PIVOT, le curseur excelle dans cette zone car il peut être étendu pour inclure le document ajouté récemment, sans modifier le script.

  • Ventilation des performances

    La principale limitation de la transposition de lignes en colonnes à l'aide de CURSOR est un inconvénient lié à l'utilisation de curseurs en général: leur coût de performance est considérable. En effet, 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 la transposition de lignes en colonnes est fondamentalement une version optimale du PIVOT dans la mesure où elle aborde la limitation dynamique des colonnes.

La version XML du script corrige cette limitation en combinant chemin XML, T-SQL dynamique et certaines fonctions intégrées (c'est-à-dire STUFF, QUOTENAME).

  • Expansion verticale

    Semblables au PIVOT et au Curseur, les nouvelles politiques 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 nouveaux documents ajoutés peuvent être affichés sans modifier le script.

  • Ventilation 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 une seconde analyse de la table dtTranspose, mais cette fois à partir d'un cache logique de lecture de données.

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

10
MikeS

Basé sur cette solution from bluefeet , voici une procédure stockée qui utilise SQL dynamique pour générer la table transposée. Il nécessite que tous les champs soient numériques sauf la colonne transposée (la colonne qui sera l'en-tête dans la table résultante):

/****** 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 la table fournie avec cette commande:

exec SQLTranspose 'yourTable', 'color'
7
Paco Zarate

Je fais d'abord UnPivot et stocke les résultats dans CTE et j'utilise l'opération CTE dans Pivot.

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;
2
mr_eclair

En ajoutant à la réponse formidable de @Paco Zarate ci-dessus, si vous souhaitez transposer une table comportant plusieurs types de colonnes, ajoutez-la ensuite à la fin de la ligne 39, de sorte qu'elle ne transpose que les colonnes int:

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 trouver d'autres system_type_id, lancez ceci:

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

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é comme une 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 procédure à suivre pour ordonner les lignes sans ordre par ( SQLAuthority.com ) et la fonction de fractionnement sur MSDN ( social.msdn.Microsoft.com )

Quand vous exécutez le 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

tu obtiens le résultat suivant

  name       rowid  1      2          3
  col_value  1      hello  beautiful  world
1
ELD

De cette façon, convertissez toutes les données des champs (colonnes) de la table 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
0
Mahmood Khezrian

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

La solution ne fonctionne qu'avec une table SQL réelle. Je l'ai essayé avec une table temporaire et aussi une table (déclarée) en mémoire, 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 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 réellement dynamique, je dois créer cette table dans laquelle je stocke temporairement "à la volée" les informations préparées que j'envoie à SQLTranspose, puis je supprime 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 être exécuté à partir d'une application d'utilisateur final (un bouton d'un formulaire/menu Microsoft Access). Lorsque j'utilise ce processus SQL (créer une table SQL, appeler SQLTranspose, supprimer une table SQL), l'application de l'utilisateur final génère une erreur car le compte SQL utilisé ne dispose pas des droits nécessaires pour supprimer une table.

Je suppose donc qu'il existe plusieurs solutions possibles:

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

  2. Découvrez une autre méthode pour la transposition de lignes et de colonnes ne nécessitant pas de table SQL réelle.

  3. Trouvez une méthode appropriée permettant au compte SQL utilisé par mes utilisateurs finaux de supprimer une table. C'est un compte SQL partagé unique 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 certaines de ces choses peuvent justifier un autre fil séparé et une question. Cependant, comme il est possible qu'une solution soit simplement une façon différente de transposer des lignes et des colonnes, je ferai mon premier article ici dans ce fil de discussion.

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

MODIFIER:

J'ai compris quelque chose qui fonctionne pour moi. Je ne sais pas si c'est le le meilleur réponds ou non.

J'ai un compte utilisateur en lecture seule qui est utilisé pour exécuter des procédures strored et donc pour générer des rapports de sortie à partir d'une base de données. Comme 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), je devais trouver un moyen pour un compte utilisateur en lecture seule de créer (puis de supprimer) une table. .

Je pensais que pour mes besoins, il était normal que le compte d'utilisateur soit autorisé à créer une table. L'utilisateur n'a toujours pas pu supprimer la table. Ma solution consistait à créer un schéma dans lequel le compte d'utilisateur est autorisé. Ensuite, chaque fois que je crée, utilise ou supprime cette table, 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 fais référence à ma table "tmpoutput", je le spécifie comme suit:

déposer la table ro.tmpoutput

0
CraigD