CiAgICA8IS0tIExpbmtlZEluIC0tPgogICAgPHNjcmlwdCB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiPgogICAgICAgIF9saW5rZWRpbl9wYXJ0bmVyX2lkID0gIjEyMzUwNzMiOwogICAgICAgIHdpbmRvdy5fbGlua2VkaW5fZGF0YV9wYXJ0bmVyX2lkcyA9IHdpbmRvdy5fbGlua2VkaW5fZGF0YV9wYXJ0bmVyX2lkcyB8fCBbXTsKICAgICAgICB3aW5kb3cuX2xpbmtlZGluX2RhdGFfcGFydG5lcl9pZHMucHVzaChfbGlua2VkaW5fcGFydG5lcl9pZCk7CiAgICA8L3NjcmlwdD48c2NyaXB0IHR5cGU9InRleHQvamF2YXNjcmlwdCI+CiAgICAgICAgKGZ1bmN0aW9uKCl7dmFyIHMgPSBkb2N1bWVudC5nZXRFbGVtZW50c0J5VGFnTmFtZSgic2NyaXB0IilbMF07CiAgICAgICAgICAgIHZhciBiID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgic2NyaXB0Iik7CiAgICAgICAgICAgIGIudHlwZSA9ICJ0ZXh0L2phdmFzY3JpcHQiO2IuYXN5bmMgPSB0cnVlOwogICAgICAgICAgICBiLnNyYyA9ICJodHRwczovL3NuYXAubGljZG4uY29tL2xpLmxtcy1hbmFseXRpY3MvaW5zaWdodC5taW4uanMiOwogICAgICAgICAgICBzLnBhcmVudE5vZGUuaW5zZXJ0QmVmb3JlKGIsIHMpO30pKCk7CiAgICA8L3NjcmlwdD4KICAgIDxub3NjcmlwdD4KICAgICAgICA8aW1nIGhlaWdodD0iMSIgd2lkdGg9IjEiIHN0eWxlPSJkaXNwbGF5Om5vbmU7IiBhbHQ9IiIgc3JjPSJodHRwczovL3B4LmFkcy5saW5rZWRpbi5jb20vY29sbGVjdC8/cGlkPTEyMzUwNzMmZm10PWdpZiIgLz4KICAgIDwvbm9zY3JpcHQ+CiAgICA8IS0tIEVuZCBMaW5rZWRJbiAtLT4KICAgIA==
Generic filters
Exact matches only
Search in title
Search in excerpt
Search in content

Relationaler Cube per ROLLUP_FINAL

In diesem Blogbeitrag wird gezeigt, wie durch die „GROUP BY“-Syntaxerweiterung „ROLLUP“ relativ einfach eine Art „relationaler Cube“ erschaffen werden kann. Außerdem enthält der Blogbeitrag eine Prozedur, die auf Grundlage eines DeltaMaster-ETL-Modells direkt einen „relationalen Cube“ in Form einer View erstellt.

Einleitung

Dass wir uns als Berater auf die Ergebnisse der Aggregationsberechnungen des SQL Server Analysis Services (SSAS) verlassen können, wird in unserem täglichen Arbeitsleben immer wieder unter Beweis gestellt. Der SSAS speichert beim Verarbeitungsprozess die Aggregationen in einem speziellen Format ab, was zu den schnellen Antwortzeiten bei der Abfrage des daraus entstehenden Cubes führt. Der Nachteil für viele ist wiederum, dass damit die doch so vertraute Welt der Tabellen und der Abfragesprache SQL verlassen wird und man sich in die unbekannte Welt der Multidimensionalität und des MDX begibt. So schlimm ist es zum Glück dann doch nicht. Aber der Wohlfühlfaktor ist bei vielen Kunden und Beratern in der relationalen Datenbankwelt doch ein Stück weit höher als in der multidimensionalen.

