SQL Server: Spalten in Zeilen

128

Suchen Sie nach einer eleganten (oder einer beliebigen) Lösung zum Konvertieren von Spalten in Zeilen.

Hier ein Beispiel: Ich habe eine Tabelle mit dem folgenden Schema:

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

Folgendes möchte ich als Ergebnis erhalten:

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

Und die Ergebniswerte sind:

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'

Und so weiter..

Macht das Sinn? Haben Sie Vorschläge, wo Sie suchen und wie Sie dies in T-SQL tun können?

Sergei
quelle
2
Haben Sie sich schon mit Pivot / Unpivot befasst ?
Josh Jay
Am Ende ging es mit der Bluefeet-Lösung. Elegant und funktional. Vielen Dank an alle.
Sergei

Antworten:

247

Mit der UNPIVOT- Funktion können Sie die Spalten in Zeilen konvertieren:

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

Beachten Sie, dass die Datentypen der Spalten, die Sie entfernen, identisch sein müssen, sodass Sie möglicherweise die Datentypen konvertieren müssen, bevor Sie das Unpivot anwenden.

Sie können auch CROSS APPLYmit UNION ALL die Spalten konvertieren:

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

Abhängig von Ihrer Version von SQL Server können Sie CROSS APPLY sogar mit der VALUES-Klausel verwenden:

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

Wenn Sie 150 Spalten zum Aufheben des Pivots haben und nicht die gesamte Abfrage fest codieren möchten, können Sie die SQL-Anweisung mithilfe von dynamischem SQL generieren:

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
quelle
4
Für diejenigen, die mehr Schrauben UNPIVOTund Muttern wollen und / vs. APPLY, dieser Blog-Beitrag von Brad Schulz (und dem Nachfolger ) aus dem Jahr 2010 ist (sind) wunderschön.
Ruffin
2
Meldung 8167, Ebene 16, Status 1, Zeile 147 Der Spaltentyp "blahblah" steht in Konflikt mit dem Typ der anderen in der UNPIVOT-Liste angegebenen Spalten.
JDPeckham
@JDPeckham Wenn Sie unterschiedliche Datentypen haben, müssen Sie diese vor dem Aufheben des Pivots in denselben Typ und dieselbe Länge konvertieren. Hier finden Sie weitere Informationen dazu .
Taryn
Die XML-Methode weist einen Fehler auf, da XML-Codes wie & gt;, & lt; und & amp;. Außerdem kann die Leistung durch Umschreiben wie folgt erheblich verbessert werden: Wählen Sie @colsUnpivot = stuff ((wählen Sie ',' + Quotenname (C.column_name) als [text ()] aus information_schema.columns als C aus, wobei C.table_name = 'yourtable' und C.column_name wie 'Indikator%' für XML-Pfad (''), Typ) .value ('text () [1]', 'nvarchar (max)'), 1, 1, '')
rrozema
24

Wenn Sie 150 Spalten haben, ist UNPIVOT meiner Meinung nach keine Option. Sie könnten also einen XML-Trick verwenden

;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

Sie könnten auch dynamisches SQL schreiben, aber ich mag XML mehr - für dynamisches SQL müssen Sie über Berechtigungen verfügen, um Daten direkt aus der Tabelle auszuwählen, und das ist nicht immer eine Option.

UPDATE
Da es in den Kommentaren eine große Flamme gibt, denke ich, dass ich einige Vor- und Nachteile von XML / Dynamic SQL hinzufügen werde. Ich werde versuchen, so objektiv wie möglich zu sein und Eleganz und Hässlichkeit nicht zu erwähnen. Wenn Sie andere Vor- und Nachteile haben, bearbeiten Sie die Antwort oder schreiben Sie Kommentare

Nachteile

  • Es ist nicht so schnell wie dynamisches SQL. Grobe Tests haben ergeben, dass XML etwa 2,5-mal langsamer als dynamisches SQL ist (es war eine Abfrage in der Tabelle mit ~ 250000 Zeilen, daher ist diese Schätzung keineswegs genau). Sie können es selbst vergleichen, wenn Sie möchten. Hier ist das Beispiel von sqlfiddle. In 100000 Zeilen waren es 29s (xml) und 14s (dynamisch).
  • Möglicherweise ist es für Personen, die mit xpath nicht vertraut sind , schwieriger zu verstehen .

