Un modo semplice per trasporre colonne e righe in SQL?


110

Come faccio a cambiare semplicemente le colonne con le righe in SQL? C'è qualche semplice comando da trasporre?

cioè trasforma questo risultato:

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

in questo:

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

PIVOT sembra troppo complesso per questo scenario.


1
stackoverflow.com/questions/10699997/… è una domanda in qualche modo simile.
Tim Dearborn

Risposte:


145

Esistono diversi modi per trasformare questi dati. Nel tuo post originale, hai affermato che PIVOTsembra troppo complesso per questo scenario, ma può essere applicato molto facilmente utilizzando sia il UNPIVOTchePIVOT funzioni in SQL Server.

Tuttavia, se non si ha accesso a tali funzioni, è possibile replicarlo utilizzando UNION ALLa UNPIVOTe quindi una funzione di aggregazione con CASEun'istruzione a PIVOT:

Crea tabella:

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

Versione Union All, Aggregate e 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

Vedi SQL Fiddle con Demo

Il UNION ALLesegue il UNPIVOTdei dati trasformando le colonne Paul, John, Tim, Ericin righe separate. Quindi si applica la funzione di aggregazione sum()con l' caseistruzione per ottenere le nuove colonne per ciascuna color.

Versione statica unpivot e pivot:

Sia l' UNPIVOTe PIVOTfunzioni in SQL Server rendono questa trasformazione molto più facile. Se conosci tutti i valori che vuoi trasformare, puoi codificarli come hardcoded in una versione statica per ottenere il risultato:

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

Vedi SQL Fiddle con Demo

La query interna con UNPIVOTesegue la stessa funzione di UNION ALL. Prende l'elenco delle colonne e lo trasforma in righe, PIVOTquindi esegue la trasformazione finale in colonne.

Versione pivot dinamica:

Se hai un numero sconosciuto di colonne ( Paul, John, Tim, Ericnel tuo esempio) e quindi un numero sconosciuto di colori da trasformare, puoi utilizzare sql dinamico per generare l'elenco UNPIVOTe quindi 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)

Vedi SQL Fiddle con Demo

La versione dinamica interroga entrambi yourtablee quindi la sys.columnstabella per generare l'elenco di elementi in UNPIVOTe PIVOT. Questo viene quindi aggiunto a una stringa di query da eseguire. Il vantaggio della versione dinamica è se si dispone di un elenco di modifiche colorse / o namesquesto genererà l'elenco in fase di esecuzione.

Tutte e tre le query produrranno lo stesso risultato:

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

Avrei dovuto sottolineare che desidero un metodo per trasporre i risultati di una query su un database. Un semplice comando di tipo di trasposizione che può essere eseguito sul set di risultati da una query senza dover sapere nulla sulla query stessa, o sulle colonne, tabelle ecc. Da cui estrae informazioni. Qualcosa come: (TRANSPOSE (SELECT ......)) Nel mio esempio sopra riportato, immagina che la prima tabella sia un set di risultati generato da una query molto complessa che coinvolge più tabelle, unioni, pivot ecc. Tuttavia il risultato è un tavolo semplice. Voglio solo capovolgere le colonne con le righe in questo risultato.
edezzie

Sto passando il risultato a ac # DataTable. Posso costruire un semplice metodo 'Transpose (Datatable dt)' per farlo lì, ma c'è qualcosa in SQL per farlo.
edezzie

@edezzie non esiste un modo semplice in SQL per farlo. Probabilmente potresti modificare la versione dinamica per passare il nome della tabella ed estrarre le informazioni dalle tabelle di sistema.
Taryn

C'è qualche motivo per cui non hai contrassegnato questa brillante risposta come RISPOSTA? Dai una medaglia a @bluefeet!
Fandango68

Funziona bene seleziona * da Table unpivot (value for name in ([Paul], [John], [Tim], [Eric])) up pivot (max (value) for color in ([Red], [Green] , [Blue])) p ma è possibile scrivere questo select * da Table unpivot (value for name in ([Paul], [John], [Tim], [Eric])) up pivot (max (value) for color in (SELECT COLOR FROM TABLENAME)) p invece di inserire il valore tra virgolette non può recuperare i valori direttamente utilizzando la query di selezione.
Mogli

