Dynamisches Pivot

Nicht immer sind die Spalten fest definiert, nach denen man eine Tabelle pivotieren möchte. Falls die Spalten auch erst nach Anwendung anderer Kriterien feststehen, hilft nur noch dynamisches SQL um das passende Statement zur Laufzeit zu erzeugen.

Die Fragestellung

Es gibt eine Tabelle (Artikel) mit Material aus verschiedenen Werkstoffen in verschiedenen Größen, welches von verschiedenen Herstellern bezogen wird. Ebenfalls in der Tabelle ist die aktuelle Lagermenge (in Stück) verzeichnet. Bei der Abfrage der Artikel sollen nur gewisse Hersteller berücksichtigt werden. Als Ergebnis soll eine Matrix ausgegeben werden, die zu jedem Werkstoff die Lagermenge für die verschiedenen Größen angibt. Die Matrix soll in der vertikalen Richtung nach Werkstoff sortiert sein und in der horizontalen Ausrichtung nach den Größen.

Hier sehen wir die Einschränkung auf die Hersteller als Ursache für die nicht bekannten Spalten. Die Größen der Materialien für die Matrix können erst dann bestimmt werden, wenn die Artikel über die Hersteller gefiltert werden.

CREATE TABLE #Artikel(Werkstoff VARCHAR(50), Abmessung REAL, Hersteller VARCHAR(10), Lagermenge INT);
-- Erster Hersteller, der viele Materialien und Größen im Angebot hat
INSERT INTO #Artikel(Werkstoff, Abmessung, Hersteller, Lagermenge) VALUES('POLYAMID - natur', 4   ,'1234', 1);
INSERT INTO #Artikel(Werkstoff, Abmessung, Hersteller, Lagermenge) VALUES('POLYAMID - natur', 4.2 ,'1234', 1);
INSERT INTO #Artikel(Werkstoff, Abmessung, Hersteller, Lagermenge) VALUES('ROSTFREI      A2', 3   ,'1234', 1);
INSERT INTO #Artikel(Werkstoff, Abmessung, Hersteller, Lagermenge) VALUES('ROSTFREI      A2', 4   ,'1234', 1);
INSERT INTO #Artikel(Werkstoff, Abmessung, Hersteller, Lagermenge) VALUES('ROSTFREI      A4', 2   ,'1234', 1);
INSERT INTO #Artikel(Werkstoff, Abmessung, Hersteller, Lagermenge) VALUES('ROSTFREI      A4', 1.6 ,'1234', 1);
-- Zweiter Hersteller, der die Menge M3 verdoppelt und auch bei M4 ein Teil beisteuert
INSERT INTO #Artikel(Werkstoff, Abmessung, Hersteller, Lagermenge) VALUES('ROSTFREI      A2', 3   ,'5678', 1);
INSERT INTO #Artikel(Werkstoff, Abmessung, Hersteller, Lagermenge) VALUES('ROSTFREI      A2', 4   ,'5678', 1);
-- Dritter Hersteller, der auch bei M4 ein Teil beisteuert und später der einzige Lieferant für M8 ist
INSERT INTO #Artikel(Werkstoff, Abmessung, Hersteller, Lagermenge) VALUES('ROSTFREI      A2', 4   ,'9876', 1);
INSERT INTO #Artikel(Werkstoff, Abmessung, Hersteller, Lagermenge) VALUES('ROSTFREI      A4', 8   ,'9876', 1);
-- Dieser Hersteller soll später nicht berücksichtigt werden
INSERT INTO #Artikel(Werkstoff, Abmessung, Hersteller, Lagermenge) VALUES('ROSTFREI      A4', 8   ,'9999', 1);

Lösung für die Spaltenliste

Zuerst einmal müssen wir die eindeutigen Kombinationen von Werkstoff und Abmessung ermitteln. Diese werden ja wie oben beschrieben über die Hersteller gefiltert. Die Abmessung benötigen wir später als varchar Wert. Daher ist dann keine Sortierung über die numerischen Werte mehr möglich. "16" soll natürlich nicht kleiner als "2" sein. Daher multiplizieren wir die Abmessungen mit 10, um die eine mögliche Nachkommastelle zu eliminieren. Diesen Integer-Wert behalten wir zusätzlich zur späteren Sortierung bei. Wenn man es genau nimmt, hätte man auch den REAL-Wert weiterverwenden können, Aber distinkte Werte sind mir hier einfach lieber.

