English Deutsch

 

SSAS 2005 MDX Tuning – unnötige MEMBER und fast noch schlimmer: STRTOMEMBER

“Das beste MDX ist das, das man nicht schreibt” (über den Buschfunk: Mosha Pasumansky)

Aber wer kommt denn auf die Idee, dass dieses Zitat wortwörtlich zu nehmen ist?

Ich sah mich mit einer Kundendimension von 172.000 Kunden konfrontiert, die etwa 200 Vertretern zugeordnet waren. Ergebnis der Abfrage sollten die Kunden sein, die in einem gewissen Monat keine Umsätze, dafür aber im Rest des Jahres Umsätze erzielt hatten. Übersetzt in den AdventureWorks Cube, wobei der Vertreter hier durch die Dimension “Produkt” ersetzt wird, sah die Abfrage zuerst so aus:

WITH

MEMBER
UmsatzDezember03
AS

(

    [Measures].[Internet Sales Amount]

    ,STRTOMEMBER(‘[Date].[Calendar].[Month].&[2003]&[12]‘, CONSTRAINED)

)

SET Kunden AS

EXCEPT(EXISTS

(NONEMPTY([Customer].[Customer].[Customer].MEMBERS

        ,[Measures].[Internet Sales Amount])

        ,[Product].[Product Categories].[Category].&[1])

,NONEMPTY([Customer].[Customer].[Customer].MEMBERS

    ,UmsatzDezember03))


SELECT

{

    [Measures].[Internet Sales Amount]

} ON 0,

{

    Kunden

} ON 1 FROM [Adventure Works]


WHERE [Date].[Calendar].[Calendar Year].&[2003]

Um der besseren Lesbarkeit willen erstellte ich das berechnete MEMBER UmsatzDezember03 um das – zugegeben etwas komplizierte – Kundenset zu filtern.

Die ursprüngliche Abfrage (nicht das Beispiel hier) benötigte 45 Sekunden, was bei einer so kleinen Dimension von 172.000 Membern nicht akzeptabel war. Query-Tuning war also unumgänglich.

Nehmen wir uns des Beispiels oben an: Diese Abfrage – mit “MDX Studio” auf dem AdventureWorks Cube ausgeführt – berechnete 44.240 Zellen und benötigte 2,6 Sekunden für die Ausführung.

Mosha’s Befehl zur Sparsamkeit gehorchend, verzichtete ich auf das Hilfsmember und brachte den Ausdruck für UmsatzDezember03 direkt in das Kundenset.

WITH

SET Customers AS

EXCEPT(

    EXISTS(NONEMPTY([Customer].[Customer].[Customer].MEMBERS

                ,[Measures].[Internet Sales Amount])

        ,[Product].[Product Categories].[Category].&[1])

,NONEMPTY([Customer].[Customer].[Customer].MEMBERS

    ,(

    [Measures].[Internet Sales Amount]

    ,STRTOMEMBER(‘[Date].[Calendar].[Month].&[2003]&[12]‘, CONSTRAINED)

)))


SELECT

{

    [Measures].[Internet Sales Amount]

} ON 0,

{

    Customers

} ON 1 FROM [Adventure Works]


WHERE [Date].[Calendar].[Calendar Year].&[2003]

Dies führte zu einer Berechnung von nur 7.272 Zellen, benötigte aber immer noch 2,6 Sekunden für die Ausführung. Nachdem ich mir auch das CONSTRAINED Flag in der STRTOMEMBER Funktion erspart hatte, lag die Ausführungszeit bei 0,6 Sekunden.

Was war passiert? Offensichtlich führte das Einfügen des berechneten Members UmsatzDezember03 dazu, das die Berechnung für den Term NONEMPTY([Customer].[Customer].[Customer].MEMBERS,UmsatzDezember03) für jedes einzelne Member der Kundendimension ausgeführt wurde – d.h. je mehr Member in der Dimension enthalten sind, desto langsamer wird die Abfrage.

Darüber hinaus ist festzustellen, dass das CONSTRAINED Flag die Query ebenfalls verlangsamt – was uns zu dem Schluss führt, das Mosha’s Zitat tatsächlich wortwörtlich zu nehmen ist. Allein das Einsparen der Worte “MEMBER, AS, STRTOMEMBER und CONSTRAINED” führt in obigem Beispiel zu einer Beschleunigung der Abfrage von 500% und zu einer 86%-igen Reduktion der berechneten Zellen.