Wenn ein Ergebnis in DeltaMaster angezweifelt wird, dann tritt man gerne den Beweis für dessen Richtigkeit an, indem die Prozesskette von vorne nach hinten durchgegangen wird, bis man sich schlussendlich auf der relationalen Ebene wiederfindet. Der Nachweis auf Basisebene ist dann über die Faktentabelle schnell erbracht. Wenn jedoch die Ergebnisse der Aggregationen angezweifelt werden, so wird es schon etwas kniffliger. Erstmal müssen die Merkmale (Dimensionen) an die Faktentabelle gejoint werden, um dann per „GROUP BY“ die Aggregationen nachstellen zu können. Das ist aufwendig und deckt in diesem Moment auch immer nur die Aggregation ab, die in der Fragestellung aktuell analysiert werden soll. Will man dann doch z.B. das Ergebnis der nächsthöheren Ebene eines Merkmals wissen, so geht das Umstellen der SQL-Syntax wieder von neuem los.

Wäre es hier nicht sehr angenehm, wenn auf eine View oder Tabelle zugegriffen werden kann, in der die Aggregationen über sämtliche Merkmale und Ebenen bereits ausgerechnet sind und das Ergebnis nur noch gefiltert werden muss, wie es im Cube auch der Fall ist? Dieser Blogbeitrag zeigt genau dafür ein Konzept auf und enthält eine Prozedur, die eine solche View anhand der FACT-ID automatisch erstellt.

Umsetzung

Die Zauberformel bei der Umsetzung einer solchen Anforderung heißt „ROLLUP“. Die „ROLLUP“-Syntax stellt eine Erweiterung/Untersyntax des „GROUP BY“ dar und kann in ein und demselben SELECT-Statement Gruppierungen auf mehreren Ebenen gleichzeitig vornehmen. Verdeutlichen wir das anhand eines einfachen Beispiels. Wir wollen relational das Jahres-, sowie das Quartalsergebnis über alle Merkmale einer Faktentabelle ermitteln. Mit dem „GROUP BY“-Befehl würden wir hierzu folgendermaßen vorgehen:

SELECT  
D0101.JahrID, 
FACT.[WertartID], 
FACT.[PeriodenansichtID], 
FACT.[KumulationID], 
FACT.[KundeID], 
FACT.[ProduktID], 
FACT.[StoffgruppeID], 
FACT.[VertriebID], 
FACT.[EinheitID], 
SUM(FACT.[Absatz]) Absatz_Aggregiert 

FROM T_FACT_01_Deckungsbeitragsrechnung AS FACT 

LEFT JOIN  T_DIM_01_03_Monat AS D0103 
ON D0103.MonatID = FACT.MonatID 
LEFT JOIN  T_DIM_01_02_Quartal AS D0102 
ON D0102.QuartalID = D0103.QuartalID 
LEFT JOIN  T_DIM_01_01_Jahr AS D0101 
ON D0101.JahrID = D0102.JahrID 

GROUP BY D0101.JahrID, FACT.[WertartID], FACT.[PeriodenansichtID], FACT.[KumulationID],
FACT.[KundeID], FACT.[ProduktID], FACT.[StoffgruppeID], FACT.[VertriebID], FACT.
[EinheitID]

 

Mit folgendem Ergebnis (Ausschnitt):

Abb. 1: Ergebnis einer Aggregation per „GROUP BY“ über ein Gruppierungsmerkmal

Wollen wir nun das Aggregationsergebnis auch noch gleichzeitig auf Quartalsebene darstellen, wird es komplizierter. Die „ROLLUP“-Syntax vereinfacht die Umsetzung dieser Anforderung ungemein. Das Statement und das Ergebnis würden in diesem Fall nämlich so aussehen:

SELECT  
D0101.JahrID, 
D0102.QuartalID, 
FACT.[WertartID], 
FACT.[PeriodenansichtID], 
FACT.[KumulationID], 
FACT.[KundeID], 
FACT.[ProduktID], 
FACT.[StoffgruppeID], 
FACT.[VertriebID], 
FACT.[EinheitID], 
SUM(FACT.[Absatz]) Absatz_Aggregiert 