-- Das sind die Ausgangsdaten
SELECT DISTINCT a.Werkstoff, CAST(a.Abmessung AS VARCHAR(10)) AS Abmessung, CAST(a.Abmessung * 10 AS INT) AS Sortierung
FROM #Artikel a
WHERE a.Hersteller IN ('1234',
                      
'5678',
                      
'9876'  );

Frank Kalis hat in seinem Artikel Spaltenwerte als kommaseparierte Liste zurückgeben Teil 2 beschrieben, wie man sich eine kommaseparierte Liste erzeugt. Daher verzichte ich hier auf die Details. Ich habe lediglich das Statement durch die Ersetzung von LEFT/LEN durch STUFF etwas übersichtlicher gestaltet.

Damit ich die distinkten Spalten nach der neu erstellten Spalte "Sortierung" sortieren kann, obwohl ich diese nicht im Resultset verwenden möchte, benötige ich eine derived table. Die Hintergründe dazu sind im Märchen von ORDER BY beschrieben.

Wir definieren uns also eine lokale Variable, in der wir die Spaltenliste aufbereiten. Die Abmessungen sollen noch ein "M" davor bekommen. Alle Spalten müssen mit eckigen Klammern umgeben sein, damit wir diese später problemlos im PIVOT-Statement verwenden können.

DECLARE @cols NVARCHAR(1000);
WITH Daten AS
(
  
SELECT DISTINCT a.Werkstoff, CAST(a.Abmessung AS VARCHAR(10)) AS Abmessung, CAST(a.Abmessung * 10 AS INT) AS Sortierung
  
FROM #Artikel a
  
WHERE a.Hersteller IN (    '1234',
                          
'5678',
                          
'9876'  )
)
SELECT    @cols = STUFF(( SELECT '],[M' + Abmessung
                          
FROM        (SELECT DISTINCT Abmessung, Sortierung
                          
FROM        Daten ) a
                          
ORDER BY Sortierung
                          
FOR XML PATH('')
                          ),
1, 2, '') + ']';

Dynamisches Pivot

Der Rest ist dann nicht mehr so schwer. Wir müssen lediglich ein syntaktisch korrektes PIVOT-Statement in einer lokalen Variablen aufbereiten. Die Spaltenliste haben wir ja oben schon erledigt. Hier verwenden wir wieder eine Common Table Expression (CTE) mit dem Namen FilteredData, welche uns aus der Tabelle Artikel die entsprechenden Sätze mit Lagermenge für die gewünschten Hersteller liefert. Diese CTE ist dann der Input für das PIVOT-Statement.

Die aufbereiteten Spalten benötigen wir zwei mal. Einmal in der Select-Liste, damit die Tabelle auch wirklich so aussieht, wie gewünscht, zum anderen in der IN-Bedingung des PIVOT-Teils.

Ganz am Ende sortieren wir noch das Ergebnis nach dem Werkstoff, damit dieser auch in alphabetischer Reihenfolge ausgegeben wird.

Das so aufbereitete Statement führen wir am Ende mit EXEC aus.

DECLARE @query VARCHAR(8000);
SET @query = N'With FilteredData
as
(SELECT  a.Werkstoff, ''M'' + cast(a.Abmessung as varchar(10)) as Abmessung, Lagermenge
FROM #Artikel a
WHERE a.Hersteller IN (''1234'',
                    ''5678'',
                    ''9876'')
)
SELECT Werkstoff,'
+
CAST(@cols    AS VARCHAR(1000)) +'
FROM FilteredData p
PIVOT (
Sum(Lagermenge)
FOR Abmessung
IN ('
+
CAST(@cols    AS VARCHAR(1000)) +' )
) as x
ORDER BY Werkstoff
;'

EXECUTE(@query);

Das Ergebnis

In diesem Bild sehen wir also die Spalten und Werkstoffe in der gewünschten Reihenfolge. Das Element M3 ist zwei mal im Lager (zwei Hersteller) und M4 (ROSTFREI A2) ist drei mal vorhanden, da es von drei Herstellern geliefert wurde. Der einzige Satz für M8 kommt vom letzten Lieferanten.

WerkstoffM1.6M2M3M4M4.2M8
POLYAMID - naturNULLNULLNULL11NULL
ROSTFREI A2NULLNULL23NULLNULL
ROSTFREI A411NULLNULLNULL1

  pivot_order_by.sql