In den Analysis Services 2008 tritt dieses Problem nicht mehr auf. “Hilfsmember” können hier ohne Scham verwendet werden. Beide auf einem SQL Server 2008 ausgeführten Abfragen berechneten nur die nötigen 7.272 Zellen.

MDX ParallelPeriod – bleib auf Deinem Level (Andere gibt’s vielleicht nicht)

Um innerhalb einer Zeitdimension zurückliegende Member zu erreichen braucht es kein Voodoo, MDX stellt eine bequeme Funktion namens “PARALLELPERIOD” zur Verfügung, welche zu einem angegebenen Element (Bsp. August 2003) über eine Ebene der Zeitdimension (Monat, Quartal, Jahr …) eine bestimmte Anzahl von Schritten (12 Monate, 2 Jahre o. ä.) zurück oder vorwärts geht.

Zum Beispiel folgende Abfrage:

WITH
MEMBER LevelMonth_OneYearAgo AS
PARALLELPERIOD([Date].[Calendar].[Month],12,
[Date].[Calendar].CURRENTMEMBER).MEMBER_CAPTION
MEMBER LevelYear_OneYearAgo AS
PARALLELPERIOD([Date].[Calendar].[Calendar Year],1,
[Date].[Calendar].CURRENTMEMBER).MEMBER_CAPTION
SELECT {
LevelMonth_OneYearAgo
,LevelYear_OneYearAgo
} ON 0 ,
{
[Date].[Calendar].[Month].&[2003]&[8]
}ON 1 FROM [Adventure Works]

liefert zurück:

Obwohl das Hierarchielevel “[Date].[Calendar].[Calendar Year]” auf keiner Achse angegeben ist, kann es durch seine bestehende Attributrelation zur Ebene “[Date].[Calendar].[Month]” angesprochen werden .

Sind also Attributbeziehungen innerhalb der verschiedenen Zeitebenen definiert, können auch andere als in der Abfrage verwendete Ebenen der Zeitdimension mit PARALLELPERIOD verwendet werden.

Was aber passiert, wenn es im angegebenen zurückliegenden Zeitraum keinen Member gibt? Im AdventureWorks Cube 2008 ist der erste Member der Zeitdimension der erste Juli 2001. Ersetzt man den oben abgefragten Monat auf der Achse mit “[Date].[Calendar].[Month].&[2002]&[6]” liefert die Abfrage folgendes zurück:

Das Ergebnis für PARALLELPERIOD mit Level “Month” ist korrekt, aber was ist mit dem Ergebnis für Level “Year” los? Angezeigt wird der Dezember 2001, der nun definitiv kein Jahr hinter dem Juni 2002 liegt. In diesem Fall wird nun der letzte Monat des Vorjahres angezeigt, also das letzte Member der darunterliegenden Ebene. Auch darauf sollte man sich nun nicht verlassen, beachten Sie das folgende Beispiel (und, wenn möglich, führen Sie das Statement auch mal auf Ihrem eigenen Adventure Works Cube aus)

Ziel der Abfrage ist den allseits beliebten “Sales Amount” der Monate 2001 bis Ende 2002 zu ermitteln. Beim Cubebrowsen stellt sich folgendes heraus:

Zwischen Januar 2001 und Juni 2001 sind keine Werte für “Sales Amount” vorhanden, eine ähnliche MDX-Abfrage wie folgende:

WITH
//LAG:
Gibt das Element zurück, das eine angegebene Anzahl von Positionen (hier 12) vor einem angegebenen Element entlang der Dimension des Elements liegt.
MEMBER [Measures].[Lag_Month] AS
([Measures].[Sales Amount],
[Date].[Calendar].CURRENTMEMBER.LAG(12))

//PARALLELPERIOD: Gibt ein Element aus einer früheren Periode in derselben relativen Position wie ein angegebenes Element zurück

MEMBER [Measures].[PP_LevelMonth] AS
([Measures].[Sales Amount],
PARALLELPERIOD([Date].[Calendar].[Month],12,
[Date].[Calendar].CURRENTMEMBER))

MEMBER
[Measures].[PP_LevelYear] AS

