MDX - Expansion unvollständiger Tupel-Ausdrücke

Von Lukas Hillesheim, 30. Dezember 2022
Blog Article & Copyright by Lukas Hillesheim
In diesem Artikel wird beschrieben, wie ein explizit übergebener, unvollständiger Tupel-Ausdruck wie z.B. ([East]) implizit zu einem vollständigen Tupel wie z.B. ([East], [2018], [Amount] etc.) expandiert wird. Dieses Wissen ist wesentlich, um zu verstehen, welche automatischen Verhaltensweisen zur Rückgabe eines bestimmten Wertes führen.

1. Allgemeines

Ein vollständiger Tupel-Ausdruck wird verwendet, um eindeutig auf eine einzelne Cube-Zelle und deren Wert zu verweisen. Auf die [Measures]-Dimension wird weiter unten eingegangen. Beispiel:
Figure 1 - Complete Tuple – All dimensions and hierarchies mentioned
Figure 1 - Complete Tuple – All dimensions and hierarchies mentioned
Wenn im Tupel -Ausdruck nicht alle Dimensionen bzw. Hierarchieen verwendet werden, ist der Tupel-Ausdruck unvollständig. Beispiel:
Figure 2 - Incomplete Tuple – Only [City] mentioned
Figure 2 - Incomplete Tuple – Only [City] mentioned
Um einen Wert zurückgeben zu können, muss die Engine das unvollständige Tupel expandieren und setzt dabei für jede fehlende Hierarchie bzw. Dimension implizit einen Member ein. 
Viele MDX-Anwender gehen davon aus, dass dabei jeweils der [ALL]-Member zum Zuge kommt. Die Expansion sähe dann folgendermassen aus:
Figure 3 - Tuple with [ALL]-Members
Figure 3 - Tuple with [ALL]-Members
Dies ist jedoch nicht ganz richtig. Bei der Expansion wird häufig - aber eben nicht immer - der [ALL]-Member verwendet. Es hängt vom Context ab, in dem der unvollständige Tupel aufgerufen wird. Der Context kann als ein Set von Membern der verschiedenen Hierarchieen betrachtet werden, das die vollständige Dimensionalität des Cubes darstellt. Auf Grund der Vollständigkeit kann auf den Context im Falle fehlender Hierarchie-Member zurückgegriffen werden, so wie es beim Expansions-Prozess geschieht. Durch Folgendes kann der Context gesteuert werden:
  • - Schema der Dimensionen bzw. Hierarchieen
  • - Schema des Cubes
  • - Plazierung von Tupel-Sets auf Axis 0 und Axis 1
  • - Plazierung von Tupel-Sets auf der Slicer Axis
  • - Steuern von CurrentMembern durch Attribute-Relationships
  • - Aufruf von Tupels innerhalb von Iteratoren

2. Steuern des Initial Contexts durch das Dimensions-Schema

Das Schema der Dimensionen trägt mit jeder Hierarchie dazu bei, das Set der Context-Members zu vervollständigen. Das passiert vor der Ausführung.
Jede Hierarchie verfügt über einen Default Member. Standardmäßig ist der Default-Member einer Hierarchie auf den [ALL]-Member gesetzt, kann jedoch in SSDT auch geändert werden. Vor der Ausführung werde die Default-Members jeder Hierarchie dem Initial Context hinzugefügt.
Figure 4 - AS DataTools-Project: Year-Hierarchy
Figure 4 - SSDT dimension editor: Year-Hierarchy
Figure 5 - Default Member for [Year]-Hierarchy
Figure 5 - Default Member for [Year]-Hierarchy
Figure 6 - Default: Default member not modified -> Default = [ALL]
Figure 6 - Default: no modification -> Default = [ALL]
Nachfolgend zwei Beispiele, bei denen der [Year].[ALL]-Member aufgerufen wird. Beide Tupel-Ausdrücke geben den gleichen Wert zurück. Im ersten Beispiel wird der [Year].[ALL]-Member implizit durch Expansion aufgerufen. Im zweiten Beispiel wird der [Year].[ALL]-Member explizit aufgerufen. Die Engine gibt jeweils den gleichen Zell-Wert zurück, da das Schema noch nicht modifiziert wurde.
Beispiel 1. Implizite Verwendung des [Year].[ALL]-Members:
Figure 7 - Implicit [Year].[ALL] member
Figure 7 - Implicit [Year].[ALL] member
Beispiel 2. Explizite Verwendung des [Year].[ALL]-Members. Der Wert bleibt gleich; jedoch erscheint der [ALL]-Member zusätzlich auf Axis 0.
Figure 8 - Explicit [Year].[ALL]-member
Figure 8 - Explicit [Year].[ALL]-member
Der Default-Member [ALL] für die [Year]-Hierarchie kann in SSDT auf einen spezifischen Member wie z.B. [2018] abgeändert werden:
Figure 9 - Dimension modification: default member for [Year]-hierarchy set to [2018]
Figure 9 - Schema modification: default member for [Year]-hierarchy set to [2018]
Beispiel 3. Nach der Schema-Änderung gibt der Tupel-Ausdruck an Stelle von 7869 den Wert 2058 zurück:
Figure 10 - Modified tuple expansion regarding [Year].[2018]
Figure 10 - Tuple expansion regarding [Year].[2018]
Die Tupel-Expansion würde nun immer implizit den [Year].[2018]-Member berücksichtigen:
Figure 11 – Tuple expansion after schema modification
Figure 11 - Tuple expansion regarding [Year].[2018]