FROM T_FACT_01_Deckungsbeitragsrechnung AS FACT 

LEFT JOIN  T_DIM_01_03_Monat AS D0103 
ON D0103.MonatID = FACT.MonatID 
LEFT JOIN  T_DIM_01_02_Quartal AS D0102 
ON D0102.QuartalID = D0103.QuartalID 
LEFT JOIN  T_DIM_01_01_Jahr AS D0101 
ON D0101.JahrID = D0102.JahrID 

GROUP BY  ROLLUP(D0101.JahrID,D0102.QuartalID), FACT.[WertartID], FACT.
[PeriodenansichtID], FACT.[KumulationID], FACT.[KundeID], FACT.[ProduktID], FACT.
[StoffgruppeID], FACT.[VertriebID], FACT.[EinheitID]

Abb. 2: Ergebnis einer Aggregation per Group By & Rollup über zwei Gruppierungsmerkmale

In die Klammern der „ROLLUP“-Syntax schreibt man die Spalten der Hierarchieebenen eines Merkmals in absteigender Reihenfolge vom höchsten Level zum niedrigsten Level. Das Rollup führt dann über mehrere Hierarchieebenen gleichzeitig eine Aggregation durch. Damit kann in diesem Beispiel sowohl die Frage über den Jahreswert, als auch über den Quartalswert beantwortet werden. Der Datensatz mit einem Jahreswert hat in der QuartalID-Spalte folgerichtig einen NULL-Wert. Hätte man in diesem Beispiel auch die Monatsebene in das „ROLLUP“ eingeschlossen, hätte der Datensatz in der MonatID-Spalte natürlich ebenfalls einen NULL-Wert.

Dies kann beliebig erweitert werden, sodass man ebenfalls auch Fragestellungen beantworten kann, die eine Gruppierung von mehreren Merkmalen erfordern. Hierzu kann das „GROUP BY“ einfach um weitere „ROLLUP“s erweitert werden. Soll beispielsweise ermittelt werden, wie hoch der Absatz im Jahr 2018 in der Produkthauptgruppe 1 war, kann das Statement wie folgt aussehen.

SELECT  
D0101.JahrID, 
D0102.QuartalID, 
D0601.[ProdukthauptgruppeID], 
D0602.[ProduktgruppeID], 
D0603.[ProduktID], 
SUM(FACT.[Absatz]) Absatz_Aggregiert 

FROM T_FACT_01_Deckungsbeitragsrechnung AS FACT 

LEFT JOIN  T_DIM_01_03_Monat AS D0103 
ON D0103.MonatID = FACT.MonatID 
LEFT JOIN  T_DIM_01_02_Quartal AS D0102 
ON D0102.QuartalID = D0103.QuartalID 
LEFT JOIN  T_DIM_01_01_Jahr AS D0101 
ON D0101.JahrID = D0102.JahrID 
LEFT JOIN  T_DIM_06_03_Produkt AS D0603 
ON D0603.ProduktID = FACT.ProduktID 
LEFT JOIN  T_DIM_06_02_Produktgruppe AS D0602 
ON D0602.ProduktgruppeID = D0603.ProduktgruppeID 
LEFT JOIN  T_DIM_06_01_Produkthauptgruppe AS D0601 
ON D0601.ProdukthauptgruppeID = D0602.ProdukthauptgruppeID 

GROUP BY   
ROLLUP(D0101.JahrID,D0102.QuartalID), 
ROLLUP(D0601.ProdukthauptgruppeID,D0602.ProduktgruppeID,D0603.ProduktID) 

HAVING 1=1 
AND D0601.[ProdukthauptgruppeID] = 1 
AND D0602.[ProduktgruppeID] IS NULL 
AND D0603.[ProduktID] IS NULL 
AND D0101.[JahrID] = 2018 
AND D0102.[QuartalID] IS NULL

