SQL Server: colonne per righe


129

Alla ricerca di una soluzione elegante (o qualsiasi) per convertire colonne in righe.

Ecco un esempio: ho una tabella con il seguente schema:

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

Ecco cosa voglio ottenere come risultato:

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

E i valori del risultato saranno:

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'

E così via..

Questo ha senso? Hai qualche suggerimento su dove cercare e come farlo in T-SQL?


2
Hai già esaminato Pivot / Unpivot ?
Josh Jay,

Alla fine è andato con la soluzione del bluefeet. Elegante e funzionale Grazie mille a tutti
Sergei

Risposte:


248

È possibile utilizzare la funzione UNPIVOT per convertire le colonne in righe:

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

Nota, i tipi di dati delle colonne che non stai promuovendo devono essere gli stessi, quindi potresti dover convertire i tipi di dati prima di applicare il non perno.

È inoltre possibile utilizzare CROSS APPLYcon UNION ALL per convertire le colonne:

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

A seconda della versione di SQL Server è possibile utilizzare CROSS APPLY con la clausola VALUES:

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

Infine, se hai 150 colonne da annullare la rotazione e non vuoi codificare a fondo l'intera query, puoi generare l'istruzione sql usando SQL dinamico:

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;

4
Per coloro che vogliono più dadi e bulloni su UNPIVOTe / vs. APPLY, questo post del blog 2010 di Brad Schulz (e il seguito ) è (sono) bellissimo.
ruffin,

2
Messaggio 8167, livello 16, stato 1, riga 147 Il tipo di colonna "blahblah" è in conflitto con il tipo di altre colonne specificato nell'elenco UNPIVOT.
JDPeckham,

@JDPeckham Se si hanno tipi di dati diversi, è necessario convertirli per avere lo stesso tipo e lunghezza prima di eseguire il non perno. Ecco maggiori informazioni a riguardo .
Taryn

il metodo xml ha un difetto perché non riesce a ripristinare i codici XML come & gt ;, & lt; e & amp ;. Inoltre, le prestazioni possono essere notevolmente migliorate riscrivendo come segue: selezionare @colsUnpivot = stuff ((selezionare ',' + quotename (C.column_name) come [text ()] da information_schema.columns come C dove C.table_name = 'yourtable' e C.column_name come 'Indicator%' per xml path (''), digitare) .value ('text () [1]', 'nvarchar (max)'), 1, 1, '')
rrozema

24

bene Se hai 150 colonne, penso che UNPIVOT non sia un'opzione. Quindi potresti usare il trucco 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

Potresti anche scrivere SQL dinamico, ma mi piace di più xml: per SQL dinamico devi avere le autorizzazioni per selezionare i dati direttamente dalla tabella e non è sempre un'opzione.

AGGIORNAMENTO
Dato che c'è una grande fiamma nei commenti, penso che aggiungerò alcuni pro e contro di xml / SQL dinamico. Cercherò di essere il più obiettivo possibile e non menzionare l'eleganza e la bruttezza. Se hai altri pro e contro, modifica la risposta o scrivi nei commenti

cons

  • non è veloce come l'SQL dinamico, i test approssimativi mi hanno dato che xml è circa 2,5 volte più lento di quello dinamico (era una query sulla tabella ~ 250000 righe, quindi questa stima non è esattamente esatta). Puoi confrontarlo tu stesso se vuoi, ecco l' esempio sqlfiddle , su 100000 righe era 29s (xml) contro 14s (dinamico);
  • potrebbe essere potrebbe essere più difficile da capire per le persone che non hanno familiarità con xpath;