3. Steuern des Initial Contexts durch das Cube-Schema

Syntaktisch betrachtet gibt es keinen Unterschied zwischen der Dimension [Measures] und jeder anderen Dimension. Der Unterschied zu [City], [Region], [Year] etc. ist, dass die Member von [Measures] nicht in einer Hierarchie organisiert sind, sondern direkt der Dimension zugeordnet sind. Der Dimension [Measures] fehlt eine Hierarchie. Ohne Hierarchie wiederum kann es aber auch keinen [ALL]-Member geben. Die Frage ist: warum verfügt [Measures] über keine Hierarchie?
Stellen Sie sich nachfolgendes Szenario vor mit den beiden [Measures]-Membern [Sum] und [PercentOfParent]:
  • - [Measures].[Sum]. Rollup mit Hilfe einer Summen-Aggregation
  • - [Measures].[PercentOfParent]. Rollup mit Hilfe einer Division
Für einen potentiellen [ALL]-Member würde sich die Frage ergeben, welche der beiden Rollup-Algorithmen – Summe bzw. Division – angewendet werden soll. Keine der beiden Algorithmen wäre jedoch sinnvoll: Weder die Verwendung von [Sum] im Rahmen einer Division, noch die Verwendung von [PercentOfParent] im Rahmen einer Addition. Die Ergebnisse aus beiden Verfahren wären für den Anwender unverständlich. Da also die Member [Amount], [Count] etc. zwar Bestandteil der [Measures]-Dimension sind, aber nicht sinnvoll innerhalb einer Hierarchie organisiert werden können, existiert in der [Measures]-Dimension folglich keine Hierarchie und somit auch kein [ALL]-Member.
Stattdessen wird in der [Measures]-Dimension einer der Member als Default Member festgelegt. Dies geschieht über die Eigenschaften des Cubes:
Figure 12 - Cube Schema
Figure 12 - Cube Schema
Standardmäßig ist die „DefaultMeasure“-Eigenschaft nicht festgelegt. In diesem Fall nimmt der Cube den ersten Member der [Measures]-Dimension, den er vorfindet – also im Beispiel [Amount] - und setzt ihn als Default. Das Standard-Verhalten läßt sich wie folgt ändern:
Figure 13 - Cube Schema: Modify Default Member
Figure 13 - Cube Schema: Modify Default Member
Unter der Voraussetzung, dass der [Measures].[Count]-Member zum Default gemacht würde, ergäbe sich für die "Berlin"-Abfrage wieder ein anderes Ergebnis:
Figure 14 - Modified tuple expansion regarding [Year].[2018] and [Measures].[Count]
Figure 14 - Tuple expansion regarding [Year].[2018] and [Measures].[Count]
Die Tupel-Expansion würde nun immer implizit den [2018]-Member und den [Count]-Member berücksichtigen:
Figure 15 -  Tuple expansion regarding [Year].[2018] and [Measures].[Count]
Figure 15 - Tuple expansion regarding [Year].[2018] and [Measures].[Count]

4. Steuern des Runtime Contexts durch die Tupel-Sets auf Axis 0, Axis 1 und Slicer Axis

Auf jeder Query-Axis kann ein Tupel-Set plaziert werden. Die in den Tupel-Sets verwendeten Hierarchieen mit ihren jeweiligen Membern tragen zur Expansion bei. Syntaktisch werden Axis 0, 1 und die Slicer Axis gleich behandelt. Der Unterschied zwischen Axis 0/1 und der Slicer Axis ist einzig, dass die Tupels auf der Slicer Axis nicht für die Anzeige verwendet werden.
Im nachfolgenden Beispiel befindet sich
  • - Ein 2-dimensionales Tupel-Set mit 2 Tupels auf Axis 0
  • - Ein 1-dimensionales Tupel-Set mit 2 Tupels auf Axis 1
  • - Ein 1-dimensionales Tupel auf der Slicer Axis
