Zum Inhalt springen

Fortgeschritten: virtuelle Cubes, Parent-Child, berechnete Members

Diese Seite behandelt fünf fortgeschrittene Modellierungsmuster, die über den Standard-Sternschema-Cube hinausgehen: das Kombinieren mehrerer Fakten-Tabellen in einem Cube, Parent-Child-Hierarchien, im Schema definierte berechnete Members, wiederverwendbare benannte Sets und Bridge-Dimensionen (Many-to-Many).

Multi-Fakten-Cubes (die Mondrian-4-Antwort auf “virtuelle Cubes”)

Mondrian 3 enthielt ein <VirtualCube>-Element, das zwei oder mehr reguläre Cubes zusammenfügte. Mondrian 4 hat kein <VirtualCube>-Element. Die Funktionalität wurde in das reguläre <Cube>-Modell integriert: Ein einzelner Cube kann mehrere <MeasureGroup>-Elemente enthalten, die jeweils auf eine andere Fakten-Tabelle verweisen. Damit erreichen Sie alles, was ein virtueller Cube tat – mit weniger Aufwand und besserer Abfrageplanung.

Sie verwenden einen Cube mit mehreren Measure-Groups, wenn Sie Folgendes haben:

  • Fakten-Tabellen mit unterschiedlichen Granularitäten – etwa eine auf Tagesebene und eine auf Monatsebene.
  • Fakten-Tabellen mit unterschiedlicher Dimensionalität – etwa eine, die Product, Time und Customer abdeckt, und eine andere, die Product, Time und Warehouse abdeckt.
  • Eine Aggregat-Tabelle, die Sie zusammen mit der Basis-Faktentabelle registrieren möchten (ein gängiges Muster – aggregierte Measure-Groups verwenden type="aggregate").

Beispiel: Sales und Warehouse in einem Cube

Warehouse and Sales:
dimensions:
- source: "Time"
- source: "Product"
- source: "Store"
- source: "Customer"
- source: "Warehouse"
measure_groups:
- name: "Sales"
table: "sales_fact_1997"
measures:
- name: "Unit Sales"
column: "unit_sales"
aggregator: "sum"
format_string: "Standard"
- name: "Store Sales"
column: "store_sales"
aggregator: "sum"
format_string: "#,###.00"
- name: "Store Cost"
column: "store_cost"
aggregator: "sum"
format_string: "#,###.00"
dimension_links:
- type: "foreign_key"
dimension: "Time"
foreign_key_column: "time_id"
- type: "foreign_key"
dimension: "Product"
foreign_key_column: "product_id"
- type: "foreign_key"
dimension: "Store"
foreign_key_column: "store_id"
- type: "foreign_key"
dimension: "Customer"
foreign_key_column: "customer_id"
- type: "no_link"
dimension: "Warehouse"
- name: "Warehouse"
table: "inventory_fact_1997"
measures:
- name: "Units Ordered"
column: "units_ordered"
aggregator: "sum"
- name: "Warehouse Sales"
column: "warehouse_sales"
aggregator: "sum"
- name: "Warehouse Cost"
column: "warehouse_cost"
aggregator: "sum"
dimension_links:
- type: "foreign_key"
dimension: "Time"
foreign_key_column: "time_id"
- type: "foreign_key"
dimension: "Product"
foreign_key_column: "product_id"
- type: "foreign_key"
dimension: "Store"
foreign_key_column: "store_id"
- type: "foreign_key"
dimension: "Warehouse"
foreign_key_column: "warehouse_id"
- type: "no_link"
dimension: "Customer"
calculated_members:
- name: "Profit Per Unit Shipped"
dimension: "Measures"
formula: "([Measures].[Store Sales] - [Measures].[Store Cost]) / [Measures].[Units\
\ Ordered]"

Wie Mondrian nicht-konforme Dimensionen behandelt

Dimensionen, die von beiden Measure-Groups gemeinsam genutzt werden – Time und Product im obigen Beispiel – sind konforme Dimensionen. Mondrian synchronisiert den Kontext über die Measure-Groups hinweg automatisch für diese. Wenn der aktuelle Kontext [Time].[1997].[Q2] und [Product].[Beer] ist, werden Measures aus beiden Gruppen korrekt aufgelöst.