professionisti

  • è lo stesso ambito delle altre query e potrebbe essere molto utile. Alcuni esempi vengono in mente
    • è possibile eseguire query insertede deletedtabelle all'interno del trigger (impossibile con la dinamica);
    • l'utente non deve disporre delle autorizzazioni per la selezione diretta dalla tabella. Quello che voglio dire è se hai un livello di procedure memorizzate e l'utente ha le autorizzazioni per eseguire sp, ma non hai le autorizzazioni per eseguire query direttamente sulle tabelle, puoi comunque utilizzare questa query all'interno della procedura memorizzata;
    • potresti interrogare la variabile di tabella che hai popolato nel tuo ambito (per passarla all'interno di SQL dinamico devi invece renderla tabella temporanea o creare un tipo e passarlo come parametro in SQL dinamico;
  • è possibile eseguire questa query all'interno della funzione (scalare o con valori di tabella). Non è possibile utilizzare SQL dinamico all'interno delle funzioni;

2
Quali dati stai selezionando con XML che non richiedono la selezione dei dati dalla tabella?
Aaron Bertrand,

1
Ad esempio, potresti decidere di non concedere agli utenti le autorizzazioni per selezionare i dati dalle tabelle, ma solo sulle procedure memorizzate che funzionano con le tabelle, quindi potrei selezionare per xml all'interno della procedura, ma devo usare alcune soluzioni alternative se voglio usare SQL dinamico
Roman Pekar,

3
Se vuoi che i tuoi utenti siano in grado di eseguire il codice, devi in ​​qualche modo dare loro tutto l'accesso di cui hanno bisogno per eseguire il codice. Non inventare requisiti che non esistono per rendere migliore la tua risposta (non devi anche commentare le risposte della concorrenza per guardare la tua risposta - se hanno trovato quella risposta, possono trovare anche la tua).
Aaron Bertrand il

2
Inoltre, se la tua giustificazione per l'uso di XML è che puoi metterlo in una procedura memorizzata per evitare l'accesso diretto alla tabella, forse il tuo esempio dovrebbe mostrare come inserirlo in una procedura memorizzata e come concedere i diritti a un utente in modo che può eseguirlo senza avere accesso in lettura alla tabella sottostante. Per me questo è un creep, perché la maggior parte delle persone che scrivono query su una tabella hanno accesso in lettura alla tabella.
Aaron Bertrand il

2
Direi che una differenza di 10 volte nella durata è importante, sì. E ~ 8.000 righe non sono "grandi quantità di dati" - dovremmo vedere cosa succede contro 800.000 righe?
Aaron Bertrand,

7

Solo per aiutare i nuovi lettori, ho creato un esempio per comprendere meglio la risposta di @ bluefeet su 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;

3

Avevo bisogno di una soluzione per convertire le colonne in righe in Microsoft SQL Server, senza conoscere i nomi delle colonne (utilizzate nel trigger) e senza sql dinamico (sql dinamico è troppo lento per l'uso in un trigger).

Ho finalmente trovato questa soluzione, che funziona benissimo:

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)

Come puoi vedere, converto la riga in XML (Subquery select i, * per xml raw, converte tutte le colonne in una colonna xml)

Quindi CROSS APPLY una funzione per ogni attributo XML di questa colonna, in modo da ottenere una riga per attributo.

Nel complesso, ciò converte le colonne in righe, senza conoscere i nomi delle colonne e senza utilizzare sql dinamico. È abbastanza veloce per il mio scopo.

(Modifica: ho appena visto Roman Pekar rispondere sopra, che sta facendo lo stesso. Ho usato prima il trigger sql dinamico con i cursori, che era da 10 a 100 volte più lento di questa soluzione, ma forse è stato causato dal cursore, non dal sql dinamico. Comunque, questa soluzione è molto semplice e universale, quindi è definitivamente un'opzione).

Lascio questo commento in questo posto, perché voglio fare riferimento a questa spiegazione nel mio post sul trigger di controllo completo, che puoi trovare qui: https://stackoverflow.com/a/43800286/4160788


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

È possibile ottenere un elenco di tabelle con un numero di righe> 350. È possibile visualizzare l'elenco delle soluzioni della tabella come riga.


2

Solo perché non l'ho visto menzionato.

Se 2016+, ecco ancora un'altra opzione per annullare la dinamica dei dati senza effettivamente utilizzare Dynamic SQL.

Esempio

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

ritorna

ID  Item    Value
1   Col1    A
1   Col2    B
2   Col1    R
2   Col2    C
3   Col1    X
3   Col2    D
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.