Abb. 3: Ergebnis der Fragestellung nach dem Absatz im Jahr 2018 der Produkthauptgruppe 1

Denkt man das Konzept des „ROLLUP“s weiter, so können aus der Modelldefinition des DeltaMaster ETL genau die Informationen gezogen werden, die benötigt werden, um eine Art „relationalen Cube“ zu erstellen. Dieser ist dann gleichermaßen über alle Elemente der angebundenen Dimensionen filterbar und gibt einem das gewünschte aggregierte Ergebnis in der relationalen Datenbank aus. Hierfür habe ich folgende Prozedur erstellt, die auf Grundlage eines DeltaMaster-ETL-Modells einen „relationalen Cube“ in Form einer View generiert. Die Prozedur kann über folgende Syntax erstellt werden:

CREATE OR ALTER PROC P_CREATE_relational_Cube (
@FactID int,
@InclDIMs varchar(max) = NULL
) AS

DECLARE @CreateSQL varchar(max),

@FACT_SourceTableName varchar(250),

@FACT_FactName varchar(250),

@DIM int,

@Last_DIM varchar(100) = '',

@DIM_Joins varchar(max),

@DIM_Join_List varchar(max) = '',

@Level int,

@DESC_Level int,

@LevelName varchar(250),

@DESC_LevelName varchar(250),

@RollUp_DIM varchar(250),

@RollUp_DIM_List varchar(max) = '',

@Select_List varchar(max) = '',

@SQL_InclDIMs varchar(max),

@MeasureID int,

@Measure_List varchar(max) = '',

@Measure_Name varchar(250),

@Measure_AggregationType int

DECLARE      @SQL_InclDIMs_Table TABLE (DimensionID int)

 

SET @FACT_SourceTableName = 'T_FACT_' + RIGHT(100 + @FactID,2) + '_' + (SELECT FactName FROM [dbo].[T_Model_Facts] WHERE FactID = @FactID)
SET @FACT_FactName = (SELECT FactName FROM [dbo].[T_Model_Facts] WHERE FactID = @FactID)

 

-- Definition der zu gruppierenden Dimensionen für den Cursor

IF @InclDIMs IS NULL
BEGIN
SET @SQL_InclDIMs = 'SELECT [DimensionID] FROM [dbo].[T_Model_Fact_Dimension_SourceTable] WHERE FactID = ' + CAST(@FactID as varchar(20))
END
ELSE
BEGIN
SET @SQL_InclDIMs = 'SELECT [DimensionID] FROM [dbo].[T_Model_Fact_Dimension_SourceTable] WHERE FactID = ' + CAST(@FactID as varchar(20)) +  ' AND DimensionID IN (' + @InclDIMs + ')'
END

-- Übergabe der zuvor definierten Dimensionen in Tabellen-Parameter
INSERT INTO @SQL_InclDIMs_Table (DimensionID)
EXEC (@SQL_InclDIMs)

--Erstellen von Join- und Select-Liste

DECLARE Cursor_Join_DIM CURSOR FOR
SELECT DimensionID FROM @SQL_InclDIMs_Table

OPEN Cursor_Join_DIM

FETCH Cursor_Join_DIM INTO @DIM
WHILE @@FETCH_STATUS = 0

BEGIN

DECLARE Cursor_Join_LEVEL CURSOR FOR
SELECT DimensionLevelID FROM [dbo].[T_Model_Dimension_Levels] WHERE DimensionID = @DIM
ORDER BY DimensionLevelID DESC

OPEN Cursor_Join_LEVEL
FETCH Cursor_Join_LEVEL INTO @Level
WHILE @@FETCH_STATUS = 0

BEGIN

SET @DESC_Level = (SELECT DESC_DimensionLevelID FROM (SELECT ROW_NUMBER() OVER (ORDER BY
DimensionlevelID DESC) DESC_DimensionLevelID, DimensionLevelID FROM [dbo].[T_Model_Dimension_Levels] WHERE DimensionID = @DIM) t WHERE DimensionLevelID = @Level)