Dimensionen, die nur zu einer Measure-Group gehören, sind nicht konform. Wenn der aktuelle Kontext [Customer].[Jane Smith] enthält, gibt eine Measure aus der Warehouse-Gruppe (die <NoLink dimension="Customer"/> hat) NULL anstelle eines falschen Aggregats zurück. Dies ist das korrekte und erwartete Verhalten.

Aggregierte Measure-Groups

Wenn Sie eine Aggregat-Tabelle zusammen mit ihrer Basis-Faktentabelle registrieren, verwendet die aggregierte Measure-Group type="aggregate" und <CopyLink> anstelle von <ForeignKeyLink> für aggregierte Dimensionen. Vollständige Details finden Sie im Abschnitt Physisches Schema – CopyLink.

Parent-Child-Hierarchien

Eine konventionelle Hierarchie hat eine feste Anzahl von Ebenen, und jeder Member auf einer bestimmten Ebene hat seinen Parent auf der Ebene darüber. Eine Parent-Child-Hierarchie hat nur eine reale Ebene (plus den optionalen All-Member), aber Members können andere Members innerhalb derselben Ebene als Parent haben. Dies ist das richtige Modell für Organigramme, Produktbäume, Konten im Hauptbuch, geografische Regionen mit variabler Tiefe und ähnliche rekursive Strukturen.

Eine Parent-Child-Hierarchie in Mondrian 4 definieren

In Mondrian 4 drücken Sie die Parent-Child-Struktur auf einem <Attribute> innerhalb einer <Dimension> aus. Fügen Sie das Attribut parentAttribute hinzu, das auf das Attribut zeigt, welches den Parent-Schlüssel jedes Members enthält. In der FoodMart-Dimension Employees ist die selbstreferenzielle Spalte supervisor_id:

<Dimension name="Employee" table="employee" key="Employee Id">
<Attributes>
<Attribute name="Employee Id" keyColumn="employee_id" nameColumn="full_name"/>
<Attribute name="Manager Id" keyColumn="supervisor_id"/>
<!-- additional descriptive attributes -->
<Attribute name="Position Title" keyColumn="position_title" hasHierarchy="false"/>
<Attribute name="Gender" keyColumn="gender" hasHierarchy="false"/>
<Attribute name="Education Level" keyColumn="education_level" hasHierarchy="false"/>
</Attributes>
<Hierarchies>
<Hierarchy name="Employees" allMemberName="All Employees">
<Level attribute="Employee Id" parentAttribute="Manager Id" nullParentValue="0"/>
</Hierarchy>
</Hierarchies>
</Dimension>

Wichtige Attribute auf <Level>:

  • parentAttribute – der Name des Attributs, das den Parent-Schlüssel jedes Members liefert. Dieses einzelne Attribut ist für Mondrian das Signal, dass die Hierarchie eine Parent-Child-Hierarchie ist.
  • nullParentValue – der Wert, der “kein Parent” anzeigt (d. h. einen Root-Member). Standardwert ist null, aber viele Schemas verwenden stattdessen 0 oder -1, da einige Datenbanken keine Null-Werte indexieren.

Parent-Child-Hierarchien optimieren

Die naive Implementierung des Parent-Child-Rollups ist teuer: Mondrian muss eine SQL-Anweisung pro Knoten ausführen, um alle Nachkommen zu summieren. Für flache Hierarchien mit einigen Dutzend Members ist dies akzeptabel. Bei tieferen oder breiteren Bäumen – Hunderten oder Tausenden von Members – werden Sie eine deutliche Leistungseinbuße bemerken. Es gibt auch eine zweite Einschränkung: Sie können keine distinct-count-Measure in einem Cube definieren, der eine nicht optimierte Parent-Child-Hierarchie enthält, da Mondrian die erforderliche Deduplizierung nicht in Standard-SQL ausdrücken kann.

Die Lösung ist eine Closure-Tabelle.

Closure-Tabellen

Eine Closure-Tabelle ist eine flache SQL-Tabelle, die jedes Vorfahre-Nachkomme-Paar in jeder Tiefe vorberechnet. Für die employee-Tabelle sieht das so aus:

supervisor_idemployee_iddistance
110
121
132
141
153
162
220
231
252
261
330
351
440
550
660

Jede Zeile sagt: “Mitarbeiter X ist ein Nachkomme von Supervisor Y in Tiefe D”. Entscheidend ist, dass jeder Mitarbeiter als sein eigener Nachkomme bei Distanz 0 erscheint (der reflexive Closure). Mit dieser Tabelle kann Mondrian jedes Teilbaum-Aggregat mit einem einzigen SQL-Join berechnen – keine Iteration erforderlich.

Sie deklarieren die Closure-Tabelle auf dem <Level>-Element über das <Closure>-Kind:

<Hierarchy name="Employees" allMemberName="All Employees">
<Level attribute="Employee Id" parentAttribute="Manager Id" nullParentValue="0">
<Closure table="employee_closure"
parentColumn="supervisor_id"
childColumn="employee_id"/>
</Level>
</Hierarchy>

Das physische Schema benötigt außerdem einen <Link>, damit Mondrian von employee auf employee_closure joinen kann:

<Table name="employee" keyColumn="employee_id"/>
<Table name="employee_closure"/>
<Link source="employee_closure" target="employee" foreignKeyColumn="employee_id"/>

Für beste Performance fügen Sie die folgenden Indizes hinzu:

CREATE UNIQUE INDEX employee_closure_pk
ON employee_closure (supervisor_id, employee_id);
CREATE INDEX employee_closure_emp
ON employee_closure (employee_id);

Deklarieren Sie sowohl supervisor_id als auch employee_id als NOT NULL – einige Datenbankoptimierer behandeln nicht-null-indexierte Spalten deutlich besser.

Closure-Tabellen befüllen

Mondrian befüllt die Closure-Tabelle nicht – das ist Aufgabe der ETL-Schicht. Die Tabelle muss aktualisiert werden, wenn sich die Hierarchie ändert. Wenn Sie Pentaho Data Integration (Kettle) verwenden, gibt es einen integrierten Schritt Closure Generator, der dies automatisch als Teil Ihrer Lade-Pipeline übernimmt.

Wenn Sie Kettle nicht verwenden, können Sie die Tabelle mit einer Stored Procedure befüllen. Hier ein MySQL-Beispiel, das Selbst-Paare anlegt und dann eine Tiefenebene nach der anderen nach außen iteriert:

DELIMITER //
CREATE PROCEDURE populate_employee_closure()
BEGIN
DECLARE distance INT;
TRUNCATE TABLE employee_closure;
SET distance = 0;
-- seed with self-pairs (distance 0)
INSERT INTO employee_closure (supervisor_id, employee_id, distance)
SELECT employee_id, employee_id, distance
FROM employee;
-- for each (root, leaf) in the closure add (root, leaf->child)
REPEAT
SET distance = distance + 1;
INSERT INTO employee_closure (supervisor_id, employee_id, distance)
SELECT ec.supervisor_id, e.employee_id, distance
FROM employee_closure ec
JOIN employee e ON ec.employee_id = e.supervisor_id
WHERE ec.distance = distance - 1;
UNTIL (ROW_COUNT() = 0)
END REPEAT;
END //
DELIMITER ;

Führen Sie diese Prozedur nach jedem Ladevorgang aus, der die employee-Tabelle modifiziert.

Berechnete Members

Ein berechneter Member ist ein Cube-Member, dessen Wert aus einer MDX-Formel stammt und nicht aus einer Spalte der Faktentabelle. Sie können berechnete Members auf der Dimension Measures (zur Erzeugung berechneter Measures) oder auf jeder anderen Dimension im Cube definieren.

Anstatt die Formel in jeder MDX-Abfrage mit einer WITH MEMBER-Klausel zu wiederholen, definieren Sie sie einmal im Schema, und sie ist automatisch in allen Abfragen gegen diesen Cube verfügbar:

calculated_members:
- name: "Profit"
dimension: "Measures"
formula: "[Measures].[Store Sales] - [Measures].[Store Cost]"
properties:
- name: "FORMAT_STRING"
value: "$#,##0.00"