([Measures].[Sales Amount],
PARALLELPERIOD([Date].[Calendar].[Calendar Year],1,
[Date].[Calendar].CURRENTMEMBER))

SELECT
{

[Measures].[Sales Amount]
,[Measures].[Lag_Month]
,[Measures].[PP_LevelMonth]
,[Measures].[PP_LevelYear]
} ON 0,
{ //Level month on axis 1
[Date].[Calendar].[Month].&[2001]&[7]
    : [Date].[Calendar].[Month].&[2002]&[12]
} ON 1 FROM [Adventure Works]

ergibt:

Die “Sales Amount” Werte für das mit PARALLELPERIOD Level “Year” erstellte Member sind in Unordnung geraten. Schon ab Januar 2002 werden die rückliegenden Werte angezeigt, obwohl erst der Juli 2002 Werte für das aktuelle und das zurückliegende Jahr aufweist.

Die Werte der Member welche mit der MDX Funktion “Lag” oder mit dem in der Achse verwendeten Level, nämlich “Month” erstellt wurden sind korrekt.

Deshalb, “Schuster – bleib bei Deinem Leisten” und bei PARALLELPERIOD im abgefragten Level.

Kaskadierende Parameter – Easy as can be

Herr G. Ordnet, Controller der Firma Adventure Works, möchte sich für ausgewählte Produkte seines Unternehmens die Internetverkäufe und die Frachtkosten ansehen. Er beauftragt den Reportbauer Z. Schnell mit dem Erstellen eines Berichts. Selbiger überdenkt die Anforderung und stellt einen Bericht mit einer Parameterliste aller Produkte zur Verfügung.

Nachdem Herr G. Ordnet mehrere Male alle 606 Produkte seiner Firma in der Auswahlkombobox des Berichtes durchforstet hat, platzt ihm der Kragen und er bittet um eine “vernünftige Einschränkung der Produkte nach Kategorien.”

Ein Glück, dass in der Datenbank diese Produktkategorien vorhanden sind, denkt sich Herr Schnell und entwickelt die sog. “cascading parameters” – also voneinander abhängige, hierarchisch angeordnete Parameter, deren Auswahl die Wertanzahl folgender Parameter einschränkt.

Zuerst werden im Report drei statt einem Parameter erzeugt. Alle Parameter sind mehrwertig und haben keinen Standardwert.



Dann werden drei Datasets für die Parameter mit folgenden Statements angelegt:

Produktkategorie:

SELECT      ProductCategoryKey, EnglishProductCategoryName FROM  DimProductCategory

Produktunterkategorie:

SELECT        ProductSubcategoryKey, EnglishProductSubcategoryName FROM         DimProductSubcategory

WHERE        (ProductCategoryKey IN (@ProductCategory))

Produkte selbst:

SELECT        EnglishProductName, ProductKey FROM  DimProduct

WHERE        (ProductSubcategoryKey IN (@ProdSubCategory))

Schlussendlich erzeugt Herr Schnell ein Dataset für die Werteabfrage:

SELECT   DimProduct.EnglishProductName, FactInternetSales.OrderQuantity * FactInternetSales.UnitPrice AS OrderVolume, FactInternetSales.Freight FROM DimProduct INNER JOIN FactInternetSales ON DimProduct.ProductKey = FactInternetSales.ProductKey

WHERE  (FactInternetSales.ProductKey IN (@Products))


Weil Herr Schnell auf Standardwerte verzichtet hat, und alle vom vorhergehenden Parameter abhängigen Parameterabfragen ohne eine Auswahl des vorhergehenden Parameters nicht getätigt werden können, sind die Auswahlboxen der nachfolgenden Parameter solange deaktiviert, bis die nötige Vorauswahl von Kategorien oder Unterkategorien abgeschlossen ist.


 

Nun hat Herr G. Ordnet nur die Produkte zur Auswahl, deren Anzahl er vorher durch die Auswahl an Kategorien eingeschränkt hat – und Herr Schnell noch seinen Job.

Dynamische Rahmen innerhalb einer Matrix und/oder die Knechtschaft des Layouts durch das Dataset

1. Die Anforderung ist simpel:

“Die Linien innerhalb der Matrix sollen Grau und die untere Zeile soll bitte schwarz sein.”