20

Ciò normalmente richiede di conoscere in anticipo TUTTE le etichette di colonna E di riga. Come puoi vedere nella query sottostante, le etichette sono tutte elencate nella loro interezza sia nelle operazioni UNPIVOT che in (ri) PIVOT.

Configurazione dello schema di 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;

Domanda 1 :

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

Risultati :

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

Note aggiuntive:

  1. Dato un nome di tabella, è possibile determinare tutti i nomi di colonna da sys.columns o dal trucco FOR XML utilizzando local-name () .
  2. È inoltre possibile creare l'elenco di colori distinti (o valori per una colonna) utilizzando FOR XML.
  3. Quanto sopra può essere combinato in un batch SQL dinamico per gestire qualsiasi tabella.

11

Vorrei sottolineare alcune altre soluzioni per la trasposizione di colonne e righe in SQL.

Il primo è usare CURSOR. Sebbene il consenso generale nella comunità professionale sia quello di stare alla larga dai cursori di SQL Server, esistono ancora casi in cui è consigliato l'uso dei cursori. Ad ogni modo, i cursori ci presentano un'altra opzione per trasporre le righe in colonne.

  • Espansione verticale

    Simile al PIVOT, il cursore ha la capacità dinamica di aggiungere più righe man mano che il set di dati si espande per includere più numeri di criteri.

  • Espansione orizzontale

    A differenza del PIVOT, il cursore eccelle in quest'area in quanto è in grado di espandersi per includere il documento appena aggiunto, senza alterare lo script.

  • Ripartizione delle prestazioni

    La principale limitazione della trasposizione di righe in colonne utilizzando CURSOR è uno svantaggio legato all'uso dei cursori in generale: hanno un costo in termini di prestazioni significativo. Questo perché il Cursore genera una query separata per ciascuna operazione FETCH NEXT.

Un'altra soluzione per trasporre le righe in colonne consiste nell'usare XML.

La soluzione XML per trasporre le righe in colonne è fondamentalmente una versione ottimale del PIVOT in quanto affronta la limitazione dinamica delle colonne.

La versione XML dello script affronta questa limitazione utilizzando una combinazione di XML Path, T-SQL dinamico e alcune funzioni incorporate (es. STUFF, QUOTENAME).

  • Espansione verticale

    Analogamente al PIVOT e al Cursore, i criteri appena aggiunti possono essere recuperati nella versione XML dello script senza alterare lo script originale.

  • Espansione orizzontale

    A differenza del PIVOT, i documenti appena aggiunti possono essere visualizzati senza alterare lo script.

  • Ripartizione delle prestazioni

    In termini di IO, le statistiche della versione XML dello script sono quasi simili al PIVOT: l'unica differenza è che l'XML ha una seconda scansione della tabella dtTranspose ma questa volta da una cache di lettura logica dei dati.

Puoi trovare ulteriori informazioni su queste soluzioni (inclusi alcuni esempi T-SQL effettivi) in questo articolo: https://www.sqlshack.com/multiple-options-to-transposing-rows-into-columns/


8