Profis

  • Es ist der gleiche Bereich wie Ihre anderen Abfragen, und das könnte sehr praktisch sein. Einige Beispiele fallen mir ein
    • Sie könnten Abfragen insertedund deletedTabellen in Ihrem Trigger abfragen (mit Dynamic überhaupt nicht möglich).
    • Benutzer müssen keine Berechtigungen für die direkte Auswahl aus der Tabelle haben. Ich meine, wenn Sie eine Ebene für gespeicherte Prozeduren haben und der Benutzer die Berechtigung zum Ausführen von sp hat, aber keine Berechtigung zum direkten Abfragen von Tabellen hat, können Sie diese Abfrage dennoch innerhalb der gespeicherten Prozedur verwenden.
    • Sie können Tabellenvariablen abfragen, die Sie in Ihrem Bereich ausgefüllt haben (um sie innerhalb des dynamischen SQL zu übergeben, müssen Sie sie stattdessen entweder als temporäre Tabelle erstellen oder einen Typ erstellen und sie als Parameter an das dynamische SQL übergeben.
  • Sie können diese Abfrage innerhalb der Funktion ausführen (skalar oder tabellenwertig). Es ist nicht möglich, dynamisches SQL innerhalb der Funktionen zu verwenden.
Roman Pekar
quelle
2
Welche Daten wählen Sie mit XML aus, für die keine Daten aus der Tabelle ausgewählt werden müssen?
Aaron Bertrand
1
Zum Beispiel könnten Sie entscheiden , nicht Benutzerberechtigungen zu geben , aus Tabellen auszuwählen Daten, sondern nur auf gespeicherte Prozeduren mit Tabellen arbeiten, so dass ich für xml innerhalb der Prozedur wählen könnte, aber ich habe einige Abhilfen verwenden , wenn ich dynamische SQL verwenden möge
Roman Pekar
3
Wenn Sie möchten, dass Ihre Benutzer den Code ausführen können, müssen Sie ihnen den Zugriff gewähren, den sie zum Ausführen des Codes benötigen. Stellen Sie keine Anforderungen auf, die nicht vorhanden sind, damit Ihre Antwort besser klingt (Sie müssen auch keine konkurrierenden Antworten kommentieren, um Ihre Antwort anzuzeigen - wenn sie diese Antwort gefunden haben, können sie auch Ihre finden).
Aaron Bertrand
2
Auch wenn Ihre Rechtfertigung für die Verwendung von XML darin besteht, dass Sie es in eine gespeicherte Prozedur einfügen können, um einen direkten Zugriff auf die Tabelle zu vermeiden, sollte Ihr Beispiel möglicherweise zeigen, wie Sie es in eine gespeicherte Prozedur einfügen und einem Benutzer Rechte gewähren, damit dieser kann es ausführen, ohne Lesezugriff auf die zugrunde liegende Tabelle zu haben. Für mich ist das Scope Creep, da die meisten Leute, die Abfragen für eine Tabelle schreiben, Lesezugriff auf die Tabelle haben.
Aaron Bertrand
2
Ich würde sagen, ein 10-facher Unterschied in der Dauer ist wichtig, ja. Und ~ 8.000 Zeilen sind keine "großen Datenmengen" - sollten wir sehen, was mit 800.000 Zeilen passiert?
Aaron Bertrand
7

Um neuen Lesern zu helfen, habe ich ein Beispiel erstellt, um die Antwort von @ bluefeet zu UNPIVOT besser zu verstehen.

 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
quelle
3

Ich brauchte eine Lösung, um Spalten in Zeilen in Microsoft SQL Server zu konvertieren, ohne die Spaltennamen (im Trigger verwendet) und ohne dynamisches SQL zu kennen (dynamisches SQL ist zu langsam für die Verwendung in einem Trigger).

Ich habe endlich diese Lösung gefunden, die gut funktioniert:

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)

Wie Sie sehen können, konvertiere ich die Zeile in XML (Unterabfrage wähle i, * für XML Raw, dies konvertiert alle Spalten in eine XML-Spalte)

Dann wende ich eine Funktion auf jedes XML-Attribut dieser Spalte an, sodass ich eine Zeile pro Attribut erhalte.

Insgesamt werden Spalten in Zeilen konvertiert, ohne die Spaltennamen zu kennen und ohne dynamisches SQL zu verwenden. Es ist schnell genug für meinen Zweck.

(Bearbeiten: Ich habe gerade gesehen, wie Roman Pekar oben geantwortet hat, wer dasselbe tut. Ich habe zuerst den dynamischen SQL-Trigger mit Cursorn verwendet, der 10 bis 100 Mal langsamer war als diese Lösung, aber möglicherweise wurde er durch den Cursor verursacht, nicht durch den Dynamic SQL. Wie auch immer, diese Lösung ist sehr einfach und universell, daher ist sie definitiv eine Option.

Ich hinterlasse diesen Kommentar an dieser Stelle, da ich auf diese Erklärung in meinem Beitrag zum vollständigen Audit-Trigger verweisen möchte, die Sie hier finden: https://stackoverflow.com/a/43800286/4160788

schwarz
quelle
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

Sie können eine Liste von Tabellen mit Zeilenzahlen> 350 erhalten. Sie können in der Lösungsliste der Tabelle als Zeile sehen.

cunay
quelle
2

Nur weil ich es nicht erwähnt habe.

Ab 2016+ gibt es hier noch eine weitere Option zum dynamischen Aufheben des Pivots von Daten, ohne Dynamic SQL tatsächlich zu verwenden.

Beispiel

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

Kehrt zurück

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