SET @DESC_LevelName = (SELECT DimensionLevelName FROM [dbo].[T_Model_Dimension_Levels]
WHERE DimensionID = @DIM AND DimensionLevelID = @DESC_Level)

SET @LevelName = (SELECT DimensionLevelName FROM [dbo].[T_Model_Dimension_Levels]
WHERE DimensionID = @DIM AND DimensionLevelID = @Level)

SET @DIM_Joins  = 'LEFT JOIN  T_DIM_' + RIGHT(100 + @DIM,2) + '_' + RIGHT(100 + @Level,2) + '_' + @LevelName + ' AS ' + 'D' + RIGHT(100 + @DIM,2) + RIGHT(100 + @Level,2) + '

ON D' + RIGHT(100 + @DIM,2) + RIGHT(100 + @Level,2) + '.' + @LevelName + 'ID' + ' = ' + CASE WHEN EXISTS  (SELECT DimensionLevelID FROM [dbo].[T_Model_Dimension_Levels] WHERE DimensionID = @DIM AND  DimensionLevelID = (@Level + 1)) THEN  'D' + RIGHT(100 + @DIM,2) + RIGHT(100 + (@Level+1),2) ELSE 'FACT' END  + '.' + @LevelName + 'ID'

 

SET @DIM_Join_List = @DIM_Join_List  + '
'  + @DIM_Joins

 

SET @RollUp_DIM = 'D' + RIGHT(100 + @DIM,2) + RIGHT(100 + @DESC_Level,2) + '.' +
@DESC_LevelName + 'ID'

SET @Select_List = @Select_List + @RollUp_DIM + ',
'

--Erstellen RollUp-Liste (in umgekehrter Reihenfolge der Level)
SET @RollUp_DIM_List = @RollUp_DIM_List + CASE WHEN RIGHT(@Last_DIM,1) <> @DIM THEN '
ROLLUP(' + @RollUp_DIM + (CASE WHEN @Level = 1 THEN'),' ELSE ','END) ELSE  @RollUp_DIM +
(CASE WHEN @Level = 1 THEN'),' ELSE ','END)  END

 

SET @LAST_DIM =  @Last_DIM + CAST(@DIM as varchar)

FETCH Cursor_Join_LEVEL INTO @Level
END

CLOSE Cursor_Join_LEVEL
DEALLOCATE Cursor_Join_LEVEL

FETCH Cursor_Join_DIM INTO @DIM
END

CLOSE Cursor_Join_DIM
DEALLOCATE Cursor_Join_DIM

--Erstellen der Measure-Liste
DECLARE Cursor_Measures CURSOR FOR
SELECT [MeasureID] FROM [dbo].[T_Model_Fact_Measures] WHERE FactID = @FactID

OPEN Cursor_Measures
FETCH Cursor_Measures INTO @MeasureID
WHILE @@FETCH_STATUS = 0
BEGIN

 

SET @Measure_Name = (SELECT [MeasureName] FROM [dbo].[T_Model_Fact_Measures] WHERE FactID = @FactID AND MeasureID = @MeasureID)

SET @Measure_AggregationType = (SELECT [AggregationTypeID] FROM [dbo].[T_Model_Fact_Measures] WHERE FactID = @FactID AND MeasureID = @MeasureID)

SET @Measure_List = @Measure_List + CASE WHEN @Measure_AggregationType = 2 THEN 'COUNT(' ELSE 'SUM(' END + @Measure_Name +') AS ' + @Measure_Name + ',
'

FETCH Cursor_Measures INTO @MeasureID
END

CLOSE Cursor_Measures
DEALLOCATE Cursor_Measures

--Eliminieren des letzten Kommas
SET @RollUp_DIM_List = LEFT(@RollUp_DIM_List, LEN(@RollUp_DIM_List) -1)
SET @Measure_List = LEFT(@Measure_List, LEN(@Measure_List) -3)

