Carga da Dimensão Tempo (Com Feriados Brasil) - PLSQL
- Erick Furst

- 6 de set. de 2019
- 9 min de leitura
Atualizado: 24 de set.
Por que este código é útil?
Uma visão prática da Dimensão Tempo em Data Warehousing (Oracle/PLSQL)
Autor: Erick Furst
A Dimensão Tempo é uma das tabelas mais importantes em um Data Warehouse. Ela permite análises por ano, semestre, trimestre, mês e dia, viabilizando cálculos como YoY, QoQ, MoM, YTD/MTD, sazonalidade, calendários fiscais, identificação de dias úteis/feriados e muito mais.

.
Esse código — escrito em PL/SQL para Oracle — gera e carrega automaticamente todas as hierarquias temporais entre valores de variáveis, no exemplo, 01/01/2000 e 31/12/2040 em tabelas de dimensão separadas (ANO, SEMESTRE, TRIMESTRE, MÊS e DIA), já com rótulos legíveis, chaves de junção e indicadores de feriado/dia útil, incluindo feriados móveis (Páscoa e correlatos). A seguir, o que ele faz e por que isso é valioso.
O que o script cria e carrega
1) DIM_ANO (Ano)
Conteúdo: Ano corrente (NUM_ANO) e ano anterior (NUM_ANO_ANT).
Uso típico: filtros e comparações YoY (Year over Year), joins com fatos agregados por ano.
2) DIM_SEMESTRE (Semestre)
Conteúdo: combinações de ano+semestre (NUM_ANO_SMT), descrição legível (“1º Semestre de 2015”), código do semestre (“01”/“02”) e ano.
Uso típico: análises semestrais, comparações H1 vs H2, metas por semestre.
3) DIM_TRIMESTRE (Trimestre)
Conteúdo: chaves de ano+trimestre (NUM_ANO_TRM), descrição (“3º Trimestre de 2015”), código do trimestre (“01”–“04”) e referência ao semestre (NUM_ANO_SMT).
Uso típico: QoQ (Quarter over Quarter), análises típicas de empresas com reporte trimestral.
4) DIM_MES (Mês)
Conteúdo: chave ano+mês (NUM_ANO_MES), descrições (“Março de 2015”, “Mar”, “Março”), vínculo ao trimestre (NUM_ANO_TRM), campos de navegação temporal (mês anterior do mesmo ano, ano anterior, e “início do modelo de valor” 3 meses antes).
Uso típico: MoM (Month over Month), sazonalidade, janelas móveis (ex.: últimos 3 meses).
5) DIM_DATA (Dia)
Conteúdo (rico):
Chave de data (DAT_DIA) e chave ano+mês (NUM_ANO_MES).
Dia da semana (número, nome e abreviação).
Indicadores: IND_DIA_UTL (S/N), IND_FRD (S/N).
Descrição de feriado (DES_FRD) quando aplicável.
Datas auxiliares: dia anterior, primeiro/último dia do mês, início de janela móvel (ex.: D-3 meses), ano, semestre e trimestre correspondentes; data no ano anterior.
Uso típico: calendário operacional/financeiro, cálculo automático de dias úteis, identificação de feriados, fechamento de períodos, comparações D-1, M-1, Y-1, construção de painéis com “Acumulado no mês”, “Acumulado no ano”, etc.
Destaque: O script calcula feriados móveis via o algoritmo da Páscoa (computus) e deriva Carnaval, Quarta-feira de Cinzas, Sexta-feira Santa e Corpus Christi. Também inclui feriados fixos nacionais e alguns feriados municipais/estaduais (SP) como Aniversário de São Paulo (25/01), Revolução Constitucionalista (09/07) e Consciência Negra (20/11).
Boas práticas embutidas no código
Geração completa e automática (2000–2040)Evita manutenção manual, garante cobertura histórica e futura para análises e simulações.
Hierarquias coerentes para drill-down/upAno → Semestre → Trimestre → Mês → Dia. As chaves compostas (ex.: NUM_ANO_MES, NUM_ANO_TRM, NUM_ANO_SMT) padronizam joins e facilitam a navegação.
Datas de borda de mêsO padrão
add_months(Data,1) - extract(DAY from (add_months(Data,1)))
fixa o cursor no último dia do mês corrente, o que simplifica saltos corretos de 1, 3, 6 e 12 meses, mesmo em meses com 28–31 dias.
Indicadores de Dia Útil e FeriadoIND_DIA_UTL marca finais de semana e feriados como não úteis, facilitando cálculos de lead time, SLA, planejamento de capacidade e previsões que consideram apenas dias úteis.
Rótulos legíveis e abreviaçõesMelhora a usabilidade em BI (Power BI, Tableau, Qlik) e relatórios: nomes de meses, semestres, trimestres e feriados já vêm prontos para o usuário final.
Como isso é usado em análises do dia a dia
Crescimento e comparação temporal:
YoY: junte fato de vendas com DIM_DATA e compare com DAT_ANO_ANT.
QoQ/MoM: use NUM_ANO_TRM/NUM_ANO_MES para deltas por período.
Sazonalidade:
Médias por mês/semana, correlação com feriados (ex.: varejo no Natal, Turismo no Carnaval).
Calendário de negócio:
Filtrar somente dias úteis para produtividade e custos.
Fechamentos (último dia do mês), rolling windows, cortes por H1/H2.
Modelagem preditiva e FP&A:
Variáveis explicativas: dummies de feriado, mês, trimestre, dia útil; janelas móveis (3M, 12M).
Pontos de atenção e ajustes recomendados
NLS / Dia da semanaO cálculo do dia da semana via TO_CHAR(Data,'D') depende de NLS_TERRITORY. Garanta que a instância esteja alinhada (ex.: Brasil) ou substitua por lógica independente de NLS para evitar deslocamentos (domingo=1, segunda=2, etc.).
Feriados locaisO script inclui feriados federais e alguns de São Paulo. Se a empresa atuar em outras cidades/estados, parametrize feriados locais em uma tabela de feriados e faça lookup na carga (em vez de hard-code).
Idempotência e chaves/índices
Adicione unique keys (ex.: DIM_DATA.DAT_DIA, DIM_MES.NUM_ANO_MES, etc.) para evitar duplicidade ao reaplicar o script.
Crie índices nas colunas de junção mais usadas (ex.: NUM_ANO_MES, NUM_ANO_TRM, NUM_ANO_SMT) para acelerar BI/SQL.
Commits e performanceO script faz COMMIT por bloco. Para grandes janelas ou refações, considere bulk inserts (FORALL), disable/enable indexes durante a carga inicial e gather stats ao final.
Chaves substitutas vs. naturaisO script usa chaves “naturais” (ex.: YYYYMM). Se sua arquitetura padrão exigir surrogate keys (inteiros sequenciais), mantenha ambas: surrogate como PK e YYYYMM/YYYYQ como colunas degeneradas para navegação.
Calendários especiaisSe usar ano fiscal diferente do calendário (ex.: inicia em abril) ou ISO week, expanda a dimensão com esses atributos (ex.: FISCAL_YEAR, ISO_WEEK, ISO_YEAR).
InternacionalizaçãoAs descrições estão em português. Para soluções multilíngues, crie colunas DES_*_EN/ES ou uma tabela de tradução.
Exemplo de uso (joins simples)
-- Vendas por mês com comparação ano anterior
SELECT
m.NUM_ANO_MES,
m.DES_ANO_MES,
SUM(f.VALOR) AS VLR,
LAG(SUM(f.VALOR)) OVER (ORDER BY m.NUM_ANO_MES RANGE BETWEEN 12 PRECEDING AND 12 PRECEDING) AS VLR_YA
FROM FATO_VENDAS f
JOIN DIM_DATA d ON f.DATA_ID = d.DAT_DIA
JOIN DIM_MES m ON d.NUM_ANO_MES = m.NUM_ANO_MES
GROUP BY m.NUM_ANO_MES, m.DES_ANO_MES
ORDER BY m.NUM_ANO_MES;
Por que preferir este approach
Consistência: todas as áreas e relatórios partilham a mesma referência temporal.
Reprodutibilidade: sem dependência de funções “on the fly” que variam por ambiente.
Velocidade de análise: fatos se juntam a dimensões prontas, com rótulos e flags úteis.
Governança: controle claro de feriados, dias úteis e hierarquias.
Dicas finais para produção
Crie constraints e índices (PK/UK em cada tabela, FKs opcionais conforme o volume).
Gere estatísticas após a carga: DBMS_STATS.GATHER_TABLE_STATS.
Parametrize o período (2000–2040 como padrão; permita estender facilmente).
Externalize feriados em tabela própria, mantendo o cálculo de móveis como fallback.
Documente os campos (dicionário de dados) e versione o script.
Em resumo
Este código entrega uma Dimensão Tempo completa e pronta para uso — do ano ao dia, com descrições amigáveis, códigos padronizados, feriados (fixos e móveis) e indicadores de dia útil. Ele acelera a construção do seu DW/BI, melhora a qualidade das análises e padroniza a lógica temporal em toda a organização.
O código:
/*
Autor: Erick Furst
Data: 20/08/2015
Descrição: Processo para carga da Dimensão Tempo.
--------------------------------------------------
--Carga dimensão Ano (Year dimension load)
declare
Data DATE;
DataIni DATE;
DataFim DATE;
Data_Movel DATE;
BEGIN
DataIni := to_date('2000/01/01','yyyy/mm/dd');
DataFim := to_date('2040/12/31','yyyy/mm/dd');
Data := DataIni;
WHILE Data <= DataFim LOOP
INSERT INTO DIM_ANO ( NUM_ANO, NUM_ANO_ANT )
VALUES ( TO_CHAR(extract(YEAR from Data)), TO_CHAR(extract(YEAR from Data)-1) );
Data := add_months(Data,12);
Data := add_months(Data,1) - extract(DAY from (add_months(Data,1)));
END LOOP;
COMMIT;
END;
-- select * from ano
--------------------------------------------------
--Carga dimensão Semestre (Six month period dimension load)
declare
Data DATE;
DataIni DATE;
DataFim DATE;
Data_Movel DATE;
BEGIN
DataIni := to_date('2000/01/01','yyyy/mm/dd');
DataFim := to_date('2040/12/31','yyyy/mm/dd');
Data := DataIni;
WHILE Data <= DataFim LOOP
INSERT INTO DIM_SEMESTRE(NUM_ANO_SMT, DES_ANO_SMT, NUM_SMT, DES_SMT, NUM_ANO )
VALUES
(
TO_CHAR(extract(YEAR from Data)) || DECODE(TO_NUMBER(TO_CHAR(Data,'Q'))
,1,'01'
,2,'01'
,3,'02'
,4,'02'),
DECODE(TO_NUMBER(TO_CHAR(Data,'Q'))
,1,'1º Semestre de '
,2,'1º Semestre de '
,3,'2º Semestre de '
,4,'2º Semestre de ') || TO_CHAR(extract(YEAR from Data)),
DECODE(TO_NUMBER(TO_CHAR(Data,'Q'))
,1,'01'
,2,'01'
,3,'02'
,4,'02'),
DECODE(TO_NUMBER(TO_CHAR(Data,'Q'))
,1,'1º Semestre'
,2,'1º Semestre'
,3,'2º Semestre'
,4,'2º Semestre'),
TO_CHAR(extract(YEAR from Data))
);
Data := add_months(Data,6);
Data := add_months(Data,1) - extract(DAY from (add_months(Data,1)));
END LOOP;
COMMIT;
END;
-- select * from semestre
--------------------------------------------------
--Carga dimensão trimestre (Quarter dimension load)
declare
Data DATE;
DataIni DATE;
DataFim DATE;
Data_Movel DATE;
BEGIN
DataIni := to_date('2000/01/01','yyyy/mm/dd');
DataFim := to_date('2040/12/31','yyyy/mm/dd');
Data := DataIni;
WHILE Data <= DataFim LOOP
INSERT INTO DIM_TRIMESTRE(NUM_ANO_TRM, DES_ANO_TRM, NUM_TRM, DES_TRM, NUM_ANO_SMT)
VALUES
(
TO_CHAR(extract(YEAR from Data)) || DECODE(TO_NUMBER(TO_CHAR(Data,'Q'))
,1,'01'
,2,'02'
,3,'03'
,4,'04'),
DECODE(TO_NUMBER(TO_CHAR(Data,'Q'))
,1,'1º Trimestre de '
,2,'2º Trimestre de '
,3,'3º Trimestre de '
,4,'4º Trimestre de ') || TO_CHAR(extract(YEAR from Data)),
DECODE(TO_NUMBER(TO_CHAR(Data,'Q'))
,1,'01'
,2,'02'
,3,'03'
,4,'04'),
DECODE(TO_NUMBER(TO_CHAR(Data,'Q'))
,1,'1º Trimestre'
,2,'2º Trimestre'
,3,'3º Trimestre'
,4,'4º Trimestre'),
TO_CHAR(extract(YEAR from Data)) || DECODE(TO_NUMBER(TO_CHAR(Data,'Q'))
,1,'01'
,2,'01'
,3,'02'
,4,'02')
);
Data := add_months(Data,3);
Data := add_months(Data,1) - extract(DAY from (add_months(Data,1)));
END LOOP;
COMMIT;
END;
-- select * from trimestre
--------------------------------------------------
--Carga dimensão mês (month dimension load)
declare
Data DATE;
DataIni DATE;
DataFim DATE;
Data_Movel DATE;
BEGIN
DataIni := to_date('2000/01/01','yyyy/mm/dd');
DataFim := to_date('2040/12/31','yyyy/mm/dd');
Data := DataIni;
WHILE Data <= DataFim LOOP
INSERT INTO DIM_MES (NUM_ANO_MES, DES_ANO_MES, NUM_MES, DES_MES, DES_MES_ABV, NUM_ANO_TRM,
NUM_ANO_ANT_MES, NUM_ANO_MES_ANT, NUM_ANO_MES_INI_MDL_VLR)
VALUES
(
TO_CHAR(extract(YEAR from Data)) || TO_CHAR(Data, 'MM'),
(CASE extract(MONTH from Data)
WHEN (1) THEN ('Janeiro')
WHEN (2) THEN ('Fevereiro')
WHEN (3) THEN ('Março')
WHEN (4) THEN ('Abril')
WHEN (5) THEN ('Maio')
WHEN (6) THEN ('Junho')
WHEN (7) THEN ('Julho')
WHEN (8) THEN ('Agosto')
WHEN (9) THEN ('Setembro')
WHEN (10) THEN ('Outubro')
WHEN (11) THEN ('Novembro')
WHEN (12) THEN ('Dezembro')
END) || ' de ' || TO_CHAR(extract(YEAR from Data)),
TO_CHAR(Data, 'MM'),
(CASE extract(MONTH from Data)
WHEN (1) THEN ('Janeiro')
WHEN (2) THEN ('Fevereiro')
WHEN (3) THEN ('Março')
WHEN (4) THEN ('Abril')
WHEN (5) THEN ('Maio')
WHEN (6) THEN ('Junho')
WHEN (7) THEN ('Julho')
WHEN (8) THEN ('Agosto')
WHEN (9) THEN ('Setembro')
WHEN (10) THEN ('Outubro')
WHEN (11) THEN ('Novembro')
WHEN (12) THEN ('Dezembro')
END),
(CASE extract(MONTH from Data)
WHEN (1) THEN ('Jan')
WHEN (2) THEN ('Fev')
WHEN (3) THEN ('Mar')
WHEN (4) THEN ('Abr')
WHEN (5) THEN ('Mai')
WHEN (6) THEN ('Jun')
WHEN (7) THEN ('Jul')
WHEN (8) THEN ('Ago')
WHEN (9) THEN ('Set')
WHEN (10) THEN ('Out')
WHEN (11) THEN ('Nov')
WHEN (12) THEN ('Dez')
END),
TO_CHAR(extract(YEAR from Data)) || DECODE(TO_NUMBER(TO_CHAR(Data,'Q'))
,1,'01'
,2,'02'
,3,'03'
,4,'04'),
TO_NUMBER(TO_CHAR(extract(YEAR from Data)) || TO_CHAR(Data, 'MM'))-100,
CASE WHEN extract(MONTH from Data) <> 1
THEN TO_NUMBER(TO_CHAR(extract(YEAR from Data)) || TO_CHAR(Data, 'MM'))-1
ELSE TO_NUMBER(TO_CHAR(extract(YEAR from Data)) || TO_CHAR(Data, 'MM'))-89
END,
TO_NUMBER(TO_CHAR(extract(YEAR from ADD_MONTHS(Data,-3))) || TO_CHAR(ADD_MONTHS(Data,-3), 'MM'))
);
Data := add_months(Data,1);
Data := add_months(Data,1) - extract(DAY from (add_months(Data,1)));
END LOOP;
COMMIT;
END;
-- SELECT * FROM MES ORDER BY 1
--------------------------------------------------
--Carga dimensão Data (Day dimension load)
declare
Data DATE;
DataIni DATE;
DataFim DATE;
Data_Movel DATE;
VarA INT;
VarB INT;
VarC INT;
VarD INT;
VarE INT;
VarF INT;
VarG INT;
VarH INT;
VarI INT;
VarAno INT;
MesPascoa INT;
DiaPascoa INT;
DataPascoaNum INT;
DataPascoa DATE;
DataCarnaval DATE;
DataQuartaCinzas DATE;
DataSextaSanta DATE;
DataCorpusChristi DATE;
BEGIN
DataIni := to_date('2000/01/01','yyyy/mm/dd');
DataFim := to_date('2040/12/31','yyyy/mm/dd');
Data := DataIni;
data_movel := DataFim;
WHILE Data <= DataFim LOOP
--Código para cálculo de feriado móvel.
VarAno := extract(YEAR from Data);
VarA := TRUNC(VarAno/100);
VarB := VarAno-(19*TRUNC(VarAno/19));
VarC := TRUNC((VarA-17)/25);
VarD := VarA-TRUNC(VarA/4)-TRUNC((VarA-VarC)/3)+19*VarB+15;
VarE := VarD-30*TRUNC(VarD/30);
VarF := VarE-(TRUNC(VarE/28)*(1-TRUNC(VarE/28))*TRUNC(29/(VarE+1))*TRUNC(21-VarB/11));
VarG := VarAno+TRUNC(VarAno/4)+VarF+2-VarA+TRUNC(VarA/4);
VarH := VarG-(7*TRUNC(VarG/7));
VarI := VarF-VarH;
MesPascoa := 3+TRUNC((VarI+40)/44);
DiaPascoa := VarI+28-31*TRUNC(MesPascoa/4);
DataPascoaNum := (VarAno * 100 + MesPascoa) * 100 + DiaPascoa;
DataPascoa := TO_DATE(DataPascoaNum, 'yyyymmdd');
DataCarnaval := DataPascoa - 47;
DataQuartaCinzas := DataCarnaval + 1;
DataSextaSanta := DataPascoa - 2;
DataCorpusChristi := DataPascoa + 60;
INSERT INTO DIM_DATA ( DAT_DIA, NUM_ANO_MES, NUM_DIA_SEM, DES_DIA_SEM, DES_DIA_SEM_ABV, IND_DIA_UTL, IND_FRD, DES_FRD,
DAT_DIA_ANT, DAT_PRI_DIA_MES, DAT_ULT_DIA_MES, DAT_INI_MDL_VLR, NUM_ANO,
NUM_ANO_SMT, NUM_ANO_TRM, DAT_ANO_ANT, DAT_ULT_DIA_MES_ANT )
VALUES
(
TO_DATE(Data,'DD/MM/YY'),
TO_NUMBER(TO_CHAR(extract(YEAR from Data)) || TO_CHAR(Data, 'MM')),
TO_NUMBER( TO_CHAR (Data, 'D') ),
CASE TO_NUMBER( TO_CHAR (Data, 'D') )
WHEN 1 THEN 'Domingo'
WHEN 2 THEN 'Segunda-feira'
WHEN 3 THEN 'Terça-feira'
WHEN 4 THEN 'Quarta-feira'
WHEN 5 THEN 'Quinta-feira'
WHEN 6 THEN 'Sexta-feira'
WHEN 7 THEN 'Sábado'
END,
CASE TO_NUMBER( TO_CHAR (Data, 'D') )
WHEN 1 THEN 'Dom'
WHEN 2 THEN 'Seg'
WHEN 3 THEN 'Ter'
WHEN 4 THEN 'Qua'
WHEN 5 THEN 'Qui'
WHEN 6 THEN 'Sex'
WHEN 7 THEN 'Sáb'
END,
CASE WHEN EXTRACT(MONTH from Data) * 100 + EXTRACT(DAY from Data)
IN (101, 421, 501, 907, 1012, 1102, 1115, 1225, 125, 709, 1120,
-- EXTRACT(MONTH from DataCarnaval) * 100 + EXTRACT(DAY from DataCarnaval),
-- EXTRACT(MONTH from DataQuartaCinzas) * 100 + EXTRACT(DAY from DataQuartaCinzas),
EXTRACT(MONTH from DataSextaSanta) * 100 + EXTRACT(DAY from DataSextaSanta),
EXTRACT(MONTH from DataCorpusChristi) * 100 + EXTRACT(DAY from DataCorpusChristi) )
OR ( TO_NUMBER( TO_CHAR (Data, 'D') ) IN (1,7) )
THEN 'N'
ELSE 'S'
END,
CASE WHEN EXTRACT(MONTH from Data) * 100 + EXTRACT(DAY from Data)
IN (101, 421, 501, 907, 1012, 1102, 1115, 1225, 125, 709, 1120,
-- EXTRACT(MONTH from DataCarnaval) * 100 + EXTRACT(DAY from DataCarnaval),
-- EXTRACT(MONTH from DataQuartaCinzas) * 100 + EXTRACT(DAY from DataQuartaCinzas),
EXTRACT(MONTH from DataSextaSanta) * 100 + EXTRACT(DAY from DataSextaSanta),
EXTRACT(MONTH from DataCorpusChristi) * 100 + EXTRACT(DAY from DataCorpusChristi) )
THEN 'S'
ELSE 'N'
END,
CASE EXTRACT(MONTH from Data) * 100 + EXTRACT(DAY from Data)
WHEN 101 THEN 'Confraternização Universal' --01/01
WHEN 421 THEN 'Tiradentes' -- 21/04
WHEN 501 THEN 'Dia do Trabalho' -- 01/05
WHEN 907 THEN 'Idependência do Brasil' -- 07/09
WHEN 1012 THEN 'Nossa Sra. Aparecida' -- 12/10
WHEN 1102 THEN 'Finados' -- 02/11
WHEN 1115 THEN 'Proclamação da República' -- 15/11
WHEN 1225 THEN 'Natal' -- 25/12
WHEN 125 THEN 'Aniversário de São Paulo' -- 25/01
WHEN 709 THEN 'Revolução Constitucionalista de 1932' -- 09/07
WHEN 1120 THEN 'Consciência Negra' -- 20/11
WHEN EXTRACT(MONTH from DataPascoa) * 100 + EXTRACT(DAY from DataPascoa) THEN 'Páscoa'
WHEN EXTRACT(MONTH from DataCarnaval) * 100 + EXTRACT(DAY from DataCarnaval) THEN 'Carnaval'
WHEN EXTRACT(MONTH from DataQuartaCinzas) * 100 + EXTRACT(DAY from DataQuartaCinzas) THEN 'Quarta-feira de Cinzas'
WHEN EXTRACT(MONTH from DataSextaSanta) * 100 + EXTRACT(DAY from DataSextaSanta) THEN 'Sexta-feira da Paixão'
WHEN EXTRACT(MONTH from DataCorpusChristi) * 100 + EXTRACT(DAY from DataCorpusChristi) THEN 'Corpus Christi'
ELSE ('NA')
END ,
TO_CHAR(Data - 1,'DD/MM/YYYY'),
TO_CHAR((Data - extract(DAY from (Data))) + 1,'DD/MM/YYYY'),
TO_CHAR(ADD_MONTHS(Data,1) - extract(DAY from (Data)),'DD/MM/YYYY') ,
TO_CHAR((ADD_MONTHS(Data,-3) - extract(DAY from (Data))) + 1,'DD/MM/YYYY'),
TO_CHAR(extract(YEAR from Data)),
TO_CHAR(extract(YEAR from Data)) || DECODE(TO_NUMBER(TO_CHAR(Data,'Q'))
,1,'01'
,2,'01'
,3,'02'
,4,'02'),
TO_CHAR(extract(YEAR from Data)) || DECODE(TO_NUMBER(TO_CHAR(Data,'Q'))
,1,'01'
,2,'02'
,3,'03'
,4,'04'),
add_months(Data,-12),
TO_CHAR(Data - extract(DAY from Data),'DD/MM/YYYY')
);
Data := Data + 1;
END LOOP;
COMMIT;
END;


Comentários