Time intelligence (YoY, PoP, YTD, rolling declarativos)
O Mondrian-4 suporta um elemento de schema <TimeCalc> que declara métricas comuns de time intelligence. O loader do schema desugar cada declaração em um <CalculatedMember> validado em [Measures] — então você declara o que você quer em vez de escrever e manter fórmulas MDX à mão.
Por que time intelligence declarativo?
Sem <TimeCalc>, crescimento year-over-year exige um calculated member como:
<CalculatedMember name="Revenue YoY" dimension="Measures"> <Formula> ([Measures].[Revenue] - (ParallelPeriod([Calendar].[Year], 1, [Calendar].CurrentMember), [Measures].[Revenue])) / (ParallelPeriod([Calendar].[Year], 1, [Calendar].CurrentMember), [Measures].[Revenue]) </Formula> <CalculatedMemberProperty name="FORMAT_STRING" value="0.0%"/></CalculatedMember>Com <TimeCalc> a mesma métrica é:
<TimeCalc name="Revenue YoY" type="yoy" measure="Revenue" timeDimension="Calendar" formatString="0.0%"/>O loader gera o MDX para você, valida que a medida referenciada e a dimensão de tempo existem, e dispara um erro no momento da carga em vez de produzir um resultado silenciosamente errado.
Pré-requisito: uma dimensão Time tipada
<TimeCalc> exige que o cubo tenha uma dimensão Time tipada — uma <Dimension> com type="TIME" cuja hierarquia tem levels nomeados para year, quarter e month. O level de year deve carregar levelType="TimeYears", e os levels de quarter e month devem carregar levelType="TimeQuarters" e levelType="TimeMonths" respectivamente. Os cálculos within-year (ytd, pop, rolling) exigem no mínimo um level de mês na hierarquia.
Uma dimensão Calendar mínima que satisfaz o requisito:
<Dimension name="Calendar" type="TIME" table="dim_date" key="Date"> <Attributes> <Attribute name="Year" keyColumn="year_num" levelType="TimeYears"/> <Attribute name="Quarter" keyColumn="quarter_key" levelType="TimeQuarters"/> <Attribute name="Month" keyColumn="month_key" levelType="TimeMonths"/> <Attribute name="Date" keyColumn="date_key" levelType="TimeDays"/> </Attributes> <Hierarchies> <Hierarchy name="Calendar" allMemberName="All Time"> <Level attribute="Year"/> <Level attribute="Quarter"/> <Level attribute="Month"/> <Level attribute="Date"/> </Hierarchy> </Hierarchies></Dimension>Calendar: type: "TIME" table: "dim_date" key: "Date" attributes: - name: "Year" key_column: "year_num" level_type: "TimeYears" - name: "Quarter" key_column: "quarter_key" level_type: "TimeQuarters" - name: "Month" key_column: "month_key" level_type: "TimeMonths" - name: "Date" key_column: "date_key" level_type: "TimeDays" hierarchies: - name: "Calendar" all_member_name: "All Time" levels: - attribute: "Year" - attribute: "Quarter" - attribute: "Month" - attribute: "Date"Posição no schema
Elementos <TimeCalc> são envoltos em um bloco <TimeCalcs> dentro de um <Cube>, no mesmo nível de <CalculatedMembers>:
<Cube name="Monthly Revenue">
<Dimensions> <Dimension source="Calendar"/> <!-- other dimensions --> </Dimensions>
<MeasureGroups> <MeasureGroup name="Revenue" table="monthly_revenue_fact"> <Measures> <Measure name="Revenue" column="revenue" aggregator="sum"/> </Measures> <DimensionLinks> <ForeignKeyLink dimension="Calendar" foreignKeyColumn="month_key"/> </DimensionLinks> </MeasureGroup> </MeasureGroups>
<TimeCalcs> <TimeCalc name="Revenue YoY" type="yoy" measure="Revenue" timeDimension="Calendar" formatString="0.0%"/> <TimeCalc name="Revenue PoP" type="pop" measure="Revenue" timeDimension="Calendar" formatString="0.0%"/> <TimeCalc name="Revenue YTD" type="ytd" measure="Revenue" timeDimension="Calendar"/> <TimeCalc name="Revenue R3" type="rolling" measure="Revenue" timeDimension="Calendar" window="3" function="avg"/> </TimeCalcs>
</Cube>Monthly Revenue: dimensions: - source: "Calendar" measure_groups: - name: "Revenue" table: "monthly_revenue_fact" measures: - name: "Revenue" column: "revenue" aggregator: "sum" dimension_links: - type: "foreign_key" dimension: "Calendar" foreign_key_column: "month_key" time_calcs: - name: "Revenue YoY" type: "yoy" measure: "Revenue" time_dimension: "Calendar" format_string: "0.0%" - name: "Revenue PoP" type: "pop" measure: "Revenue" time_dimension: "Calendar" format_string: "0.0%" - name: "Revenue YTD" type: "ytd" measure: "Revenue" time_dimension: "Calendar" - name: "Revenue R3" type: "rolling" measure: "Revenue" time_dimension: "Calendar" window: 3 function: "avg"Referência de atributos
| Atributo | Chave XML / YAML | Obrigatório | Descrição |
|---|---|---|---|
name | name | sim | O nome do calculated member gerado. Aparece em [Measures] como qualquer outra medida. |
type | type | sim | O tipo de métrica: yoy, pop, ytd ou rolling. Veja Tipos de métrica abaixo. |
measure | measure | sim | O nome de um <Measure> existente no cubo. O loader rejeita uma medida desconhecida no momento da carga do schema. |
timeDimension | time_dimension | condicional | O nome de uma dimensão type="TIME". Pode ser omitido quando o cubo tem exatamente uma dimensão TIME; obrigatório quando tem mais de uma. |
window | window | apenas rolling | Número inteiro de períodos a incluir na janela rolling. |
function | function | apenas rolling | Função de agregação sobre a janela: sum (default) ou avg. |
formatString | format_string | não | Format string MDX aplicada ao membro gerado, ex.: "0.0%" ou "#,###". |
Tipos de métrica
yoy — crescimento year-over-year
Reporta a variação percentual comparada ao mesmo período no ano anterior.
Formato da fórmula:
([Measures].[<measure>] - (ParallelPeriod([<dim>].[<YearLevel>], 1, [<dim>].CurrentMember), [Measures].[<measure>]))/(ParallelPeriod([<dim>].[<YearLevel>], 1, [<dim>].CurrentMember), [Measures].[<measure>])A chamada ParallelPeriod navega para a mesma posição relativa um ano atrás usando o level TimeYears. O resultado é NULL para o primeiro ano completo de dados (sem ano anterior disponível).
pop — crescimento period-over-period
Reporta a variação percentual comparada ao período imediatamente anterior (o período logo antes do atual no mesmo level).
Formato da fórmula:
([Measures].[<measure>] - (PrevMember([<dim>].CurrentMember), [Measures].[<measure>]))/(PrevMember([<dim>].CurrentMember), [Measures].[<measure>])PrevMember volta uma posição na ordenação natural da dimensão. Resultados são NULL para o primeiríssimo membro de um level (sem predecessor).
ytd — cumulativo year-to-date
Reporta o valor cumulativo da medida do início do ano atual até o período atual.
Formato da fórmula:
Aggregate(Ytd([<dim>].CurrentMember), [Measures].[<measure>])Ytd() retorna o conjunto de todos os períodos do primeiro período do ano atual até o período atual. Aggregate aplica a agregação nativa da medida (tipicamente sum) sobre esse conjunto.
rolling — janela rolling
Reporta o agregado da medida sobre os últimos window períodos, usando sum ou avg.
Formato da fórmula (avg, window=3):
Avg(LastPeriods(3, [<dim>].CurrentMember), [Measures].[<measure>])Formato da fórmula (sum, window=N):
Sum(LastPeriods(N, [<dim>].CurrentMember), [Measures].[<measure>])LastPeriods(N, member) retorna o conjunto dos N períodos terminando no membro atual. Se menos que N períodos estão disponíveis (ex.: cedo no histórico de dados), a janela encolhe para o número de períodos que existem — ela não preenche com zeros.
Comportamento de validação
O loader é fail-closed: a carga do schema é abortada com uma mensagem de erro clara se qualquer das seguintes condições é detectada.
| Condição | Erro |
|---|---|
measure nomeia um membro que não existe no cubo | TimeCalc "X": measure "Y" not found in cube |
Nenhum timeDimension especificado e o cubo tem zero dimensões TIME | TimeCalc "X": cube has no TIME dimension |
Nenhum timeDimension especificado e o cubo tem mais de uma dimensão TIME | TimeCalc "X": cube has multiple TIME dimensions — specify timeDimension |
timeDimension nomeia uma dimensão que não existe no cubo | TimeCalc "X": timeDimension "Y" not found |
type="rolling" e window está faltando ou não é um inteiro positivo | TimeCalc "X": rolling type requires a positive integer window |
type="yoy" mas não existe nenhum level TimeYears na dimensão | TimeCalc "X": TIME dimension has no TimeYears level |
Não há resultado silenciosamente errado — toda configuração ruim é pega antes da primeira consulta rodar.
Exemplo trabalhado: receita mensal do demo Bank
O demo Bank traz um cubo Monthly Revenue sobre uma série de receita mensal. Os dados brutos para dois anos:
| Year | Month | Revenue |
|---|---|---|
| 2024 | Jan | 100 |
| 2024 | Feb | 200 |
| 2024 | Mar | 300 |
| 2025 | Jan | 150 |
| 2025 | Feb | 250 |
| 2025 | Mar | 350 |
O cubo declara todos os quatro tipos de <TimeCalc> contra a dimensão Calendar (Year > Quarter > Month):
<TimeCalcs> <TimeCalc name="Revenue YoY" type="yoy" measure="Revenue" timeDimension="Calendar" formatString="0.0%"/> <TimeCalc name="Revenue PoP" type="pop" measure="Revenue" timeDimension="Calendar" formatString="0.0%"/> <TimeCalc name="Revenue YTD" type="ytd" measure="Revenue" timeDimension="Calendar"/> <TimeCalc name="Revenue R3" type="rolling" measure="Revenue" timeDimension="Calendar" window="3" function="avg"/></TimeCalcs>time_calcs:- name: "Revenue YoY" type: "yoy" measure: "Revenue" time_dimension: "Calendar" format_string: "0.0%"- name: "Revenue PoP" type: "pop" measure: "Revenue" time_dimension: "Calendar" format_string: "0.0%"- name: "Revenue YTD" type: "ytd" measure: "Revenue" time_dimension: "Calendar"- name: "Revenue R3" type: "rolling" measure: "Revenue" time_dimension: "Calendar" window: 3 function: "avg"Resultados de ouro
| Célula | Valor | Como |
|---|---|---|
| Revenue YoY em [Calendar].[2025].[Q1].[Jan 2025] | 0.5 (50%) | (150 − 100) / 100 = 0.5 |
| Revenue PoP em [Calendar].[2024].[Q1].[Feb 2024] | 1.0 (100%) | (200 − 100) / 100 = 1.0 |
| Revenue YTD em [Calendar].[2024].[Q1].[Mar 2024] | 600 | 100 + 200 + 300 = 600 |
| Revenue R3 em [Calendar].[2025].[Q1].[Mar 2025] | 250 | avg(150, 250, 350) = 250 |
Consulta MDX de exemplo
SELECT { [Measures].[Revenue], [Measures].[Revenue YoY], [Measures].[Revenue PoP], [Measures].[Revenue YTD], [Measures].[Revenue R3] } ON COLUMNS, [Calendar].[Month].Members ON ROWSFROM [Monthly Revenue]Resultado parcial (2024–2025 Jan até Mar):
| Month | Revenue | YoY | PoP | YTD | R3 |
|---|---|---|---|---|---|
| Jan 2024 | 100 | — | — | 100 | 100 |
| Feb 2024 | 200 | — | 100.0% | 300 | 150 |
| Mar 2024 | 300 | — | 50.0% | 600 | 200 |
| Jan 2025 | 150 | 50.0% | −50.0% | 150 | 216.7 |
| Feb 2025 | 250 | 25.0% | 66.7% | 400 | 233.3 |
| Mar 2025 | 350 | 16.7% | 40.0% | 750 | 250 |
Traços (—) indicam NULL — sem dados de ano anterior ou sem período predecessor disponível.
Relação com <CalculatedMembers>
Declarações <TimeCalc> dessugar no momento da carga em elementos <CalculatedMember> em [Measures]. Os membros gerados são indistinguíveis de calculated members manuais no momento da consulta: aparecem em enumerações de membro XMLA, respondem a FORMAT_STRING e podem ser referenciados por outros calculated members.
Se você precisa de uma fórmula que <TimeCalc> não pode expressar — por exemplo, uma métrica blended customizada ou uma razão multi-medida antes de uma comparação de tempo — use um <CalculatedMember> plano diretamente. As duas abordagens podem coexistir no mesmo cubo.
Veja Avançado — Calculated members para a referência completa de <CalculatedMember>.