Saltearse al contenido

Conversión de divisas (FX declarativa mediante <CurrencyConversion>)

Mondrian-4 admite un elemento de schema <CurrencyConversion> que produce una medida convertida como SUM(base_measure × exchange_rate), eligiendo automáticamente la tasa correcta a la fecha de la propia fila de hechos mediante una tabla de tasas con intervalos de validez no solapados. Usted declara qué quiere; el motor compila el join por bandas de intervalo y la agregación por usted.

¿Por qué conversión de divisas declarativa?

Sin <CurrencyConversion>, convertir una medida a una divisa diferente requiere un miembro calculado escrito a mano que debe reimplementar la búsqueda con fecha efectiva cada vez que cambia la estructura de la tabla de tasas:

<CalculatedMember name="Revenue (USD)" dimension="Measures">
<Formula>
Sum(
Existing [Calendar].[Month].Members,
[Measures].[Revenue] * LookupCube("[fx_rate]", ...)
)
</Formula>
</CalculatedMember>

Con <CurrencyConversion> la misma métrica es:

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

El loader valida que la medida base nombrada y todas las columnas referenciadas existen, genera el join por bandas en tiempo de carga, y lanza un error claro en lugar de producir un resultado silenciosamente incorrecto.

Colocación en el schema

Los elementos <CurrencyConversion> se envuelven en un bloque <CurrencyConversions> dentro de un <MeasureGroup>, como hermano de <Measures> y <DimensionLinks>:

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

Referencia de atributos

Atributoclave XMLclave YAMLRequeridoDescripción
namenamenameEl nombre de la medida convertida generada. Aparece en [Measures] como cualquier otra medida.
measuremeasuremeasureEl nombre de un <Measure> existente en el mismo grupo de medidas (el valor base a multiplicar).
rateTablerateTablerate_tableNombre de la tabla física que contiene las tasas de cambio. Debe existir en el schema físico.
rateColumnrateColumnrate_columnColumna en rateTable que contiene la tasa de cambio numérica.
rateTyperateTyperate_typeValor literal usado para filtrar la tabla de tasas a una fuente de tasa (por ejemplo, "ECB", "BLOOMBERG").
rateTypeColumnrateTypeColumnrate_type_columnColumna en rateTable que contiene el discriminador de tipo de tasa.
factCurrencyColumnfactCurrencyColumnfact_currency_columnColumna en la tabla de hechos que identifica la divisa origen de cada fila.
rateCurrencyColumnrateCurrencyColumnrate_currency_columnColumna en rateTable que identifica la divisa desde la que convierte la tasa.
factDateColumnfactDateColumnfact_date_columnColumna en la tabla de hechos usada para determinar qué intervalo de tasa aplica.
rateValidFromColumnrateValidFromColumnrate_valid_from_columnColumna en rateTable para el inicio del intervalo (inclusivo).
rateValidToColumnrateValidToColumnrate_valid_to_columnColumna en rateTable para el fin del intervalo (exclusivo).
formatStringformatStringformat_stringnoCadena de formato MDX para la medida convertida, por ejemplo "#,##0.00" o "$#,##0".

Contrato de datos del intervalo con fecha efectiva

La tabla de tasas contiene una fila por combinación (currency, rate_type) por período de validez. El motor realiza un band join que selecciona exactamente la tasa cuyo intervalo contiene la fecha de la fila de hechos:

fact.factDateColumn >= rate.rateValidFromColumn
AND fact.factDateColumn < rate.rateValidToColumn
AND fact.factCurrencyColumn = rate.rateCurrencyColumn
AND rate.rateTypeColumn = '<rateType literal>'

El join es inherentemente 1-a-1 — una fila de hechos coincide con como mucho una fila de tasa — siempre que los intervalos de la tabla de tasas no se solapen. Esto significa que la agregación de la medida convertida es simplemente SUM(fact.measure × rate.rate), y cualquier medida no convertida consultada junto a ella no se ve afectada en absoluto.

Estructura recomendada de la tabla de tasas

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)
);

Use valid_to = '9999-12-31' (o el valid_from del siguiente período) como centinela para el intervalo abierto actual para que el predicado < valid_to siempre funcione limpiamente.

Requisito de backend y validación fail-closed

La validación es fail-closed: la carga del schema se aborta con un mensaje de error claro por cualquiera de las siguientes condiciones.

CondiciónError
measure no nombra una medida existente en el grupo de medidasCurrencyConversion "X": measure "Y" not found in measure group
rateTable no existe en el schema físicoCurrencyConversion "X": rateTable "Y" not found in physical schema
Alguna de las columnas nombradas no existe en la tabla referenciadaCurrencyConversion "X": column "Y" not found in table "Z"
El backend Calcite no está activoCurrencyConversion "X": requires Calcite backend

No hay resultado silenciosamente incorrecto — cada mala configuración se captura antes de que se ejecute la primera consulta.

Ejemplo trabajado: ingresos mensuales de la demo Bank

La demo Bank incluye un cubo Monthly Revenue cuyas filas de hechos mm_monthly están denominadas en EUR. Una tabla fx_rate contiene dos tasas anuales del BCE:

currency_idrate_typeratevalid_fromvalid_to
EURECB1.102024-01-012025-01-01
EURECB1.202025-01-012026-01-01

Los datos de hechos crudos:

AñoRevenue (EUR)
2024600
2025750
Total1350

Declaración en el schema

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

Resultados de referencia

FilaRevenue (EUR)Revenue (USD)Cálculo
2024600660600 × 1.10 (tasa BCE 2024)
2025750900750 × 1.20 (tasa BCE 2025)
Total general13501560660 + 900

Las filas de 2024 y 2025 usan tasas diferentes porque el band join elige la tasa cuyo intervalo [valid_from, valid_to) contiene el month_key de cada fila de hechos. La Revenue base (EUR) no se ve afectada y permanece en 1350.

Consulta MDX de ejemplo

SELECT
{ [Measures].[Revenue],
[Measures].[Revenue (USD)] } ON COLUMNS,
[Calendar].[Year].Members ON ROWS
FROM [Monthly Revenue]

Resultado:

AñoRevenue (EUR)Revenue (USD)
2024600660.00
2025750900.00
All Time1,3501,560.00

Relación con <CalculatedMembers>

<CurrencyConversion> compila a una agregación SQL nativa (SUM(fact.revenue × rate.rate)) en lugar de a un <CalculatedMember> MDX. Esto significa:

  • La conversión se evalúa en la base de datos, no en el motor MDX — es tan rápida como cualquier medida agregada ordinaria.
  • La medida convertida aparece en las enumeraciones de miembros XMLA junto a las medidas regulares.
  • Puede ser referenciada por fórmulas <CalculatedMember> (por ejemplo, para computar un margen en la divisa de destino).

Si necesita una fórmula que <CurrencyConversion> no pueda expresar — por ejemplo, convertir solo una porción de una medida, o mezclar tasas de varias fuentes — use un <CalculatedMember> plano junto con una búsqueda con script separada. Los dos enfoques pueden coexistir en el mismo cubo.

Consulte Avanzado — Miembros calculados para la referencia completa de <CalculatedMember>.