Bitte beachten Sie bzgl. der Slicer Axis, dass es zwei syntaktische Varianten gibt. Der Slicer kann ein einzelnes Tupel oder auch ein Tupel-Set sein.
Figure 16 - Runtime context
Figure 16 - Runtime context
Figure 17 - Matrix (result) with 4 cells
Figure 17 - Matrix (result) with 4 cells
Analysis Services gibt in dem Beispiel vier Zellen mit den Werten 13, 15, 15 und 12 zurück. Für die Generierung der ersten Zelle mit dem Wert 13 ergibt sich durch Expansion folgendes Tupel:
Figure 18 - Cell 1: expanded tuple
Figure 18 - Cell 1: expanded tuple
Für die Generierung der zweiten Zelle mit dem Wert 15 ergibt sich durch Expansion folgendes Tupel:
Figure 19 - Cell 2: expanded tuple
Figure 19 - Cell 2: expanded tuple

5. Expansions-Prozess bei verschachtelten Contexts

Man kann sich die Generierung des Outputs – also das darzustellende Grid aus Abschnitt 4. – als Prozess vorstellen, der in einer Schleife abläuft. Dabei tragen die Tupels auf Axis 0, Axis 1 und Slicer Axis als aktuelle Tupels zur Expansion bei. Anders ausgedrückt: Durch die Iteration innerhalb des Generierungs-Prozesses ändert sich der Kontext für das jeweils zu evaluierende Tupel und modifiert dadurch den Prozess der Tupel-Expansion.
Es existieren im nachfolgenden Beispiel konzeptionell zwei Kontexte: der Initial Context wird vor der Ausführung gebildet und enthält die Members, die durch das Dimensions- und Cube-Schema als Default festgelegt wurden. Der Runtime Kontext entsteht im Rahmen der Iteration. Das Zusammenwirken beider Kontexte kann man sich als Vererbungs-Prozess vorstellen, bei dem der Runtime-Kontext als zuletzt angewandter Kontext eine überschreibende Wirkung hat (Last-Win-Rule). Als Ergebnis der Zusammenführung beider Kontexte entsteht ein resultierender Kontext.
Aus Sicht des MDX-Anwenders ist der Effekt der Kontext-Verschachtelung der, dass alle Member, die er nicht ausdrücklich in den Tupel-Expressions aufgerufen hat, von der Engine aus dem resultierenden Kontext entnommen und für die Expansion verwendet werden.
Der folgende OOP-Pseudo-Code veranschaulicht die Generierung der Matrix innerhalb einer Iteration:
Figure 20 - OOP-Pseudocode: Matrix generation
Figure 20 - OOP-Pseudocode: Matrix generation
  • - Eine Funktion „GetMatrix“ erzeugt das Grid, also den visuellen Output
  • - Es wird zunächst der Initial Context erstellt. Dieser erhält aus dem Schema alle Default-Member
  • - Der Anwender plaziert Tupel-Sets auf Axis 0, Axis 1 und der Slicer Axis
  • - Die dreifach verschachtelte Schleife iteriert über alle Tupel-Sets
  • - Die Funktion „GetCellValue“ wird aufgerufen. Es werden alle CurrentMembers der Tupel-Sets übergeben. Außerdem wird auch der initial Context übergeben.
Figure 21 - Compute cell value regarding current tuple and context
Figure 21 - Compute cell value regarding current tuple and context
  • - Die Funktion „GetCellValue“ gibt für eine bestimmte Zelle den Wert zurück. Die „Koordinaten“ der Zelle werden aus einem Tupel gebildet.
  • - Zuerst werden dem Tupel alle Member des Initial Context hinzugefügt
  • - Dann werden die current Members hinzugefügt, also z.B. [Berlin], [2018] etc.
  • - Hierdurch ergeben sich Überschneidungen; denn für die Hierarchie [City] existiert schon der Default Member [ALL] aus dem initial Context. Die Pseudo-Funktion „Merge“ soll darstellen, dass die Current Members bereits vorhandene Member überschreiben. Nur die Member, die nicht in der Liste der Current Members enthalten sind, werden aus dem initial Context übernommen.
Der OOP-PseudoCode enthält die Expression „CurrentMember“ wie z.B. in „t1.Hierarchies[0].CurrentMember“. Hiermit soll dargestellt werden, dass es in jeder Hierarchie zu jedem Zeitpunkt einen aktuellen Member gibt und dass dieser CurrentMember zur Laufzeit im Rahmen einer Iteration geändert wird. „CurrentMember“ existiert auch als Funktion im MDX-Sprachschatz. Hier hat die Funktion die gleiche Aufgabe wie „CurrentMember“ im OOP-PseudoCode: es soll für jede Hierarchie der aktuelle Member zurück gegeben werden.
Definition des Expansions-Prozesses: Bei der Expansion werden alle Member des aktuellen Tupels zusammen mit den CurrentMembers des resultierenden Kontexts verwendet, um einen vollständigen Tupel-Ausdruck zu generieren.

6. Steuern des Runtime Context durch Attribute-Relationships

Im nachfolgenden MDX-Beispiel wird die „CurrentMember“-Funktion verwendet, um sozusagen „aktiv“ auf die aktuellen Member der Hierarchieen [Region] und [Year] zuzugreifen und sie als Calculated Member darzustellen. Bitte beachten Sie, dass ein Member keinen darstellbaren Wert hat. Um beispielsweise „East“ anzeigen zu können, muss die Name-Property des [East]-Members benutzt werden.
Figure 22 - Attribute relationships: impact on CurrentMember
Figure 22 - Attribute relationships: impact on CurrentMember
Das Ergebnis überrascht: Während [Year].CurrentMember konstant [ALL] zurückgibt, wechselt [Region].CurrentMember mit jedem [City]-Member. Der Grund hierfür ist, dass [City] und [Region] beide Teil der [dCustomer]-Dimension sind und es eine Attribute-Relationship zwischen [City] und [Region] gibt. Dagegen befinden sich [City] und [Year] in verschiedenen Dimensionen und können per Definition nicht über eine Attribute-Relationship miteinander in Beziehung stehen.
Die Wirkung der Attribute-Relationship auf den CurrentMember ist, dass – ausgehend von der N-Seite – der CurrentMember der 1-Seite gesetzt wird. Während also eine Iteration über die [City]-Members stattfindet, sorgt die Attribute-Relationship im Hintergrund dafür, dass auch der [Region].CurrentMember sich laufend ändert.

7. Steuern des Runtime Context durch Iteratoren

Abschnitt 5. zeigt, wie Initial Context und Runtime Context sich auf den Expansions-Prozess auswirken. Es gilt das Prinzip der Vererbung (s.o.). Die Verschachtelung von Kontexten endet nicht beim Runtime Kontext, sondern jeder Iterator erstellt einen eigenen, neuen Kontext, der – zusammen mit den bereits existierenden Kontexten – einen neuen resultierenden Kontext zur Folge hat.
Als Beispiel wird nachfolgend die MDX-Funktion „Generate“ verwendet. Mit Hilfe von Generate kann über ein Set iteriert werden und pro Iterations-Vorgang eine zweite Set-Expression angewandet werden. Von den beiden Syntax-Varianten, die „Generate“ bietet, wird die Variante verwendet, bei der die Resultate der jeweiligen Schleifendurchgänge zu einem String verkettet werden. Als Separator dient in diesem Beispiel das Komma.
Im Beispiel gibt „Generate“ die [City]-Members zurück, die sich in der jeweiligen Region befinden. An dieser Stelle möchte ich nicht auf die Details der relativ komplexen Funktion „Generate“ eingehen, sondern auf die rot markierte Stelle im Code verweisen: Hier wird die Expression "[City].CurrentMember" aufgerufen. Betrachtet man die MDX-Funktion "Generate" als funktionales Pendant zu einer Schleife, dann ist die Expression "[City].CurrentMember" sozusagen im Schleifenblock des Iterators plaziert und gibt jeweils das aktuelle Element der Schleife zurück.
Würde sich [City].CurrentMember nicht ändern, wäre der Code ziemlich sinnlos. Tatsächlich aber zeigt das Beispiel, dass durch „Generate“ ein neuer modifizierter Kontext entsteht, in dem es einen aktuellen [City]-Member gibt.
Figure 23 - Context nesting through iterators
Figure 23 - Context nesting through iterators
Dem letzten Beispiel ist noch hinzuzufügen, dass bei weiteren verschachtelten Kontexten das Problem der Schleifen-Variable entsteht. Stellen Sie sich folgenden OOP-Pseudocode vor:
Figure 24 - Nested iterators & unique variable names
Figure 24 - Nested iterators & unique variable names
In MDX stellt "CurrentMember" das funktionale Pendant zu einer Schleifen-Variablen dar. Im Falle einer zweifach verschachtelten Schleife würde jedoch die Expression "[City].CurrentMember" keinen eindeutigen Bezug zu einer bestimmten Schleife herstellen. In diesem Fall wären andere Patterns und Techniken notwendig. Die möglichen Lösung hierfür sind jedoch nicht mehr Teil des Artikels.