--Definieren der View
SET @CreateSQL = '

CREATE OR ALTER VIEW V_APP_'+ @FACT_FactName + ' AS

SELECT
' + @Select_List + @Measure_List +'

FROM '  +  @FACT_SourceTableName  + ' AS FACT

' + @DIM_Join_List + '

GROUP BY ' + @RollUp_DIM_List

PRINT @CreateSQL

--Erstellen der View
EXEC (@CreateSQL)
GO

Die erstellte Prozedur muss anschließend mit zwei Parametern ausgeführt werden, damit sie eine View mit dem „relationalen Cube“ generiert. Der erste Parameter ist die ID der Faktentabelle. Mit dem zweite Parameter können die Dimensionen frei definiert werden, für die überhaupt Aggregationen gerechnet werden sollen. Wird der zweite Parameter frei gelassen, so erstellt die Prozedur den „relationalen Cube“ mit allen angebundenen Dimensionen. Um genau zu definieren welche Dimensionen enthalten sein sollen, muss der zweite Parameter als komma-separierter String angegeben werden (z.B. ‘1,2,5‘). Somit könnte das Ausführen der Prozedur auf unsere Chair-Datenbank folgendermaßen aussehen: (1= Periode, 2= Wertart, 5= Kunden, 6 = Produkte, 7 = Stoffgruppe, 8 = Vertrieb)

Die Prozedur generiert nun eine View mit der Nomenklatur „V_APP_[Faktenname]“ die wie folgt ausschaut:

Wenn wir diese View dann anschließend ausführen, sehen wir, welche Größe ein solcher „relationaler Cube“ annehmen kann. Die Berechnung sämtlicher Gruppierungskombinationen lässt die Datenmenge extrem ansteigen und generiert uns in der definierten Konstellation 2.455.130 Datensätze. Zum Vergleich, die Ausgangsbasis, also die Faktentabelle, hat lediglich 38.558 Datensätze.

Abb. 4: Ergebnis der abgefragten View des relationalen Cubes

Zum Schluss wollen wir natürlich noch validieren, ob in unserem „relationalen Cube“ auch tatsächlich das gleiche Ergebnis angezeigt wird, wie im OLAP Cube. Hierzu verwenden wir wieder das Beispiel, welches wir zuvor herangezogen hatten. Wir wollen uns das Ist-Ergebnis des Jahres 2018 für die gesamte Produkthauptgruppe der Luxusmodelle (ID = 1) anschauen. Hierzu müssen wir die erstellte View per WHERE-Bedingung filtern und erhalten folgendes Ergebnis:



Abb. 5: Filterung und Ergebnis der exemplarischen Fragestellung

Wenn wir die gleiche Filterung auf dem OLAP Cube über die Sicht von DeltaMaster durchführen, sehen wir, dass in beiden Fällen das gleiche Ergebnis herauskommt.

Abb. 6: Ergebnis der exemplarischen Fragestellung in DeltaMaster (auf Grundlage eines OLAP Cubes)

Fazit

Die hier gezeigte Prozedur mit der enthaltenen „ROLLUP“-Funktionalität kann dabei helfen, Fragestellungen in der relationalen Welt zu beantworten, die zuvor erst über die Erstellung eines Cubes beantwortet werden konnten. Ein Wermutstropfen an der Lösung ist jedoch die Einschränkung der „ROLLUP“-Syntax. Diese kann nämlich nur maximal 4096 Groupingsets erstellen. Diese maximale Begrenzung ist leider schon recht schnell erreicht. Bereits bei der Faktentabelle der Deckungsbeitragsrechnung unserer Chair-Datenbank kann die View nicht erstellt werden, wenn nicht mindestens eine Dimension aus der Betrachtung herausgenommen wird. Nichtsdestotrotz kann die Prozedur auch auf größeren Modellen angewendet werden, da sich die Fragestellungen meist nicht über alle Merkmale erstrecken und somit über den zweiten Parameter gut einschränken lassen.