← Back to list

Construindo um lakehouse de dados públicos (parte 3): de arquivo solto a tabela de verdade

Na parte 2, o dado bruto das duas fontes chegou inteiro à camada Bronze, sem nenhuma transformação, guardado como JSON simples e…

Jaime Teixeira de Araújo Júnior · 2026-08-02 16:49 · 0 claps · 9.7 min read
#hive-metastore #spark #apache-spark #apache-iceberg #big-data-processing
Open on Medium ↗
Wiki topics: SEO · SEO & SEM

Construindo um lakehouse de dados públicos (parte 3): de arquivo solto a tabela de verdade

Na parte 2, o dado bruto das duas fontes chegou inteiro à camada Bronze, sem nenhuma transformação, guardado como JSON simples e particionado por data. Esta parte é sobre o que acontece a seguir: como meses de arquivos soltos, sem chave primária confiável e sujeitos a reprocessamento, viram tabelas normalizadas, sem duplicidade, mesmo quando o mesmo período é processado mais de uma vez. É aqui que o projeto deixa de ser um data lake e passa a ser, de fato, um lakehouse.

O problema que a camada Silver resolve

A Bronze guarda, para uma mesma tabela de origem, um arquivo novo a cada execução diária, nunca sobrescreve, nunca deduplica. Isso é uma vantagem para auditoria (o dado bruto original nunca desaparece), mas é inútil para consulta direta: ninguém quer varrer centenas de arquivos JSON, cada um representando um dia diferente de extração, só para saber o estado atual de um contrato. A camada Silver existe para resolver exatamente isso: normalizar o formato e consolidar tudo numa única visão atualizada, sem duplicar linha nenhuma mesmo quando a mesma janela de dados é reprocessada.

Como visto antes a extração incremental usa uma margem de segurança de 7 dias para trás, justamente porque um lançamento pode entrar na fonte com data retroativa. Isso significa que a mesma linha pode, de propósito, aparecer em mais de uma extração, e a Silver precisa lidar bem com isso.

O que é um data lakehouse?

O que é um data lakehouse?

O que aconteceria sem um formato de tabela por cima do Parquet

Vale entender o problema em termos concretos, antes de justificar a escolha. Imagine a Silver gravada como Parquet solto em HDFS, sem nenhum formato de tabela por cima: um formato colunar eficiente para leitura, mas sem nenhuma noção de transação, sem controle de versão, sem qualquer garantia de que duas execuções não duplicariam informação. Uma deduplicação ingênua, nesse cenário, só enxergaria o lote que está sendo processado naquele momento: se a mesma linha aparecer de novo numa execução seguinte (o cenário exato do reprocessamento por lookback), ela viraria uma segunda cópia solta no arquivo, sem que ninguém percebesse.

Isso é, historicamente, o problema central de qualquer data lake clássico (o termo popularizado a partir do início dos anos 2010, para descrever grandes repositórios de arquivo bruto/semi-estruturado, geralmente sobre Hadoop): armazenamento barato e flexível, mas sem as garantias transacionais que um banco de dados relacional sempre teve. A resposta do mercado a esse problema, a partir de 2018–2019, foi uma nova categoria de formato de tabela: Apache Iceberg (criado dentro da Netflix), Delta Lake (criado dentro do Databricks) e Apache Hudi (criado dentro do Uber) surgiram quase ao mesmo tempo, todos resolvendo essencialmente o mesmo problema: trazer garantias de transação (ACID), controle de versão e schema para cima de arquivos que já viviam num data lake. Esse conjunto de ideias é o que passou a ser chamado de lakehouse: em vez de Parquet solto, a Silver deste projeto é construída diretamente como tabelas Apache Iceberg.

Demystifying Open Table Formats: Delta Lake vs Iceberg vs Hudi

Demystifying Open Table Formats: Delta Lake vs Iceberg vs Hudi

Por que Iceberg, e não Delta Lake ou Hudi

Os três formatos citados acima resolvem o mesmo problema central, mas partiram de pontos de partida diferentes, e essa origem ainda importa na escolha. O Delta Lake nasceu fortemente acoplado ao Spark e ao ecossistema comercial do Databricks (embora hoje seja mais aberto do que era originalmente), o Hudi foi desenhado com forte foco em ingestão incremental de baixa latência. O Iceberg, por sua vez, foi desenhado desde o início para ser agnóstico ao motor de processamento: a especificação da tabela não pertence a nenhum motor específico, e tanto o Spark quanto o Trino (a ferramenta que consulta e constrói a camada Gold, assunto da próxima parte) leem e escrevem o mesmo formato através do mesmo catálogo.

Essa característica não foi um detalhe teórico aqui: é justamente o que faz o Spark (que escreve a Silver) e o Trino (que lê a Silver para construir a Gold, e também escreve as tabelas de resultado dos modelos de IA) enxergarem exatamente a mesma verdade sobre cada tabela, sem exportar, converter ou sincronizar nada manualmente entre os dois mundos.

Apache Spark, Hadoop & Apache Spark and Parquet & Orc Format

Apache Spark, Hadoop & Apache Spark and Parquet & Orc Format

Processamento com Apache Spark

O motor que lê a Bronze, normaliza e grava na Silver é o Apache Spark. A limitação mais óbvia do pandas para esse tipo de carga é que ele processa tudo numa única máquina, num único processo: para transformar mais de um milhão de linhas de empenhos por execução, isso deixa de escalar bem em algum momento.

O Spark nasceu em 2014 (originado como projeto acadêmico no AMPLab da Universidade da Califórnia em Berkeley, depois doado à Apache) como resposta direta a uma limitação do próprio Hadoop MapReduce, o motor de processamento distribuído que veio antes dele: o MapReduce escreve o resultado de cada etapa intermediária em disco antes de seguir para a próxima, o que o tornava seguro, porém lento para cargas de trabalho com muitas etapas encadeadas. O Spark mantém dado intermediário em memória entre as etapas sempre que possível, o que historicamente trouxe ganhos de dezenas de vezes em desempenho para esse tipo de carga, e foi um dos fatores que tornou o MapReduce, hoje, um motor raramente escolhido para projeto novo.

Vale registrar que o mercado atual tem alternativas relevantes ao Spark para processamento distribuído: Apache Flink (mais focado em streaming contínuo) e Dask (mais leve, mais próximo do ecossistema Python/pandas) são exemplos. A escolha do Spark aqui não foi feita no vácuo: é o motor com o suporte mais maduro e mais direto ao Iceberg, e processar em lote diário (não em streaming contínuo) é exatamente o cenário para o qual ele foi desenhado.

O MERGE INTO como coração da camada Silver

A peça central do job de Silver (src/spark_jobs/silver_job.py) é um MERGE INTO, o comando que decide, linha a linha, se um registro do lote recém-chegado já existe na tabela (e deve atualizar) ou é novo (e deve ser inserido), usando uma chave de negócio definida por fonte (o identificador do contrato, ou o par identificador+ano para empenhos e ordens bancárias, já que a fonte original não garante identificador único sozinho).

Dois detalhes reais valem a pena contar, porque mostram problemas que você pode se deparar ao lidar com dados reais:

  • Desempate determinístico. Quando a mesma data_extracao traz duas versões do mesmo registro (a fonte, às vezes, responde de forma levemente diferente para a mesma linha em requisições diferentes), simplesmente remover duplicatas com a função padrão do Spark escolhe uma linha de forma não determinística, pode mudar a cada execução, dependendo do plano físico interno. A solução foi ordenar por um hash calculado sobre todas as colunas da linha, garantindo que a escolha seja sempre a mesma, execução após execução, sem inventar um conceito de "mais recente" que a fonte não fornece (não existe coluna de última atualização por linha).
  • Tipo inconsistente entre lotes. O mesmo campo, vindo de uma API JSON, às vezes chega como texto e às vezes como verdadeiro/falso, dependendo do lote. Sem tratar isso, o MERGE falha inteiro por causa de uma única coluna inconsistente. A correção converte cada coluna do lote para o tipo já estabelecido na tabela antes de tentar o merge: um valor realmente incompatível vira nulo (conversão best-effort), em vez de derrubar a carga inteira.

Dessa forma, reprocessar o mesmo dia duas vezes seguidas resulta exatamente na mesma contagem de linhas, sem duplicar nada, a garantia que Parquet solto, sozinho, não seria capaz de dar.

Hive Metastore — A Ponte entre Dados e Metadados

Hive Metastore — A Ponte entre Dados e Metadados

Hive Metastore como Catálogo de dados

Duas ferramentas diferentes trabalham sobre as mesmas tabelas Iceberg: o Spark escreve a Silver, e o Trino lê a Silver para construir a Gold. Para as duas enxergarem exatamente a mesma coisa, existe um catálogo compartilhado, o Hive Metastore, que não guarda o dado em si, só sabe, para cada tabela, onde os arquivos dela estão e qual é a versão mais recente.

Há uma curiosidade aqui: o Hive Metastore nasceu como parte do Apache Hive, criado dentro do Facebook por volta de 2010, o primeiro motor a trazer uma linguagem parecida com SQL para cima do Hadoop. O Hive-motor-de-consulta, hoje, é raramente a primeira escolha para projeto novo: motores mais modernos como o próprio Trino o superam em desempenho para consulta interativa. Mas o Hive Metastore, a peça de catálogo, virou um padrão de fato para catalogar tabelas, falado nativamente por ferramentas muito mais novas (Spark e Trino incluídos).

Ecossistema Apache Hadoop

Ecossistema Apache Hadoop

O ecossistema Hadoop, além do HDFS

Vale um parênteses maior aqui, porque este artigo já cruzou com peças do ecossistema Hadoop mais de uma vez (o MapReduce, na seção sobre o Spark, o Hive Metastore, na seção anterior), sem deixar claro que Hadoop não é um produto único: é um guarda-chuva de ferramentas que cresceu em torno da mesma ideia original, guardar e processar dado em grande volume, distribuído entre máquinas comuns.

O HDFS é a camada de armazenamento desse guarda-chuva. O MapReduce foi o motor de processamento original: um jeito de dividir um cálculo grande em duas etapas, uma que distribui o trabalho entre várias máquinas (“map”) e outra que junta os resultados parciais (“reduce”), a base sobre a qual o Hadoop provou que processamento distribuído em hardware comum era viável, muito antes de o Spark existir. Em cima dessas duas camadas de base, cresceram outras ferramentas pensadas para tornar o Hadoop utilizável por um público maior do que só quem sabia escrever MapReduce diretamente:

  • Apache Hive: uma camada de consulta que traduz comandos parecidos com SQL (HiveQL) em jobs de processamento distribuído por baixo dos panos, permitindo escrever uma consulta declarativa em vez de código de processamento na mão. O catálogo que esse motor usa para saber onde cada tabela está guardada, o Hive Metastore, é justamente a peça que este projeto reaproveita, mesmo sem rodar o motor de consulta Hive propriamente dito.
  • Apache Pig: uma linguagem de script (Pig Latin) para descrever pipelines de transformação de dado passo a passo, pensada para quem prefere um fluxo mais procedural do que consultas declarativas em SQL, uma alternativa ao Hive para o mesmo tipo de tarefa.
  • Apache HBase: um banco de dados NoSQL, orientado a colunas, construído sobre o próprio HDFS, inspirado no Bigtable do Google. Resolve um problema que o HDFS sozinho não resolve bem: leitura e escrita aleatória, rápida, linha a linha, num volume grande de dado, algo que um sistema de arquivos pensado para escrita em bloco e leitura sequencial não entrega.
  • Apache Sqoop: feito especificamente para mover dado entre bancos relacionais e o HDFS, em lote, nos dois sentidos. É o mesmo tipo de problema que os extractors deste projeto resolvem na mão (parte 2 desta série, extraindo do PostgreSQL de origem pro Bronze), com a diferença de que aqui a escolha foi um script Python próprio, não o Sqoop, para manter controle fino sobre paginação, watermark incremental e validação que o pipeline já precisa ter de qualquer forma.

Todas essas ferramentas (MapReduce, Hive, Pig) têm uma característica em comum: processam dado em lote, o mesmo padrão que a Silver deste projeto usa. Para o cenário onde o dado precisa ser processado assim que chega, não só uma vez por dia, o mesmo ecossistema cresceu uma segunda família, voltada a streaming: Apache Kafka (fila de mensagens distribuída, para transportar eventos em tempo real entre sistemas), Apache Flume (feito para coletar log e evento continuamente e entregar no HDFS) e Apache Storm (processamento de streaming em tempo real, evento a evento), além do Spark Streaming, a extensão do próprio Spark para o mesmo problema. Nenhuma delas se aplica aqui: as duas fontes deste projeto (API paginada, banco relacional) são naturalmente de lote, não fluxos contínuos de evento, e processar uma vez por dia, como a DAG bronze_extract já faz, resolve o problema real sem a complexidade adicional de um pipeline de streaming.

Nenhuma das ferramentas citadas nesta seção (Hive-motor, Pig, HBase, Sqoop, e a família de streaming) é usada neste projeto: a Silver e a Gold aqui rodam sobre Spark, Iceberg e Trino, motores mais novos que assumiram boa parte desses papéis. Mas entender que elas existem lado a lado, todas gravitando em torno do mesmo par HDFS + MapReduce, explica por que o ecossistema Hadoop foi desenhado desde o início para múltiplas ferramentas conviverem sobre o mesmo armazenamento e o mesmo catálogo, exatamente o padrão que o Spark e o Trino aproveitam aqui.

DockerOperator, e não modo cliente do Spark

Nem toda decisão técnica se resolve só com comparação de mercado: às vezes o risco só aparece testando a combinação específica de peças envolvidas. Rodar o job de Silver em modo cliente do Spark (SparkSubmitOperator), por exemplo, colocaria o processo "condutor" (driver) dentro do próprio container do Airflow, com os executores respondendo de containers separados do cluster Spark: uma combinação propensa a problemas de rede entre executores e driver, além de a imagem do Airflow, de qualquer forma, não ser um bom ambiente para rodar Spark de verdade.

A orquestração do job de Silver no Airflow (DAG silver_transform) evita esse risco com um DockerOperator: o Airflow apenas dispara um container dedicado, já com o runtime Spark completo (Java 17, Spark 3.5.3, o jar do Iceberg embutido), rodando spark-submit em modo local dentro dele mesmo, sem depender de rede entre containers separados para a execução diária. O cluster Spark dedicado (spark-master/spark-worker) continua existindo, mas reservado para reprocessamentos grandes e pontuais do histórico completo, não para a rotina diária.

O que vem a seguir

Com a Silver normalizada, deduplicada entre execuções e catalogada de um jeito que Spark e outras ferramentas enxergam da mesma forma, falta a última transformação: organizar esse dado num modelo pensado para consulta de negócio (dimensões, fatos) e testes automáticos que garantem que a modelagem não quebrou silenciosamente. Essa é a camada Gold, construída de forma declarativa com dbt sobre o Trino, assunto da parte 4.


메타데이터
post_id
37248dcf5c4b
slug
construindo-um-lakehouse-de-dados-públicos-parte-3-de-arquivo-solto-a-tabela-de-verdade-37248dcf5c4b
url
https://medium.com/@jaimejrs/construindo-um-lakehouse-de-dados-p%C3%BAblicos-parte-3-de-arquivo-solto-a-tabela-de-verdade-37248dcf5c4b
canonical_url
https://medium.com/@jaimejrs/construindo-um-lakehouse-de-dados-p%C3%BAblicos-parte-3-de-arquivo-solto-a-tabela-de-verdade-37248dcf5c4b
author_url
https://medium.com/@jaimejrs
status
ok
fetched_at
2026-08-06 12:20:19