Sie können die Formel auch als XML-Attribut schreiben, wenn Sie Kürze bevorzugen:

calculated_members:
- name: "Profit"
dimension: "Measures"
formula: "[Measures].[Store Sales] - [Measures].[Store Cost]"
properties:
- name: "FORMAT_STRING"
value: "$#,##0.00"

Beide Formen liefern identische Ergebnisse – und da der Konverter sowohl das Kind <Formula> als auch das Attribut formula in denselben YAML-Schlüssel formula: zusammenfasst, ergeben beide XML-Schreibweisen das gleiche, oben gezeigte YAML.

CalculatedMemberProperty

<CalculatedMemberProperty> setzt MDX-Solve-Order-Eigenschaften, Format-Strings und Typinformationen auf den berechneten Member. Die am häufigsten verwendeten Eigenschaften sind:

PropertyZweckBeispielwert
FORMAT_STRINGSteuert, wie der Wert in Clients dargestellt wird"$#,##0.00"
DATATYPETeilt XMLA-Clients den Rückgabetyp mit"Numeric", "Integer", "String"
SOLVE_ORDERPriorität, wenn mehrere berechnete Members im Geltungsbereich sind"2000"

FORMAT_STRING kann anstelle eines Literals einen bedingten Ausdruck enthalten:

calculated_members:
- name: "Conditional Profit"
dimension: "Measures"
formula: "[Measures].[Store Sales] - [Measures].[Store Cost]"
properties:
- name: "FORMAT_STRING"
expression: "Iif(Value < 0, '|($#,##0.00)|style=red', '|$#,##0.00|style=green')"

Wenn Mondrian eine Zelle rendert, wertet es zuerst den Ausdruck aus, um einen Format-String zu erhalten, und wendet diesen Format-String dann auf den Zellwert an.

Sichtbarkeit

Setzen Sie visible="false" auf einem <CalculatedMember> (oder auf einer <Measure>), um ihn aus den Member-Browsern von Client-Tools auszublenden. Dies ist nützlich, wenn Sie ein Ergebnis über Zwischenschritte aufbauen, die nicht direkt offengelegt werden sollen:

<Measure name="Store Cost" column="store_cost" aggregator="sum"
formatString="#,###.00" visible="false"/>
<CalculatedMember name="Margin" dimension="Measures" visible="false">
<Formula>([Measures].[Store Sales] - [Measures].[Store Cost]) / [Measures].[Store Cost]</Formula>
</CalculatedMember>
<CalculatedMember name="Store Sqft" dimension="Measures" visible="false">
<Formula>[Store].Properties("Sqft")</Formula>
</CalculatedMember>
<CalculatedMember name="Margin per Sqft" dimension="Measures" visible="true">
<Formula>[Measures].[Margin] / [Measures].[Store Cost]</Formula>
<CalculatedMemberProperty name="FORMAT_STRING" value="$#,##0.00"/>
</CalculatedMember>

Nur “Margin per Sqft” erscheint im Client; die anderen sind Hilfsmittel.

Berechnete Members in Multi-Fakten-Cubes

<CalculatedMembers> befindet sich auf der <Cube>-Ebene und kann Measures aus jeder der Measure-Groups des Cubes referenzieren. Dies ist der natürliche Platz für KPIs über mehrere Faktentabellen hinweg:

calculated_members:
- name: "Profit Growth"
dimension: "Measures"
visible: true
formula: "([Measures].[Profit] - [Measures].[Profit last Period]) / [Measures].[Profit\
\ last Period]"
properties:
- name: "FORMAT_STRING"
value: "0.0%"

Benannte Sets

Ein benanntes Set ist ein wiederverwendbarer MDX-Set-Ausdruck, der im Schema definiert ist. Es ist das Schema-Äquivalent einer WITH SET-Klausel in einer MDX-Abfrage. Einmal definiert, ist das Set implizit in jeder Abfrage gegen den Cube (oder das Schema) verfügbar, in dem es deklariert ist.

Benannte Sets auf Cube-Ebene

Deklarieren Sie ein benanntes Set innerhalb eines <Cube>, um es für alle Abfragen gegen diesen Cube verfügbar zu machen:

named_sets:
- name: "Top Sellers"
formula: "TopCount([Warehouse].[Warehouse Name].MEMBERS, 5, [Measures].[Warehouse\
\ Sales])"

Sie können dann [Top Sellers] direkt in MDX verwenden:

SELECT
{[Measures].[Warehouse Sales]} ON COLUMNS,
{[Top Sellers]} ON ROWS
FROM [Warehouse]
WHERE [Time].[Year].[1997]

Was Folgendes zurückgeben könnte:

WarehouseWarehouse Sales
Treehouse Distribution31,116.37
Jorge Garcia, Inc.30,743.77
Artesia Warehousing, Inc.29,207.96
Jorgensen Service Storage22,869.79
Destination, Inc.22,187.42

Benannte Sets auf Schema-Ebene

Sie können benannte Sets auch auf Schema-Ebene außerhalb eines Cubes deklarieren. Benannte Sets auf Schema-Ebene sind über alle Cubes des Schemas hinweg verfügbar, aber nur in Cubes gültig, die die Dimensionen enthalten, auf die die Formel verweist:

<Schema name="FoodMart">
<Cube name="Sales" .../>
<Cube name="Warehouse" .../>
<NamedSets>
<NamedSet name="CA Cities"
formula="{[Store].[USA].[CA].Children}"/>
<NamedSet name="Top CA Cities">
<Formula>TopCount([CA Cities], 2, [Measures].[Unit Sales])</Formula>
</NamedSet>
</NamedSets>
</Schema>

[CA Cities] ist in jedem Cube gültig, der eine [Store]-Dimension hat. Die Verwendung in einem Cube ohne diese Dimension löst einen Fehler zur Abfragezeit aus, nicht zur Ladezeit des Schemas.

Das Attribut formula und das untergeordnete Element <Formula> sind äquivalent. Verwenden Sie die Attributform für kurze Ausdrücke, das Kindelement für bessere Lesbarkeit bei längeren.

Bridge-Dimensionen (Many-to-Many)

Eine normale Dimension hat eine Eins-zu-Viele-Beziehung zum Fact: Jede Fact-Zeile gehört zu genau einem Kunden, einem Produkt, einem Tag. Eine Many-to-Many-Beziehung (oder Bridge) ist anders – eine einzelne Fact-Zeile kann zu mehreren Dimension-Members gleichzeitig gehören.

Das klassische Beispiel ist ein gemeinsames Bankkonto. Ein Konto hat einen Saldo, kann aber von zwei oder mehr Kunden gemeinschaftlich gehalten werden:

ACCOUNTS (fact) OWNERSHIP (bridge)
acct year balance acct customer weight
1 2024 1000 1 Alice 0.50
2 2024 500 1 Bob 0.50
3 2025 300 2 Bob 1.00
3 Alice 0.25
total balance = 1800 3 Carol 0.75

Konto 1 ist im Besitz sowohl von Alice als auch von Bob. Es gibt keine einzelne customer_id-Spalte, die Sie auf die Fact-Tabelle setzen können, daher kann ein einfacher <ForeignKeyLink> dies nicht modellieren. Stattdessen lebt die Beziehung in einer separaten Bridge-Tabelle (OWNERSHIP), die Konten zu Kunden zuordnet, optional mit einer Eigentums-Gewichtung.

Das Fan-Out-Problem

Der naive Weg, “Saldo nach Kunde” zu beantworten, ist, fact → bridge → customer zu joinen und SUM(balance) zu bilden. Aber dieser Join expandiert (fan-out): Die einzelne $1000-Zeile von Konto 1 wird zu zwei Zeilen (eine für Alice, eine für Bob). Summieren Sie naiv über alle Kunden, erhalten Sie 3100, nicht die echten 1800 – Konto 1 doppelt gezählt, Konto 3 doppelt gezählt. Dieses Doppelzählen ist die Kerngefahr der Many-to-Many-Modellierung.

Saikus Mondrian behandelt den Fan-Out korrekt mit einem <BridgeLink>.

Ein <BridgeLink> ersetzt den <ForeignKeyLink> für die Many-to-Many-Dimension innerhalb der <DimensionLinks> der Measure-Group:

measure_groups:
- name: "Balances"
table: "account_fact"
measures:
- name: "Balance"
column: "balance"
aggregator: "sum"
dimension_links:
- type: "foreign_key"
dimension: "Date"
foreign_key_column: "date_key"
- type: "bridge"
dimension: "Customer"
bridge_table: "account_owner"
fact_foreign_key_column: "account_id"
bridge_fact_key_column: "account_id"
bridge_dimension_key_column: "customer_id"
AttributErforderlichBeschreibung
dimensionjaDie Many-to-Many-Dimension, die dieser Link auflöst.
bridgeTablejaDie physische Tabelle, die das Mapping Fact↔Dimension enthält. Muss im <PhysicalSchema> deklariert sein.
factForeignKeyColumnjaSpalte auf der Fact-Tabelle, auf die der Bridge zurückjoint (der Fact-Granularitätsschlüssel).
bridgeFactKeyColumnjaSpalte auf der Bridge, die factForeignKeyColumn entspricht.
bridgeDimensionKeyColumnjaSpalte auf der Bridge, die dem Dimensionsschlüssel entspricht.
aggregationneinfullCount (Standard) oder weighted. Siehe unten.
weightColumnneinSpalte auf der Bridge mit der Allokationsgewichtung. Erforderlich, wenn aggregation="weighted".

Die Bridge-Dimension erfordert außerdem, dass die Fact-Granularität deklariert wird, damit Saiku den Fan-Out deduplizieren kann. Deklarieren Sie sie als <Key> der Faktentabelle im physischen Schema:

tables:
- name: "account_fact"
key:
- "account_id"

Allokation: full-count vs. weighted

Es gibt zwei ehrliche Möglichkeiten, einen gemeinsam genutzten Fact-Wert seinen Eigentümern zuzuordnen.

fullCount (Standard) schreibt jedem Eigentümer den gesamten Wert zu. Jeder Kunde sieht den vollen Saldo jedes Kontos, auf dem er steht:

Balance by Customer (fullCount):
Alice 1300 (acct1 1000 + acct3 300)
Bob 1500 (acct1 1000 + acct2 500)
Carol 300 (acct3 300)

Die Zahlen pro Kunde überlappen sich absichtlich – Alice und Bob sehen beide die vollen $1000 von Konto 1 – das ist es, was Sie für die Frage “Über wie viel Saldo hat dieser Kunde Unterschriftsberechtigung?” wollen. Wann immer Full-Count-Summen über Eigentümer hinweg aggregiert werden – die Gesamtsumme (All Customers) oder eine Zwischenebene (siehe Mehrstufige Bridge-Dimensionen unten) – wendet Saiku ein symmetrisches Aggregat an: Es dedupliziert zurück auf die Fact-Granularität, bevor summiert wird, sodass die Gesamtsumme die echten 1800 ist und nicht die expandierten 3100.

weighted teilt jeden Wert anhand der Gewichtsspalte des Bridges, sodass die Teile das Ganze ergeben:

dimension_links:
- type: "bridge"
dimension: "Customer"
bridge_table: "account_owner"
fact_foreign_key_column: "account_id"
bridge_fact_key_column: "account_id"
bridge_dimension_key_column: "customer_id"
aggregation: "weighted"
weight_column: "weight"
Balance by Customer (weighted):
Alice 575 (1000×0.50 + 300×0.25)
Bob 1000 (1000×0.50 + 500×1.00)
Carol 225 (300×0.75)
total 1800 (reconciles exactly)

Verwenden Sie weighted für “Was ist der wirtschaftliche Anteil dieses Kunden?”, und wenn sich die Gewichtungen pro Fact-Zeile zu 1 summieren, gleicht jede Ebene – einschließlich der Gesamtsumme – automatisch mit dem Fact-Total ab.

Mehrstufige Bridge-Dimensionen

Eine Bridge-Dimension ist nicht auf eine einzelne Ebene beschränkt – sie kann eine Hierarchie haben, und Full-Count-Rollups bleiben auf jeder Ebene korrekt. Angenommen, jeder Kunde gehört zu einem Segment (Alice und Bob sind Premium, Carol ist Standard) und Sie rollen den Bridge bis auf das Segment hoch:

shared_dimensions:
Customer:
table: "dim_customer"
key: "Customer"
attributes:
- name: "Segment"
key_column: "segment"
- name: "Customer"
key_column: "customer_id"
name_column: "customer_name"
hierarchies:
- name: "By Segment"
all_member_name: "All Customers"
levels:
- "Segment"
- "Customer"
Balance by Segment (fullCount):
Premium 1800 (owns acct1, acct2, acct3 — de-duplicated)
Standard 300 (owns acct3)

Konto 1 gehört Alice und Bob, beide Premium – aber es wird in der Premium-Summe einmal gezählt, nicht zweimal. Das ist das symmetrische Aggregat in Aktion auf einer Zwischenebene: ohne es würde Premium die expandierten 2800 anzeigen. Konto 3 erscheint in beiden Segmenten (Alice ist Premium, Carol ist Standard) – das ist die beabsichtigte Full-Count-Überlappung zwischen Segmenten, im Unterschied zur Doppelzählung innerhalb eines Segments, die die Dedup entfernt.

Gewichtete Rollups benötigen keine Deduplizierung – die gewichteten Anteile jedes Eigentümers summieren sich sauber, sodass Premium = 1575, Standard = 225, und sich immer noch auf 1800 abgleicht.

Was funktioniert

Eine Bridge-Dimension verhält sich nach der Deklaration wie jede andere Dimension. All das funktioniert nativ:

  • die Bridge-Dimension auf Zeilen, Spalten oder im Slicer (WHERE);
  • gekreuzt mit normalen Foreign-Key-Dimensionen (z. B. Customer × Region), auf verschiedenen Achsen oder als Crossjoin auf derselben Achse;
  • mehrstufige Bridge-Hierarchien – Full-Count-Summen deduplizieren korrekt auf jeder Ebene, nicht nur auf Blatt und Gesamtsumme;
  • mehrere Measures in einer Abfrage, einschließlich Measures auf berechneten Spalten (ein CASE oder Arithmetik wie revenue - cost), die wie reine Measures deduplizieren;
  • NON EMPTY (Kunden ohne Konten werden unterdrückt);
  • explizite Member-Sets und .Members.

Anforderungen und Einschränkungen

  • Die Faktentabelle muss ihre Granularität als <Key> deklarieren, damit fullCount deduplizieren kann. (Das ist es, was mehrstufige Rollups korrekt macht.)
  • weighted erfordert eine weightColumn; fullCount ignoriert jegliche Gewichtung.
  • Beide Allokationen decken reine Real-Spalten-Measures und Measures auf berechneten Spalten ab (ein CASE oder Arithmetik wie revenue - cost): fullCount dedupliziert den Calc-Ausdruck auf der Fact-Granularität, und weighted skaliert ihn mit der Gewichtung (SUM(expression × weight)).
  • Der Bridge-Join ist pro Hop einspaltig. Zusammengesetzte Join-Schlüssel sind eine allgemeine Einschränkung des Calcite-Join-Pfads (nicht spezifisch für Bridges) – wenn Ihre Bridge-Schlüssel mehrspaltig sind, modellieren Sie stattdessen einen einzelnen Surrogat-Granularitätsschlüssel.

Ein vollständiges, ladbares Beispiel – Schema, Seed-Daten und Beispiel-MDX mit erwarteten Zahlen – ist in der Cube-Bibliothek unter many-to-many enthalten.

Distinct-Granularität auf Measure-Ebene (distinctKeyColumn)

Ein Bridge löst Fan-Out durch einen Join. Aber Fan-Out kann auch aus der Granularität der Faktentabelle selbst entstehen: Eine Fact-Zeile, die einmal gezählt werden sollte, wird physisch als mehrere Zeilen gespeichert (eine Bestellung mit mehreren Positionen, ein pro Berührung protokolliertes Ereignis), und ein einfaches SUM über die Spalte zählt doppelt. Wenn die Duplizierung durch eine Spalte auf der eigenen Faktentabelle der Measure geschlüsselt wird, brauchen Sie keinen Bridge – Sie können die Dedup-Granularität direkt mit distinctKeyColumn an der Measure festnageln.