Sulla base di questa soluzione di bluefeet, ecco una stored procedure che utilizza sql dinamico per generare la tabella trasposta. Richiede che tutti i campi siano numerici ad eccezione della colonna trasposta (la colonna che sarà l'intestazione nella tabella risultante):

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

Puoi testarlo con la tabella fornita con questo comando:

exec SQLTranspose 'yourTable', 'color'

argg. ma ne voglio uno in cui tutti i campi siano nvarchar (max)
hamish

1
Per tutti i campi nvarchar (max) basta cambiare la somma (valore) con max (valore) nella sesta riga dalla fine
Paco Zarate

2

Sto facendo UnPivotprima e memorizzare i risultati nel CTEe con il CTEdi Pivotfunzionamento.

dimostrazione

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

Aggiungendo alla fantastica risposta di @Paco Zarate sopra, se vuoi trasporre una tabella che ha più tipi di colonne, aggiungilo alla fine della riga 39, quindi traspone solo le intcolonne:

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

Ecco la query completa che viene modificata:

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

Per trovare altri system_type_id, esegui questo:

select name, system_type_id from sys.types order by name

1

Mi piace condividere il codice che sto usando per trasporre un testo diviso in base alla risposta + bluefeet. In questo approccio sono implementato come procedura in 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

Sto mescolando questa soluzione con le informazioni su come ordinare le righe senza ordine di ( SQLAuthority.com ) e la funzione di divisione su MSDN ( social.msdn.microsoft.com )

Quando esegui il 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

si ottiene il risultato successivo

  name       rowid  1      2          3
  col_value  1      hello  beautiful  world

1

In questo modo converti tutti i dati da campi (colonne) nella tabella in record (riga).

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

Sono stato in grado di utilizzare la soluzione di Paco Zarate e funziona magnificamente. Ho dovuto aggiungere una riga ("SET ANSI_WARNINGS ON"), ma potrebbe essere qualcosa di unico nel modo in cui l'ho usata o chiamata. C'è un problema con il mio utilizzo e spero che qualcuno possa aiutarmi con esso:

La soluzione funziona solo con una tabella SQL effettiva. L'ho provato con una tabella temporanea e anche una tabella in memoria (dichiarata) ma non funziona con quelle. Quindi nel mio codice chiamante creo una tabella sul mio database SQL e quindi chiamo SQLTranspose. Ancora una volta, funziona alla grande. È proprio quello che voglio. Ecco il mio problema:

Affinché la soluzione complessiva sia veramente dinamica, ho bisogno di creare quella tabella in cui memorizzo temporaneamente le informazioni preparate che sto inviando a SQLTranspose "al volo", quindi eliminare quella tabella una volta chiamato SQLTranspose. L'eliminazione della tabella presenta un problema con il mio piano di implementazione finale. Il codice deve essere eseguito da un'applicazione dell'utente finale (un pulsante in un form / menu di Microsoft Access). Quando utilizzo questo processo SQL (creare una tabella SQL, chiamare SQLTranspose, eliminare la tabella SQL) l'applicazione dell'utente finale rileva un errore perché l'account SQL utilizzato non dispone dei diritti per eliminare una tabella.

Quindi immagino che ci siano alcune possibili soluzioni:

  1. Trova un modo per far funzionare SQLTranspose con una tabella temporanea o una variabile di tabella dichiarata.

  2. Scopri un altro metodo per la trasposizione di righe e colonne che non richiede una tabella SQL effettiva.

  3. Individuare un metodo appropriato per consentire all'account SQL utilizzato dai miei utenti finali di eliminare una tabella. È un singolo account SQL condiviso codificato nella mia applicazione Access. Sembra che l'autorizzazione sia un privilegio di tipo dbo che non può essere concesso.

Riconosco che alcuni di questi possono giustificare un altro thread e domanda separati. Tuttavia, poiché esiste la possibilità che una soluzione possa essere semplicemente un modo diverso per eseguire la trasposizione di righe e colonne, farò il mio primo post qui in questo thread.

EDIT: Ho anche sostituito sum (value) con max (value) nella sesta riga dalla fine, come suggerito da Paco.

MODIFICARE:

Ho scoperto qualcosa che funziona per me. Non so se sia il migliore risposta oppure no.

Ho un account utente di sola lettura che viene utilizzato per eseguire procedure strored e quindi generare output di report da un database. Poiché la funzione SQLTranspose che ho creato funzionerà solo con una tabella "legittima" (non una tabella dichiarata e non una tabella temporanea) ho dovuto trovare un modo per un account utente di sola lettura per creare (e poi eliminare) una tabella .

Ho pensato che per i miei scopi va bene che l'account utente possa creare una tabella. L'utente comunque non poteva eliminare la tabella. La mia soluzione era creare uno schema in cui l'account utente fosse autorizzato. Quindi, ogni volta che creo, uso o elimino quella tabella, la riferisco con lo schema specificato.

Ho emesso questo comando per la prima volta da un account 'sa' o 'sysadmin': CREATE SCHEMA ro AUTHORIZATION

Ogni volta che mi riferisco alla mia tabella "tmpoutput" lo specifico come in questo esempio:

drop table ro.tmpoutput


Ho scoperto qualcosa che funziona per me. Non so se sia la risposta migliore oppure no.
CraigD
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.