Pular para o conteúdo principal

Estrutura de Tabelas - Fenabrave Emplacamento

📋 Visão Geral

O projeto segue o padrão Medallion Architecture com três camadas de dados:

PostgreSQL (retroativo 2003-2026) ┐
├─→ Bronze (dados brutos)
FTP Fenabrave (diário + mensal) ┘


Silver (normalized)


Gold (star schema - Delta Lake)

🥉 Bronze Layer

Armazena dados brutos no S3 em dois locais.

Priorização de Fontes (deduplicação por numero_chassis):

  1. ftp_diario — fonte oficial diária (prioridade máxima)
  2. postgres — retroativo histórico (carga inicial 2003–2026)
  3. ftp_mensal — correção/fechamento de mês (última prioridade)

1. PostgreSQL Bronze

Prefixo: fenabrave/bronze/postgres/dwemplacamento/

Fonte: tabelas PostgreSQL do schema dwemplacamento (dados históricos)

Tabelas extraídas:

TabelaTipoParticionamento
fato_emplacamento/Fatoano=YYYY
dim_empresa/DimensãoNão
dim_fabricante/DimensãoNão
dim_versao_associada/DimensãoNão
dim_versao/DimensãoNão
dim_segmento/DimensãoNão
dim_local/DimensãoNão
dim_combustivel/DimensãoNão

Formato: Parquet (extraído com DAG postgres_emplacamento_retroativo)

Características:

  • Particionadas por ano (tabelas com coluna de data)
  • Idempotente: partições já presentes são puladas
  • Máximo 2 extrações paralelas para não sobrecarregar o banco

2. FTP Bronze

Prefixo: fenabrave/bronze/fenabrave/

Fonte: arquivos .txt do FTP Fenabrave (dados diários/mensais)

Estrutura de diretórios:

fenabrave/bronze/
└── ABRACAF/
├── Emplacamentos_Diario_Segmentos_NC_YYYY-MM-DD.txt # uma cópia por dia
├── Emplacamentos_Diario_Segmentos_Mdl_NC_YYYY-MM-DD.txt # uma cópia por dia
├── Emplacamentos_Mensal_Fabricante_NC_YYYY-MM.txt # baixado apenas 1× por mês
└── Emplacamentos_Mensal_Segmentos_Mdl_NC_YYYY-MM.txt # baixado apenas 1× por mês

Características:

  • Formato: arquivos .txt (pipe-delimited, detecta encoding automaticamente)
  • Filtro: apenas arquivos com "diario" ou "mensal" no nome E contendo "nc"
  • Apenas ABRACAF é processado atualmente (filtro /ABRACAF/ no medallion)
  • Nomes de coluna normalizados para snake_case
  • Diário usa sufixo YYYY-MM-DD (uma cópia por dia, histórico completo)
  • Mensal usa sufixo YYYY-MM e é baixado apenas uma vez por mês — não há re-download diário, para reduzir consumo de banda/tempo no FTP. A cópia única do mês fica disponível para correção/fechamento no medallion.

🥈 Silver Layer

Prefixo: fenabrave/silver/ano=YYYY/chunk_NNNN.parquet

Dados transformados, limpos e normalizados. Consolidado a partir de Bronze.

Particionamento

  • Por ano: ano=YYYY (extraído de data_emplacamento)
  • Por chunk: máximo 2.000.000 linhas por arquivo (controle de memória)

Esquema Silver

Estrutura: Todas as colunas são String para garantir equivalência entre Postgres e FTP.

