Zum Inhalt springen

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"/>

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>

Attributreferenz

AttributXML-SchlüsselYAML-SchlüsselErforderlichBeschreibung
namenamenamejaDer Name der erzeugten umgerechneten Measure. Erscheint in [Measures] wie jede andere Measure.
measuremeasuremeasurejaDer Name einer existierenden <Measure> in derselben Measure-Group (der Basiswert, mit dem multipliziert wird).
rateTablerateTablerate_tablejaPhysischer Tabellenname, der Wechselkurse enthält. Muss im physischen Schema existieren.
rateColumnrateColumnrate_columnjaSpalte in rateTable, die den numerischen Wechselkurs enthält.
rateTyperateTyperate_typejaLiteraler Wert zum Filtern der Kurstabelle auf eine Kursquelle (z. B. "ECB", "BLOOMBERG").
rateTypeColumnrateTypeColumnrate_type_columnjaSpalte in rateTable, die den Kurstyp-Diskriminator enthält.
factCurrencyColumnfactCurrencyColumnfact_currency_columnjaSpalte in der Faktentabelle, die die Ausgangswährung jeder Zeile identifiziert.
rateCurrencyColumnrateCurrencyColumnrate_currency_columnjaSpalte in rateTable, die die Währung identifiziert, aus der der Kurs umrechnet.
factDateColumnfactDateColumnfact_date_columnjaSpalte in der Faktentabelle, die bestimmt, welches Kursintervall gilt.
rateValidFromColumnrateValidFromColumnrate_valid_from_columnjaSpalte in rateTable für den Intervallanfang (inklusive).
rateValidToColumnrateValidToColumnrate_valid_to_columnjaSpalte in rateTable für das Intervallende (exklusive).
formatStringformatStringformat_stringneinMDX-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.rateValidFromColumn
AND fact.factDateColumn < rate.rateValidToColumn
AND fact.factCurrencyColumn = rate.rateCurrencyColumn
AND 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.

BedingungFehler
measure benennt keine existierende Measure in der Measure-GroupCurrencyConversion "X": measure "Y" not found in measure group
rateTable existiert nicht im physischen SchemaCurrencyConversion "X": rateTable "Y" not found in physical schema
Eine der genannten Spalten existiert nicht in der referenzierten TabelleCurrencyConversion "X": column "Y" not found in table "Z"
Calcite-Backend ist nicht aktivCurrencyConversion "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_idrate_typeratevalid_fromvalid_to
EURECB1.102024-01-012025-01-01
EURECB1.202025-01-012026-01-01

Die rohen Fact-Daten:

YearRevenue (EUR)
2024600
2025750
Gesamt1350

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>

Erwartete Ergebnisse

ZeileRevenue (EUR)Revenue (USD)Berechnung
2024600660600 × 1.10 (ECB-Kurs 2024)
2025750900750 × 1.20 (ECB-Kurs 2025)
Gesamtsumme13501560660 + 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 ROWS
FROM [Monthly Revenue]

Ergebnis:

YearRevenue (EUR)Revenue (USD)
2024600660.00
2025750900.00
All Time1,3501,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.