Währungsumrechnung (deklaratives FX via <CurrencyConversion>)
Mondrian-4 unterstützt ein <CurrencyConversion>-Schema-Element, das eine umgerechnete Measure als SUM(base_measure × exchange_rate) erzeugt und dabei automatisch den korrekten Kurs zum Datum der Fact-Zeile selbst über eine Kurstabelle mit nicht überlappenden Gültigkeitsintervallen auswählt. Sie deklarieren was Sie möchten; die Engine kompiliert den Intervall-Band-Join und die Aggregation für Sie.
Warum deklarative Währungsumrechnung?
Ohne <CurrencyConversion> erfordert die Umrechnung einer Measure in eine andere Währung einen handgeschriebenen berechneten Member, der das Stichtags-Lookup jedes Mal neu implementieren muss, wenn sich die Struktur der Kurstabelle ändert:
<CalculatedMember name="Revenue (USD)" dimension="Measures"> <Formula> Sum( Existing [Calendar].[Month].Members, [Measures].[Revenue] * LookupCube("[fx_rate]", ...) ) </Formula></CalculatedMember>Mit <CurrencyConversion> ist dieselbe Metrik:
<CurrencyConversion name="Revenue (USD)" measure="Revenue" rateTable="fx_rate" rateColumn="rate" rateType="ECB" rateTypeColumn="rate_type" factCurrencyColumn="currency_id" rateCurrencyColumn="currency_id" factDateColumn="month_key" rateValidFromColumn="valid_from" rateValidToColumn="valid_to" formatString="#,##0.00"/>currency_conversions:- name: "Revenue (USD)" measure: "Revenue" rate_table: "fx_rate" rate_column: "rate" rate_type: "ECB" rate_type_column: "rate_type" fact_currency_column: "currency_id" rate_currency_column: "currency_id" fact_date_column: "month_key" rate_valid_from_column: "valid_from" rate_valid_to_column: "valid_to" format_string: "#,##0.00"Der Loader validiert, dass die benannte Basis-Measure und alle referenzierten Spalten existieren, generiert den Band-Join zur Ladezeit und wirft einen klaren Fehler, anstatt ein still falsches Ergebnis zu erzeugen.
Platzierung im Schema
<CurrencyConversion>-Elemente werden in einem <CurrencyConversions>-Block innerhalb einer <MeasureGroup> als Geschwister von <Measures> und <DimensionLinks> gekapselt:
<Cube name="Monthly Revenue">
<MeasureGroups> <MeasureGroup name="Revenue" table="mm_monthly">
<Measures> <Measure name="Revenue" column="revenue" aggregator="sum"/> </Measures>
<DimensionLinks> <ForeignKeyLink dimension="Calendar" foreignKeyColumn="month_key"/> </DimensionLinks>
<CurrencyConversions> <CurrencyConversion name="Revenue (USD)" measure="Revenue" rateTable="fx_rate" rateColumn="rate" rateType="ECB" rateTypeColumn="rate_type" factCurrencyColumn="currency_id" rateCurrencyColumn="currency_id" factDateColumn="month_key" rateValidFromColumn="valid_from" rateValidToColumn="valid_to" formatString="#,##0.00"/> </CurrencyConversions>
</MeasureGroup> </MeasureGroups>
</Cube>measure_groups:- name: "Revenue" table: "mm_monthly" measures: - name: "Revenue" column: "revenue" aggregator: "sum" dimension_links: - type: "foreign_key" dimension: "Calendar" foreign_key_column: "month_key" currency_conversions: - name: "Revenue (USD)" measure: "Revenue" rate_table: "fx_rate" rate_column: "rate" rate_type: "ECB" rate_type_column: "rate_type" fact_currency_column: "currency_id" rate_currency_column: "currency_id" fact_date_column: "month_key" rate_valid_from_column: "valid_from" rate_valid_to_column: "valid_to" format_string: "#,##0.00"Attributreferenz
| Attribut | XML-Schlüssel | YAML-Schlüssel | Erforderlich | Beschreibung |
|---|---|---|---|---|
name | name | name | ja | Der Name der erzeugten umgerechneten Measure. Erscheint in [Measures] wie jede andere Measure. |
measure | measure | measure | ja | Der Name einer existierenden <Measure> in derselben Measure-Group (der Basiswert, mit dem multipliziert wird). |
rateTable | rateTable | rate_table | ja | Physischer Tabellenname, der Wechselkurse enthält. Muss im physischen Schema existieren. |
rateColumn | rateColumn | rate_column | ja | Spalte in rateTable, die den numerischen Wechselkurs enthält. |
rateType | rateType | rate_type | ja | Literaler Wert zum Filtern der Kurstabelle auf eine Kursquelle (z. B. "ECB", "BLOOMBERG"). |
rateTypeColumn | rateTypeColumn | rate_type_column | ja | Spalte in rateTable, die den Kurstyp-Diskriminator enthält. |
factCurrencyColumn | factCurrencyColumn | fact_currency_column | ja | Spalte in der Faktentabelle, die die Ausgangswährung jeder Zeile identifiziert. |
rateCurrencyColumn | rateCurrencyColumn | rate_currency_column | ja | Spalte in rateTable, die die Währung identifiziert, aus der der Kurs umrechnet. |
factDateColumn | factDateColumn | fact_date_column | ja | Spalte in der Faktentabelle, die bestimmt, welches Kursintervall gilt. |
rateValidFromColumn | rateValidFromColumn | rate_valid_from_column | ja | Spalte in rateTable für den Intervallanfang (inklusive). |
rateValidToColumn | rateValidToColumn | rate_valid_to_column | ja | Spalte in rateTable für das Intervallende (exklusive). |
formatString | formatString | format_string | nein | MDX-Format-String für die umgerechnete Measure, z. B. "#,##0.00" oder "$#,##0". |
Daten-Vertrag für Stichtags-Intervalle
Die Kurstabelle enthält eine Zeile pro (currency, rate_type)-Kombination pro Gültigkeitszeitraum. Die Engine führt einen Band-Join durch, der genau den Kurs auswählt, dessen Intervall das Datum der Fact-Zeile enthält:
fact.factDateColumn >= rate.rateValidFromColumnAND fact.factDateColumn < rate.rateValidToColumnAND fact.factCurrencyColumn = rate.rateCurrencyColumnAND rate.rateTypeColumn = '<rateType literal>'Der Join ist von Natur aus 1-zu-1 – eine Fact-Zeile passt höchstens zu einer Kurszeile – vorausgesetzt, die Intervalle der Kurstabelle überlappen sich nicht. Das bedeutet, dass die Aggregation der umgerechneten Measure einfach SUM(fact.measure × rate.rate) ist und jede daneben abgefragte nicht umgerechnete Measure völlig unberührt bleibt.
Empfohlene Struktur der Kurstabelle
CREATE TABLE fx_rate ( currency_id VARCHAR(3) NOT NULL, -- e.g. 'EUR' rate_type VARCHAR(16) NOT NULL, -- e.g. 'ECB' rate DECIMAL(18,6) NOT NULL, valid_from DATE NOT NULL, -- inclusive valid_to DATE NOT NULL, -- exclusive PRIMARY KEY (currency_id, rate_type, valid_from));Verwenden Sie valid_to = '9999-12-31' (oder das valid_from der nächsten Periode) als Sentinel für das aktuell offene Intervall, damit das Prädikat < valid_to immer sauber funktioniert.
Backend-Anforderung und Fail-Closed-Validierung
Die Validierung ist fail-closed: Das Laden des Schemas wird mit einer klaren Fehlermeldung abgebrochen, wenn eine der folgenden Bedingungen erfüllt ist.
| Bedingung | Fehler |
|---|---|
measure benennt keine existierende Measure in der Measure-Group | CurrencyConversion "X": measure "Y" not found in measure group |
rateTable existiert nicht im physischen Schema | CurrencyConversion "X": rateTable "Y" not found in physical schema |
| Eine der genannten Spalten existiert nicht in der referenzierten Tabelle | CurrencyConversion "X": column "Y" not found in table "Z" |
| Calcite-Backend ist nicht aktiv | CurrencyConversion "X": requires Calcite backend |
Es gibt kein still falsches Ergebnis – jede Fehlkonfiguration wird vor der ersten Abfrage erkannt.
Beispiel: Monatlicher Umsatz aus der Bank-Demo
Die Bank-Demo liefert einen Monthly Revenue-Cube, dessen mm_monthly-Fact-Zeilen in EUR ausgewiesen sind. Eine fx_rate-Tabelle enthält zwei jährliche ECB-Kurse:
| currency_id | rate_type | rate | valid_from | valid_to |
|---|---|---|---|---|
| EUR | ECB | 1.10 | 2024-01-01 | 2025-01-01 |
| EUR | ECB | 1.20 | 2025-01-01 | 2026-01-01 |
Die rohen Fact-Daten:
| Year | Revenue (EUR) |
|---|---|
| 2024 | 600 |
| 2025 | 750 |
| Gesamt | 1350 |
Schema-Deklaration
<MeasureGroup name="Revenue" table="mm_monthly">
<Measures> <Measure name="Revenue" column="revenue" aggregator="sum"/> </Measures>
<DimensionLinks> <ForeignKeyLink dimension="Calendar" foreignKeyColumn="month_key"/> </DimensionLinks>
<CurrencyConversions> <CurrencyConversion name="Revenue (USD)" measure="Revenue" rateTable="fx_rate" rateColumn="rate" rateType="ECB" rateTypeColumn="rate_type" factCurrencyColumn="currency_id" rateCurrencyColumn="currency_id" factDateColumn="month_key" rateValidFromColumn="valid_from" rateValidToColumn="valid_to" formatString="#,##0.00"/> </CurrencyConversions>
</MeasureGroup>- name: "Revenue" table: "mm_monthly" measures: - name: "Revenue" column: "revenue" aggregator: "sum" dimension_links: - type: "foreign_key" dimension: "Calendar" foreign_key_column: "month_key" currency_conversions: - name: "Revenue (USD)" measure: "Revenue" rate_table: "fx_rate" rate_column: "rate" rate_type: "ECB" rate_type_column: "rate_type" fact_currency_column: "currency_id" rate_currency_column: "currency_id" fact_date_column: "month_key" rate_valid_from_column: "valid_from" rate_valid_to_column: "valid_to" format_string: "#,##0.00"Erwartete Ergebnisse
| Zeile | Revenue (EUR) | Revenue (USD) | Berechnung |
|---|---|---|---|
| 2024 | 600 | 660 | 600 × 1.10 (ECB-Kurs 2024) |
| 2025 | 750 | 900 | 750 × 1.20 (ECB-Kurs 2025) |
| Gesamtsumme | 1350 | 1560 | 660 + 900 |
Die Zeilen 2024 und 2025 verwenden unterschiedliche Kurse, weil der Band-Join den Kurs auswählt, dessen Intervall [valid_from, valid_to) den month_key jeder Fact-Zeile enthält. Die Basis-Revenue (EUR) ist unberührt und bleibt bei 1350.
Beispiel-MDX-Abfrage
SELECT { [Measures].[Revenue], [Measures].[Revenue (USD)] } ON COLUMNS, [Calendar].[Year].Members ON ROWSFROM [Monthly Revenue]Ergebnis:
| Year | Revenue (EUR) | Revenue (USD) |
|---|---|---|
| 2024 | 600 | 660.00 |
| 2025 | 750 | 900.00 |
| All Time | 1,350 | 1,560.00 |
Beziehung zu <CalculatedMembers>
<CurrencyConversion> kompiliert zu einer nativen SQL-Aggregation (SUM(fact.revenue × rate.rate)) statt zu einem MDX-<CalculatedMember>. Das bedeutet:
- Die Umrechnung wird in der Datenbank ausgewertet, nicht in der MDX-Engine – sie ist so schnell wie jede gewöhnliche Aggregat-Measure.
- Die umgerechnete Measure erscheint in XMLA-Member-Enumerationen neben regulären Measures.
- Sie kann von
<CalculatedMember>-Formeln referenziert werden (z. B. zur Berechnung einer Marge in der Zielwährung).
Wenn Sie eine Formel benötigen, die <CurrencyConversion> nicht ausdrücken kann – etwa nur einen Slice einer Measure umrechnen oder Kurse aus mehreren Quellen mischen –, verwenden Sie ein einfaches <CalculatedMember> zusammen mit einem separaten skript-basierten Lookup. Beide Ansätze können im selben Cube koexistieren.
Siehe Fortgeschritten – Berechnete Members für die vollständige <CalculatedMember>-Referenz.