ColunaTipoOrigemDescrição
cnpjStringPG + FTPCNPJ da concessionária
is_cpf_empresaStringPG + FTPTrue se CNPJ tem 11 dígitos (pessoa física)
is_cpf_clienteStringPG + FTPTrue se Tipo_Pessoa do FTP da ABRACAF for Física
razao_socialStringPG + FTPRazão social ou "Pessoa Física"
_origemStringSistemaFonte: ftp_diario, ftp_mensal ou postgres
data_emplacamentoDatePG + FTPData do emplacamento
numero_chassisStringPGNúmero do chassis
numero_placaStringPGPlaca do veículo
Fabricante
fabricante_codigoIntPGCódigo do fabricante
fabricanteStringPGNome do fabricante
Modelo
modelo_codigoIntPGCódigo da versão
modeloStringPGDescrição da versão
grupomodeloveiculo_codigoIntPGCódigo do grupo modelo
grupomodeloveiculoStringPGNome do grupo modelo
Segmentação
segmento_codigoIntPGCódigo da categoria
segmentoStringPGNome da categoria
subsegmento_codigoIntPGCódigo do segmento
subsegmentoStringPGNome do segmento
combustivel_codigoIntPGCódigo do combustível
combustivelStringPGTipo de combustível
Localização
municipio_codigoIntPG + FTPCódigo IBGE do município
municipioStringPG + FTPNome do município (campo único, sem pivot)
estadoStringPG + FTPUF (campo único, sem pivot)
Região (pivotada por grupo_id)
regiao_abracafStringPG (pivotado)Região segundo ABRACAF
regiao_abcnissanStringPG (pivotado)Região segundo ABCNISSAN
regiao_abrareStringPG (pivotado)Região segundo ABRARE
Veículo
potenciaStringPGPotência do motor
cilindradasStringPG + FTPCilindradas do motor (cm³)
capacidade_cargaStringPGCapacidade de carga (kg)
capacidade_passageirosStringPGNúmero de passageiros
cor_veiculoStringFTPCor do veículo
codigo_nacionalidadeStringPGCódigo do país de origem
ano_fabricacaoIntPGAno de fabricação
Restrições
restricao_01StringPGRestrição 01
restricao_02StringPGRestrição 02
restricao_03StringPGRestrição 03
restricao_04StringPGRestrição 04
Metadados
_source_fileStringSistemaPath S3 do arquivo de origem

🥇 Gold Layer

Tipo: Delta Lake (S3: s3://bucket/fenabrave/gold/)

Implementa um star schema com dimensões desnormalizadas e fato.

Tabelas Gold

📊 dim_concessionaria

Dimensão de empresas concessionárias.

ColunaTipoDescrição
concessionaria_skInt64Surrogate Key (auto-incremento)
cnpjStringNatural Key
is_cpf_empresaBooleanTrue se CNPJ tem 11 dígitos (pessoa física)
razao_socialStringRazão social (campo único, sem pivot)
regiao_abracafStringRegião ABRACAF (pivotado por grupo_id)
regiao_abcnissanStringRegião ABCNISSAN (pivotado por grupo_id)
regiao_abrareStringRegião ABRARE (pivotado por grupo_id)

Estatísticas esperadas: ~5-10k registros


🏙️ dim_local

Dimensão de localidades (municípios).

ColunaTipoDescrição
local_skInt64Surrogate Key (auto-incremento)
municipio_codigoIntNatural Key (código IBGE)
municipioStringNome do município (campo único, sem pivot)
estadoStringUF (campo único, sem pivot)
regiao_abracafStringRegião ABRACAF (pivotado por grupo_id)
regiao_abcnissanStringRegião ABCNISSAN (pivotado por grupo_id)
regiao_abrareStringRegião ABRARE (pivotado por grupo_id)

Estatísticas esperadas: ~5-6k registros


🚗 dim_veiculo

Dimensão de características de veículos.

ColunaTipoDescrição
veiculo_skInt64Surrogate Key (auto-incremento)
veiculo_nkStringNatural Key (concat: modelo_codigo|fabricante_codigo|combustivel_codigo|subsegmento_codigo)
fabricante_codigoIntCódigo do fabricante
fabricanteStringNome do fabricante
modelo_codigoIntCódigo da versão/modelo
modeloStringDescrição da versão
grupomodeloveiculo_codigoIntCódigo do grupo modelo
grupomodeloveiculoStringNome do grupo modelo
segmento_codigoIntCódigo da categoria
segmentoStringNome da categoria
subsegmento_codigoIntCódigo do subsegmento
subsegmentoStringNome do subsegmento
combustivel_codigoIntCódigo do combustível
combustivelStringTipo de combustível
cor_veiculoStringCor do veículo
potenciaStringPotência do motor
cilindradasStringCilindradas do motor (cm³)
capacidade_cargaStringCapacidade de carga (kg)
capacidade_passageirosStringNúmero de passageiros

Estatísticas esperadas: ~100-200k registros


📅 dim_calendario

Dimensão de datas (cobertura 2000–2100, gerada pela DAG dim_calendario).

ColunaTipoDescrição
dataDateNatural Key
anoInt32Ano
semestreInt321 ou 2
trimestreInt321 a 4
mesInt321 a 12
nome_mesStringJaneiro, Fevereiro, ...
semana_anoInt32Semana ISO do ano
dia_do_mesInt321 a 31
dia_do_anoInt321 a 366
dia_da_semanaInt32ISO: 1=Segunda ... 7=Domingo
nome_dia_semanaStringSegunda-feira, Terça-feira, ...
fim_de_semanaBooleanTrue se sábado ou domingo
feriado_nacionalBooleanTrue se feriado nacional
nome_feriadoStringNome do feriado (null se não for)
dia_utilBooleanTrue se não é fim de semana nem feriado
primeiro_dia_do_mesBooleanTrue se é dia 1
ultimo_dia_do_mesBooleanTrue se é último dia do mês

Feriados: nacionais brasileiros fixos + móveis (Carnaval, Páscoa, Sexta-feira da Paixão, Corpus Christi). Consciência Negra a partir de 2023.

Uso: join com fato_emplacamentos.data_emplacamento para enriquecer análises temporais. Também serve de referência para o ajuste de data_emplacamento no fato (que aplica nearest_business_day).

Estatísticas esperadas: ~36k registros (101 anos × 365.25)


📈 fato_emplacamentos

Tabela de fatos (detalhes de emplacamentos).

ColunaTipoDescrição
data_emplacamentoDateData ajustada para o dia útil mais próximo dentro do mesmo mês (pula fim de semana e feriados)
data_emplacamento_originalDateData crua do source (sem ajuste de dia útil)
ano_fabricacaoIntAno de fabricação
numero_chassisStringNúmero do chassis
numero_placaStringPlaca do veículo
codigo_nacionalidadeStringCódigo do país de origem
restricao_01StringRestrição 01
restricao_02StringRestrição 02
restricao_03StringRestrição 03
restricao_04StringRestrição 04
concessionaria_skInt64FK → dim_concessionaria
local_skInt64FK → dim_local
veiculo_skInt64FK → dim_veiculo
is_cpf_clienteBooleanTrue se Tipo_Pessoa do ftp ABRACAF é física
_source_fileStringPath S3 da origem
_ano_particaoIntPartição: ano para otimizar queries
_origemStringFonte: ftp_diario, ftp_mensal ou postgres (última coluna)

Particionamento: _ano_particao=YYYY

Estatísticas esperadas: ~25-50M registros


🔷 Supabase Layer

Tipo: Postgres (Supabase) — espelho relacional do Gold para consumo SQL/BI.

Schema: dw_emplacamento (não public).

Atualização: task sync_supabase no fim da DAG fenabrave_ftp_to_bronze (MWAA Serverless, 11h UTC). Carga inicial via scripts/supabase_initial_load.py standalone — não passa por Lambda.

Princípio

Espelho 1:1 das tabelas do Gold, no mesmo shape dimensional (mesmas colunas, mesmas SKs). Diferente do Gold (Delta Lake), aqui temos PKs/UNIQUE declaradas e particionamento nativo Postgres.

Gold Delta tipoPostgres tipo
Int64BIGINT
Int / Int32INTEGER
StringTEXT
DateDATE
BooleanBOOLEAN

Tabelas

📊 dw_emplacamento.dim_concessionaria

PK: concessionaria_sk (BIGINT). UNIQUE: cnpj. Espelha o Gold.

🏙️ dw_emplacamento.dim_local

PK: local_sk (BIGINT). UNIQUE: municipio_codigo. Espelha o Gold.

🚗 dw_emplacamento.dim_veiculo

PK: veiculo_sk (BIGINT). UNIQUE: veiculo_nk. Espelha o Gold.

📅 dw_emplacamento.dim_calendario

PK: data (DATE). Espelha o Gold (cobertura 2000–2100).

📈 dw_emplacamento.fato_emplacamentos

ColunaTipoObservação
_ano_particaoINTEGER NOT NULLColuna de particionamento
numero_placaTEXT NOT NULLPK composta com _ano_particao
data_emplacamentoDATEAjustada para dia útil
data_emplacamento_originalDATESem ajuste
ano_fabricacaoINTEGER
numero_chassisTEXT
codigo_nacionalidadeTEXT
restricao_01..04TEXT
concessionaria_skBIGINTFK lógica → dim_concessionaria
local_skBIGINTFK lógica → dim_local
veiculo_skBIGINTFK lógica → dim_veiculo
is_cpf_clienteBOOLEAN
_source_fileTEXT
_origemTEXT

Particionamento: PARTITION BY LIST (_ano_particao)

CREATE TABLE fato_emplacamentos_2025 PARTITION OF fato_emplacamentos FOR VALUES IN (2025);
CREATE TABLE fato_emplacamentos_2026 PARTITION OF fato_emplacamentos FOR VALUES IN (2026);

Queries em fato_emplacamentos com filtro WHERE _ano_particao = YYYY fazem partition pruning automático — só tocam a partição filha. Sem filtro, lê todas as partições (mesma performance de tabela única).

FKs NÃO são declaradas — só índices nas SKs. Razão: o pipeline TRUNCA as dims a cada sync e FK travaria o TRUNCATE.

Índices:

  • ix_fato_data_emplacamento em data_emplacamento
  • ix_fato_concessionaria_sk em concessionaria_sk
  • ix_fato_local_sk em local_sk
  • ix_fato_veiculo_sk em veiculo_sk

Como os dados chegam

Camada anteriorPara Supabase
Dims do Gold (4 tabelas Delta)TRUNCATE + COPY (full refresh diário)
Fato do Gold (Delta particionado por _ano_particao)UPSERT por (_ano_particao, numero_placa) em staging temp + INSERT ... ON CONFLICT DO UPDATE

Janela do UPSERT diário: últimos 7 dias do fato (cobre reposts da Fenabrave sem precisar de tracking de estado). Idempotente.

Particularidades vs Gold

ItemGold (Delta)Supabase
Schemas3://...../fenabrave/gold/dw_emplacamento
PKImplícita (chave única do Delta)Declarada nativa
ParticionamentoPor _ano_particao (Delta)PARTITION BY LIST (Postgres)
RLSn/aDesabilitado (necessário pro COPY)
Cobertura temporal2003–2026 (todo histórico)Apenas 2025–2026
Linha sem numero_placaMantidaDescartada (PK NOT NULL)

Estatísticas esperadas

TabelaLinhasTamanho
dim_concessionaria~27 k~5 MB
dim_local~5,5 k~1 MB
dim_veiculo~17 k~5 MB
dim_calendario~37 k~5 MB
fato_emplacamentos_2025~2,5 M~675 MB
fato_emplacamentos_2026até ~2,5 M (conforme ano avança)até ~675 MB
Total (cheio)~5 M~1,4 GB

Free tier do Supabase (500 MB) não cabe — produção usa plano Pro (8 GB).

Para uso diário ver

Ver infra/supabase/README.md no repositório — como conectar (pooler, roles, search_path), queries de exemplo, operações de manutenção (adicionar ano novo, role read-only, validar consistência) e gotchas.


🔄 Fluxo de Dados

┌─────────────────────────────────────────────────────────────────┐
│ ORIGEM: Postgres + FTP │
└──────────────────────┬──────────────────────────────────────────┘

┌─────────────┴─────────────┐
│ │
DAG: postgres_ DAG: fenabrave_
emplacamento_retroativo ftp_to_bronze
│ │
└─────────────┬─────────────┘

┌──────────────────────────────────────┐
│ Bronze: dados brutos │
│ - postgres/dwemplacamento/* │
│ - fenabrave/bronze/ABRACAF/* │
└──────────────────────┬───────────────┘

DAG: fenabrave_medallion

┌─────────────────┴─────────────────┐
│ Task: bronze_to_silver │
└─────────────────┬─────────────────┘

┌──────────────────────────────────────┐
│ Silver: dados normalizados │
│ - fenabrave/silver/ano=YYYY/chunk_* │
└──────────────────────┬───────────────┘

┌─────────────────┴─────────────────┐
│ Task: silver_to_gold │
└─────────────────┬─────────────────┘

┌──────────────────────────────────────┐
│ Gold: star schema (Delta Lake) │
│ - dim_concessionaria │
│ - dim_local │
│ - dim_veiculo │
│ - dim_calendario (DAG própria) │
│ - fato_emplacamentos │
└──────────────┬───────────────────────┘

┌──────────┴──────────┐
↓ ↓
Task: sync_dynamo Task: sync_supabase
│ │
↓ ↓
┌─────────────┐ ┌──────────────────────────────┐
│ DynamoDB │ │ Supabase (dw_emplacamento) │
│ (lookup │ │ - dim_* (full refresh) │
│ placa) │ │ - fato_emplacamentos │
└─────────────┘ │ (UPSERT últimos 7 dias) │
└──────────────────────────────┘

⚙️ Configurações

Variáveis de Ambiente

PostgreSQL:

PG_EMPLACAMENTO_HOST=<host>
PG_EMPLACAMENTO_PORT=5432
PG_EMPLACAMENTO_DB=emplacamento
PG_EMPLACAMENTO_SCHEMA=dwemplacamento
PG_EMPLACAMENTO_USER=<user>
PG_EMPLACAMENTO_PASS=<password>

FTP Fenabrave:

FTP_FENABRAVE_HOST=ftp.fenabrave.org.br (padrão)
FTP_FENABRAVE_USER=<user>
FTP_FENABRAVE_PASS=<password>
FTP_FENABRAVE_ROOT=/ABRACAF (padrão, processado atualmente)

S3/MinIO:

S3_CONN_ID=minio (dev) ou aws_default (prod)
BUCKET=fenabrave (padrão)

Filtros

medallion.py - FILTER_YEARS:

FILTER_YEARS: set[int] = {2024, 2025, 2026} # Deixe vazio para todos

emplacamento_retroativo.py - FILTER_YEARS:

FILTER_YEARS: set[int] = {2026} # Deixe vazio para todos

📊 Estatísticas

CamadaEntidadeLinhasParticionamentoTamanho Esperado
Golddim_concessionaria~10kNão~10 MB
Golddim_local~6kNão~5 MB
Golddim_veiculo~150kNão~50 MB
Golddim_calendario~36kNão~2 MB
Goldfato_emplacamentos~50MPor ano~500 MB-1 GB
Silvertodos os anos~50Mano=YYYY + chunks~200 MB-500 MB

🔍 Queries de Exemplo

Contar emplacamentos por concessionária (2026)

SELECT
c.razao_social,
COUNT(*) as total_emplacamentos,
COUNT(DISTINCT f.numero_placa) as veiculos_unicos
FROM fato_emplacamentos f
JOIN dim_concessionaria c ON f.concessionaria_sk = c.concessionaria_sk
WHERE f._ano_particao = 2026
GROUP BY c.razao_social
ORDER BY total_emplacamentos DESC;

Emplacamentos por estado (último mês)

SELECT
l.estado,
COUNT(*) as total
FROM fato_emplacamentos f
JOIN dim_local l ON f.local_sk = l.local_sk
WHERE f._ano_particao = 2026
AND YEAR(f.data_emplacamento) = 2026
AND MONTH(f.data_emplacamento) = 12
GROUP BY l.estado;

Veículos mais emplacados

SELECT
v.fabricante,
v.modelo,
COUNT(*) as total
FROM fato_emplacamentos f
JOIN dim_veiculo v ON f.veiculo_sk = v.veiculo_sk
WHERE f._ano_particao = 2026
GROUP BY v.fabricante, v.modelo
ORDER BY total DESC
LIMIT 10;

📝 Notas

  • Idempotência: Bronze e Silver podem ser reprocessados sem duplicação
  • Upsert em Gold: Dimensões usam merge Delta (natural key), fato usa overwrite no 1º chunk e append nos demais
  • Schema Consistency: Silver tracking schema para evitar dtype mismatch ao concatenar frames
  • Memory Management: Leitura streaming e chunks para processar 50M+ linhas sem sobrecarregar RAM
  • Particionamento: Silver e Fato particionados por ano para otimizar queries