ORDER_LINES (fact)
order_id line region amount
1 a North 100
1 b North 100 ← amount repeats per line of order 1
2 a South 50
3 a North 300
3 b North 300 ← amount repeats per line of order 3
SUM(amount) = 100+100+50+300+300 = 850 (WRONG — double-counts)
SUM(amount) DISTINCT over order_id = 100 + 50 + 300 = 450 (correct)

Deklarieren Sie den Dedup-Schlüssel auf der Measure. Er muss zu einer Spalte auf der eigenen Faktentabelle der Measure auflösen und ist nur für aggregator="sum" und aggregator="avg" zulässig:

measures:
- name: "Order Amount"
column: "amount"
aggregator: "sum"
distinct_key_column: "order_id"

Semantik

Die Measure aggregiert über SELECT DISTINCT (group keys, distinctKeyColumn, operand) – jeder eindeutige Schlüssel trägt seinen Wert einmal bei, auch wenn die Fact-Granularität die Zeile wiederholt. Sie verwendet dieselbe symmetrische Aggregat-Maschinerie wie der Bridge-Fan-Out (die Dedup wird durch die Measure-Deklaration und nicht durch die Join-Topologie gesteuert):

  • aggregator="sum"SUM über die eindeutigen Schlüssel (die sum_distinct-Form von LookML).
  • aggregator="avg"AVG über die eindeutigen Schlüssel (average_distinct von LookML).

Rollups bleiben auf jeder Ebene korrekt: By Region über das Beispiel liest North = 400, South = 50 – die distinct-Werte, niemals die expandierten 850.

Lässt sich mit Zeilen-Sicherheit kombinieren

Die Distinct-Granularität wird nach der Zeilen-Sicherheits-Filterung angewendet, nicht davor. <PredicateGrant> und Bridge-Member-Grants filtern die Fact-Zeilen innerhalb der DISTINCT-Unterabfrage, sodass die Dedup immer genau auf den Zeilen operiert, die der Aufrufer sehen darf – es gibt keinen Pfad, der einen Wert dedupliziert, den die Rolle nicht sehen kann, und dann die Gesamtsumme leakt. Die Distinct-Granularität ist eine feste Schema-Eigenschaft (nicht rollenabhängig), sie fügt also keine neue Cache-Dimension hinzu, und der Segment-Cache isoliert Rollen weiterhin: Ein für die Grants einer Rolle vorgewärmter Wert wird niemals einer anderen Rolle serviert. (Dies wird von den Komposition-Tests für Zeilen-Sicherheit abgedeckt, einschließlich eines No-Cross-Role-Cache-Bleed-Tests.)

Anforderungen und Einschränkungen

  • distinctKeyColumn muss zu einer echten Spalte auf der eigenen Faktentabelle der Measure aufgelöst werden. Ein Cross-Table- oder nicht auflösbarer Schlüssel wird zur Ladezeit abgewiesen (fail-closed) – niemals eine still-falsche Dedup.
  • Nur zulässig für aggregator="sum" und aggregator="avg".
  • Ein Segment-Load, der eine Distinct-Granularitäts-Measure mit einer einfachen Measure mischt oder zwei Measures mit unterschiedlichen Distinct-Schlüsseln, wird automatisch in homogene Pro-Measure-Segmente aufgeteilt (das anfrageweite DISTINCT kann nicht zwei Granularitäten gleichzeitig bedienen).
  • Wenn der Distinct-Schlüssel mit dem eigenen Granularitätsschlüssel der Faktentabelle übereinstimmt (z. B. ihrem Primary Key), ist die Dedup ein No-op, und die Measure verhält sich wie eine einfache sum/avg. Das ist das natürliche Ziel für LookMLs sum_distinct/average_distinct – siehe Migration von Looker.

Adaptiert aus dem Mondrian-Projekt-Schema-Guide (EPL v1.0). Bridge-Dimensionen (Many-to-Many) und Distinct-Granularität auf Measure-Ebene sind Saiku-Erweiterungen zu Mondrian 4.