Die meisten Reportdesigner schreiben sich solch ein Anliegen gar nicht auf, sondern setzen schlicht die Rahmenfarbe der Zeilen auf Grau und die des Subtotals auf Schwarz und reichen den Report zufrieden weiter.

Ergebnis:

2. Die Anforderung erweitert sich “gering”:

“Sehr geehrter Reportdesigner, die senkrechten Linien gefallen uns nicht, könnten Sie stattdessen einen Abstand zwischen die Länderspalten einbauen?”

Noch kommt keine Unruhe auf, flugs wird folgendes Design umgesetzt:
Auf den Zeilen wird eine neue Gruppe mit derselben Gruppierung wie die vorhergehende und im Detailfeld der Matrix einfach eine weitere Spalte eingefügt.
Die Rahmen der eingefügten Abstandsspalten bleiben unsichtbar, die Rahmenfarbe des Subtotals bleibt weiterhin schwarz.

Ergebnis:

Die Linie des Subtotals zieht sich leider über alle Spalten hinweg. Dies mag dem einen oder anderen nicht der Rede wert erscheinen, ärgerlich und unschön bleibt es trotzdem.

Konsequenz des pfiffigen Reportdesigners:

Ausblenden der Rahmen des Subtotals und Formatierung der Rahmenfarbe der Zeilengruppen und des Detailfeldes!

Dies kann durch folgende Ausdrücke erreicht werden (bei BorderStyle, Bottom=Solid)

Für die Zeilengruppen (Textbox Product) gilt:
BorderColor=IIF(RowNumber(“MyDataSetName”)=CountRows(“MyDataSetName”), “Black”,”Silver”)
d.h.:
Wenn die letzte Zeile des Datasets erreicht ist, färbe die Rahmen bitte Schwarz ein, sonst Silber.

Für die Zeile funktioniert das prima, für das Detailfeld muss aber auf die letzte Zeile der Spaltengruppe verwiesen werden, da das Scope (“MyDataSetName”) auf Detailebene nicht gültig ist.

Für das Detailfeld (Textbox mit Wertfeld) muss also gelten:
BorderColor=IIF(RowNumber(“MyColumnGroupName“)=CountRows(“MyColumnGroupName “),”Black”,”Silver”)

Und genau an dieser Stelle fängt der Ärger an.

Nehmen wir an, der Reportdesigner ist ein sparsamer Mensch und möchte sich im Dataset die leeren Zeilen für den OrderCount in Australien und Frankreich sparen und filtert diese Zeilen heraus.

MDX:

SELECT
NON
EMPTY

{

[Measures].[Reseller Order Count]

} ON
COLUMNS,

NON
EMPTY
— > !! FILTERT LEERZEILEN

{ (STRTOSET(@countryCountry,CONSTRAINED) ,


STRTOSET(@productProduct,CONSTRAINED))} ON
ROWS
FROM [Adventure Works]

Das Dataset sieht dann wie folgt aus:

Ergebnis im bereitgestellten Report: (Die Vorschau im VisualStudio zählt natürlich NICHT!)

Das Problem wird schnell klar, natürlich ist die letzte Zeile in der Ländergruppe für Australien auch die einzige,
der o.g. Ausdruck sorgt für eine schwarze Färbung und alle
nachfolgenden, vom Report zu rendernden Elemente übernehmen die Formatierung des letzten Elementes, welches noch einen direkten Bezug zur Datasetzeile bzw. den Zeilen der angegebenen Gruppe besitzt. Das Phänomen kann sowohl auf Zeilen wie auch auf Spaltengruppen beobachtet werden.

Ein weiterer Versuch, die Ausdrücke für die Rahmenfarbe anders zu formulieren:
BorderColor=IIF(InScope(“matrix1_RowGroup1″),”Silver”,”Black”)

zeigte gar kein Ergebnis für die Spalten:

Damit bleibt leider kein anderer Schluss, als die Leerzeilen für die Ländergruppierung mit ins Dataset aufzunehmen:

SELECT
NON
EMPTY

{

[Measures].[Reseller Order Count]

} ON
COLUMNS,

–non empty auskommentiert

{ (STRTOSET(@countryCountry,CONSTRAINED) ,


STRTOSET(@productProduct,CONSTRAINED))} ON
ROWS
FROM [Adventure Works]

Dataset:

Gewünschtes Ergebnis im Report: