English Deutsch

 

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.

Problem mit Excel 2007 und Analysis Services KPIs

Das Einbinden und Anzeigen von Analysis Services KPIs in Excel 2007 ist ja eine faszinierende und oft beworbene Möglichkeit. Leider zeigt sich in der Praxis oft, dass KPIs wirklich noch einmal speziell für Excel 2007 geprüft werden müssen, weil Berechnungen und Formatierungen in dem MDX, das Excel bei Abfragen generiert, oftmals nicht funktionieren. Dasselbe gilt leider auch für KPIs, die in einer SharePoint-KPI-Liste verwendet werden sollen. Unlängst ist uns aber ein besonders gemeiner Fall begegnet: beim Öffnen einer Excel-Tabelle mit enthaltener Cube-Abfrage erschien immer dieser Fehler:

 


 

Der Grund war ein auf dem Cube definierter KPI mit einer Gewichtung:

 


 

Nachdem die Gewichtung entfernt wurde, ließen sich die Excel-Tabellen wieder normal öffnen. Vielen Dank an Horst Haschke von Hiestand und Suhr für das Übermitteln der Lösung!

OLAP-Datenquellen webbasiert abfragen mit Dundas OLAP-Browser

Zahreiche ixto-Kunden wünschen sich, und damit sind sie wahrscheinlich nicht alleine, denn ihr Wunsch ist verständlich, eine Möglichkeit, OLAP-Datenquellen Web-basiert abzufragen. Die Vorteile liegen auf der Hand: das Vermeiden einer lokalen Softwareinstallation und deren Pflege sowie der damit verbunden Flexibilität. Der Markt an solchen Tools ist recht überschaubar und es findet sich sogar ein alter Bekannter in der Schar der Anbietern: Dundas Data Visualization. Bekannt vor allem durch seine Chart-Elemente für Reporting Services, die bekanntlich seit SQL Server 2008 auch in diesem integriert sind, liefert Dundas ebenfalls einen OLAP-Client für Web-basierte Anwendungen. Die Funktionalität und Tauglichkeit des solchen soll an dieser Stelle vorgestellt werden.

Zunächst ist zu bemerken, dass der Dundas Olap-Browser als Version für Windows Forms als auch für ASP.Net verfügbar ist. Da eingangs die Rede eines reinen Webclients ist, ist die Entscheidung leicht zu treffen und fällt selbstverständlich auf ASP.Net. Wie bei Dundas üblich, gibt es eine Testversion, die weder in Funktionalität noch in zeitlicher Lauffähigkeit beschränkt ist, sondern nur ein Wasserzeichen in sämtlichen Grafiken trägt, um als Demo aufzufallen. Die ist nach einer Registrierung bei Dundas schnell installiert und bringt auch gleich zahlreiche Beispiel-Anwendungen mit, die die Funktionalitäten hinreichend demonstrieren. Sie sind sind sofort einsatzbereit, da ein Offline-Cube ebenfalls zum Lieferumfang gehört, falls keine OLAP-Datenquelle zur Verfügung steht. Eigene Analysis-Services Datenbanken sind aber auch schnell angebunden, so dass wir unverzüglich in unseren Cubes browsen können.

Dem Entwickler stehen dazu zwei Wege offen:

  • Zum einen gibt es eine fertige Olap-Browser Komponte in der VisualStudio Toolbox, der direkt in eine ASP.NET-Seite eingefügt werden kann und auf den Namen OlapClient hört. Er enthält bereits alle wesentlichen Funktionen, so dass der Aufwand für Anpassungen des Layouts und der Funktionalität gegen null geht.
  • Darüber hinaus sind die einzelnen Bestandteile als Einzelkomponenten verfügbar, die sich frei positionieren und kombinieren lassen. Diese sind im Detail:
    • - OlapChart,
    • - OlapGrid,
    • - CubeSelector,
    • - CubeDimensionBrowser

    und einige mehr. Bindeglied der einzelnen Komponenten ist das OlapManager-Element. Über dieses wird definiert, dass die Komponenten auf die gleichen Daten zugreifen, Kennzahlen und Dimensionen verknüpft werden und Filter korrekt wirken.

 

Der OLAP-Browser organisiert sich in sogenannten Reports, die beliebig angelegt und verändert werden können. Einmal definierte Abfragen auf die Cubes bleiben damit verfüg- und veränderbar.

Visualisieren lassen sich die Daten innerhalb von Grafiken (Charts) und Pivottabellen (Grids). Dazu stehen die üblichen gestalterischen Möglichkeiten der Farb- und Formenwahl zur Verfügung. Drilldown, Drillthrough und weitere Cube-Operationen sind ebenfalls genauso verfügbar wie Tabellen-Operationen wie das Transponieren.

Ein Highlight ist ohne Zweifel die Web2.0-Funktionalität basierend auf dem Duo JavaScript und XML. Measures, Dimensionen und Filter lassen sich durch Drag&Drop wie bei einer lokalen Anwendung definieren, wobei sich das Look&Feel an den einschlägigen OLAP-Werkzeugen, wie dem SQL Server Management Studio orientiert.

Etwas hakelig, wenn auch trotzdem möglich, ist der Export nach Microsoft Excel. Es handelt sich hier um ein oft durch Kunden angefragtes Feature, leider ist es trotzdem keine integrierte Funktion des OLAP-Browsers. Jedoch liefern die Beispieldateien hier genügend Informationen in Form von Beispiel-Code, um den Export auch in eigene Anwendungen einzubauen.

Am Ende bleibt trotz dieser kleinen Einschränkung ein sehr positiver Eindruck eines stabilen und einfach zu handhabenden Werkzeugs, OLAP-Quellen auch ohne lokal installierte Software abzufragen.

Korrekte MDX-Berechnungen mit den Analysis Services trotz Multiselect

Seit mehreren Jahren wird immer wieder über die Problematik von berechneten Elementen in den Analysis Services in Verbindung mit Multiselects diskutiert.

 

So schrieb Mosha bereits 2005 in Writing multiselect friendly MDX calculations über die Probleme, die auftreten wenn eine Berechnung ein CURRENTMEMBER benötigt und Sets in der WHERE Clause verwendet werden.

 

Das Thema ist aber leider immer noch aktuell und wurde durch Excel 2007 und dessen Vorliebe für Subselects beim Auswählen mehrerer Elemente der gleichen Hierarchie eher noch verschlimmert. Denn dann geben die Analysis Services unter Umständen keinen Fehler mehr zurück, sondern zeigen einfach falsche Werte an.

 

Da die Hoffnung „Mit Katmai wird alles besser“ für diese Problematik auch nicht zutrifft, wird im Folgenden noch mal auf die Problematik eingegangen und anschließend mit Hilfe von SCOPE eine mögliche Lösung erklärt. Die Beispiele basieren wie immer auf dem Adventure Works Cube, den es bei Codeplex zum Download gibt.

 

Das Problem

Um das eigentliche Problem zu verstehen, benötigen wir zwei berechnete Elemente. Zum einen werden wir uns „Ratio to Parent Product“ anschauen, das bereits in der Adventure Works enthalten ist und folgende Definition hat:

 

Create Member CurrentCube.[Measures].[Ratio to Parent Product]

 

 As Case

 

        When [Product].[Product Model Categories].CurrentMember.Level.Ordinal

             = 0

        Then 1

 

        Else [Measures].[Sales Amount]

             /

             ( [Product].[Product Model Categories].CurrentMember.Parent,

               [Measures].[Sales Amount] )

 

    End,

Format_String = "Percent" ;

Zusätzlich definieren wir uns ein einfaches eigenes Measure, dass die Anzahl der Produkte für die jeweilige Auswahl anzeigt:

 

Create MEMBER CurrentCube.[Measures].[Number of Products]

AS

    COUNT(EXISTING([Product].[Product].[Product].MEMBERS));

EXISTING sorgt dabei dafür, dass nicht immer die gleiche Anzahl zurückgegeben wird, sondern das Measure für die jeweils ausgewählten Koordinaten des Cubes neu berechnet wird.

 

Nun können wir uns das ganze in Excel ansehen. Wenn wir die beiden berechneten Measures und zusätzlich noch den Sales Amount auswählen und die „Product Categories“ auf den Zeilen ausgeben, dann ergibt sich folgendes Bild:

 

clip_image002

 

Wie zu erwarten war, werden sowohl Number of Products, als auch die Ratio to Parent richtig für die einzelnen Produktkategorien angezeigt. Dass die Summe der Einzelwerte der „Number of Products“ nicht dem Gesamtergebnis entspricht, liegt übrigens daran, dass 209 Produkte dem, unglücklicherweise unsichtbaren, UnknownMember zugeschlagen wurden.

 

Wenn wir nun die Produktkategorien von den Zeilen in den Filter verschieben und dort mehrere Elemente auswählen,

 

clip_image004

 

dann nutzt Excel im Hintergrund ein SUBSELECT, um diese Einschränkung abzubilden. Das Ergebnis sieht dann folgendermaßen aus:

 

clip_image006

 

Für den Sales Amount wird korrekt die Summe der Kategorien „Accessoires“ und „Bikes“ angezeigt, was beweist, dass Excel mit seinem MDX nicht völlig daneben liegen kann. Trotzdem stimmen die Zahlen der anderen beiden Measures nicht. Es sieht aus, als wäre gar keine Einschränkung gemacht worden. Würde man sich ein Measure definieren, das [Product].[Product Model Categories].CurrentMember.Name ausgibt, dann könnte man sehen, dass genau das geschehen ist. Innerhalb der MDX-Scripts, gibt es nur eine gewählte Produktkategorie, nämlich „[All]“. Unsere berechneten Elemente bekommen von der Tatsache, dass sie in einem beschränkten Subcube laufen, nichts mit.

 

Die Lösung?

Die Grundidee, um dieses Problem zu umgehen, ist echte Measures anzulegen und diese dann per SCOPE so zu überschreiben, dass die eigentlichen Werte dann wieder per MDX ermittelt werden.

 

Beginnen wir also mit dem Anlegen eines „echten“ Measures für die Anzahl der Produkte. Da es diesen Wert auf Faktenebene nicht gibt, müssen wir uns also erst mal eine Spalte erzeugen, die wir später als Quelle für unser neues Measure nutzen können. Wir erzeugen uns also in der Data Source View für die FactSalesSummary eine leere Dummy-Spalte.

 

clip_image008

 

Dazu überarbeiten wir die Abfrage, die hinter der Faktentabelle liegt so, dass in beiden Teilen des UNIONS die neue Spalte „Dummy“ auftaucht.

 

SELECT    

     

      , NULL AS [Dummy]

FROM FactResellerSales

UNION

SELECT    

     

      , NULL AS [Dummy]

FROM FactInternetSales

 

Nun können wir diese Spalte im Cube verwenden und erzeugen uns auf dessen Basis ein neues Measure Namens „Scoped Number of Products“.

 

clip_image010

 

Dabei ist zu beachten, dass die Aggregat-Funktion, die bei Usage ausgewählt wird, später auch wirklich Anwendung findet. Während bei berechneten Elementen auf jeder Ebene im Cube die Berechnung stattfindet, wird beim SCOPING „nur“ für den explizit überschriebenen Subcube das MDX verwendet. Für alle nicht überschriebenen Bereiche wird normal aggregiert, wobei allerdings die „neuen“ Werte herangezogen werden.

 

Wir wählen also bei Usage die Summe aus und haben unser neues Measure. Nach dem Bereitstellen und Aufbereiten des überarbeiteten Cubes können wir das auch schon verwenden, erhalten aber selbstverständlich nur leere Zellen. Die eigentliche Berechnung müssen wir nun noch unter „Calculations“ definieren:

 

SCOPE ([Measures].[Scoped Number of Products], [Product].[Product].[Product].MEMBERS );

    THIS = 1;

    SCOPE ([Product].[Product].[All Products].UnknownMember);

       THIS = 0;

    END SCOPE;

END SCOPE;

 

Für alle Elemente des Levels “Product” in der Hierarchie “Product” der Dimension “Product” wird damit der Wert des Measures „Scoped Number of Products“ auf 1 gesetzt. Alle anderen Ebenen und Hierarchien, wie z. B. der All-Knoten, werden dabei ignoriert, da wir dafür ja die bereits definierte Summen-Funktion nutzen wollen.

 

Um eine Vergleichbarkeit mit dem alten berechneten Element herzustellen, verhindert das zweite SCOPE lediglich, dass das unsichtbare UnknownMember der Produkt-Hierarchie mitgezählt wird.

 

Um zu überprüfen, ob die Zahlen passen, nehmen wir das neue Measure nun in unsere erste Abfrage von oben mit auf:

 

clip_image012

 

Soweit, so gut. Spannender wird es jetzt aber, wenn wir die Kategorien wieder von den Zeilen in den Filter verschieben und nur die ersten beiden Elemente auswählen – Excel also wieder sein beliebtes SUBSELECT ausführt:

 

clip_image014

 

Wie erhofft ergibt 35 + 125 nun nicht mehr 606, sondern Excel gibt die korrekte Summe 160 zurück. Dadurch, dass wir das Measure auf der untersten Ebene der Produktdimension überschrieben haben, nutzen die Analysis Services nicht mehr bestehende Aggregate, sondern müssen alle betroffenen Produkte zur Laufzeit berechnen. Die anschließend stattfindende Aggregation der Werte wird dann nur für den betroffenen Subcube, also unter Berücksichtigung des Subselects, durchgeführt.

 

Um zu zeigen, dass dies nicht nur für simples Zählen funktioniert, erzeugen wir uns ein weiteres Measure auf Basis unserer Dummy-Spalte, nennen es „Scoped Ratio to Parent Product“ und erweitern unser MDX-Script um folgenden Ausdruck:

 

SCOPE ([Measures].[Scoped Ratio to Parent Product], [Product].[Product Model Categories].MEMBERS);

    SCOPE(DESCENDANTS([Product].[Product Model Categories].[All Products], , AFTER));

        THIS = [Measures].[Sales Amount]

             / ( [Product].[Product Model Categories].CurrentMember.Parent,

               [Measures].[Sales Amount] );

    END SCOPE;

    Format_String(THIS) = "Percent" ;

END SCOPE;

 

Dabei sorgt das DESCENDANTS(…) dafür, dass alle Elemente der Hierarchie “Product Model Categories” außer dem All-Knoten überschrieben werden. Das Ergebnis in Excel sieht dann folgendermaßen aus:

 

clip_image016

 

Auch hier sorgt das Überschreiben auf unterster Ebene dafür, dass die Berechnung richtig funktioniert. In dem Augenblick, in dem die einzelnen Knoten der ausgewerteten Hierarchie berechnet werden, gibt es eben immer ein CurrentMember, egal ob nun ein oder mehrere Knoten ausgewählt sind.

 

Einschränkungen

Zugegeben, habe ich mir das ganze hier etwas leicht gemacht. Die beiden Berechnungen einfach zu summieren, macht nämlich eigentlich keinen Sinn. Sowohl die Anzahl der Produkte, als auch die Prozente sind sogenannte semi-additive Measures, die eben nicht einfach summiert werden dürfen. Für beide fehlt also mindestens eine Sonderbehandlung für die Zeitdimension, die durch Nutzung anderer Aggregatfunktionen oder erweiterter SCOPE-Statements erreicht werden könnte, die den Rahmen dieses Beitrags sprengen würden.

 

Außerdem muss ich davor warnen jetzt ab sofort alle berechneten Elemente durch solche SCOPEs abzulösen, denn das ganze hat natürlich einen Haken. Der Grund, warum das Ganze funktioniert, nämlich das Berechnen auf unteren Ebenen, hat gleichzeitig negative Auswirkungen auf die Performance. Es können eben nicht mehr vorberechnete Werte aus den Aggregationen oder dem Cache übernommen werden. Bei jeder Abfrage, die sonst nur wenige Zellen zur Berechnung hätte nutzen müssen, muss jetzt unter Umständen ein Vielfaches an Daten berücksichtigt werden, um das gewünschte Ergebnis zu erhalten.

 

Nichts desto trotz zeigt das Ganze, dass es möglich ist, Berechnungen so zu definieren, dass sie Multiselect-fähig sind, obwohl sie Funktionen wie EXISTING oder CurrentMember benötigen. Wer mit relativ wenig Daten arbeitet, oder wem es nicht gelingt seinen Nutzern das Multiselect in Excel auszureden, der kommt so vielleicht zu einer Lösung.

Dimensionsdatensicherheit als Fehlerquelle für "unsaubere" Sets

Hallo liebe Reportbauer, MDXler und alle die es werden wollen,

ich bin vor kurzem bei einem Kunden über ein Phänomen gestolpert, dass ich Euch nicht vorenthalten will.

 

Wie immer wieder von mir gepredigt wird, arbeiten wir in der Regel zur Ausklammerung bestimmter Dimensionsknoten mit so genannten „Negativ-Listen“. Das bedeutet, wenn wir aus der Dimension Buchstaben die Knoten A, B und E sehen wollen sagen wir nicht {A, B, E} sondern {Buchstaben.ALLE – {C, D}}. Dadurch würden neue Knoten in der Dimension (z.B. der neu eingeführte Buchstabe F ;) ) nicht per Default unterdrückt, sondern wären erst mal sichtbar. In den meisten echten Anwendungsfällen ist diese Negativ-Liste oft auch deutlich kürzer als die Positiv-Liste. Aber genug über den Sinn und Unsinn von Negativ-Listen, das Statement

 

SELECT

{}

ON COLUMNS,

{[Product].[Category].[Accessories], [Product].[Category].[Bikes], [Product].[Category].[Clothing]}

ON ROWS

FROM

[Adventure Works]

 

liefert das gleiche Ergebnis zurück, wie

 

SELECT

{}

ON COLUMNS,

{[Product].[Category].[Category].

MEMBERS – [Product].[Category].[Components]} ON ROWS

FROM

[Adventure Works]

 

Nun hat sich innerhalb des besagten Kundenprojektes heimlich durchgesetzt, dass die abgekürzte Schreibweise verwendet wird, also {– {C, D}} statt { Buchstaben.ALLE – {C, D}}. Also auch die folgende dritte Variante hat das gleiche Ergebnis wie die oberen beiden.

 

SELECT

{}

ON COLUMNS,

{-{[Product].[Category].[Components]}}

ON ROWS

FROM

[Adventure Works]

 

Dies hat eigentlich auch super funktioniert, ist an Kürze kaum noch zu übertreffen UND SOLLTE TROTZDEM NIE WIEDER GETAN WERDEN ;-)

 

Implizit erkennt der AS bei dieser Schreibweise, welche Elemente auf der linken Seite dieses Komplements gehören, nämlich Buchstaben.ALLE. Sobald aber eine Dimensionssicherheit ins Spiel kommt wird’s gefährlich. AS sucht die Knoten dann nur noch in dem Bereich in dem der Nutzer berechtigt ist. Wenn er den Knoten da nicht findet, weiß er nicht, ob er nicht existiert, oder ob er nicht berechtigt ist und ersetzt ihn durch die leere Menge ({}).

 

Aus { Buchstaben.ALLE – {C, D}} würde also intern { Buchstaben.ALLE – { }}. Nicht schlimm, funktioniert super. Ihr könnt Euch aber denken, dass das nicht mehr funktioniert, wenn aus {– {C, D}} durch dieses Verhalten {-{}} wird. Und das Böse daran ist, dass das erst auftritt wenn der erste eingeschränkte Nutzer darauf zugreift. Für Euch als Entwickler sieht es aus als wäre alles fein, während der arme Benutzer mit der Meldung "Die Menge muss eine einzige Hierarchie aufweisen, um mit dem Komplementoperator verwendet zu werden." bzw. "The set must have a single hierarchy to be used with the complement operator." begrüßt wird.

 

Also kurz:

Niemals {– {C, D}}

Immer { Buchstaben.ALLE – {C, D}}  oder aber EXCEPT(Buchstaben.ALLE, {C, D})