Apostila completa de Engenharia de Dados
Toda decisão orientada por dados — e todo agente de IA que presta — depende de um dado que chegou íntegro, no horário certo e no formato certo. Esta apostila constrói a disciplina que garante isso: da primeira ingestão em batch ao lakehouse transacional, da orquestração de DAGs ao streaming exactly-once, sempre com o olho no que empresas realmente pedem em vagas e entrevistas.
O que é Engenharia de Dados e onde ela vive
Objetivo: entender o problema que a Engenharia de Dados resolve, como o papel se distingue de analista, cientista e ML engineer, e que forma tem a stack moderna.
1.1 O problema: dado existe, dado confiável não
Quase toda empresa já tem dados: num banco de produção, numa planilha, num CRM SaaS, nos logs de um servidor, num arquivo que alguém exporta toda segunda-feira. O que quase nenhuma empresa tem de graça é esse dado reunido, limpo, histórico, versionado e disponível no momento em que uma decisão precisa dele.
Engenharia de Dados é a disciplina que projeta e opera os sistemas que transformam dados dispersos e crus em produtos de dados confiáveis: tabelas, APIs e feeds que um analista, um dashboard, um modelo de ML ou um agente de IA podem consumir sem precisar saber de onde aquilo veio nem se está certo.
O produto do engenheiro de dados não é código — é confiança. Um pipeline que roda mas entrega número errado é pior do que pipeline nenhum, porque alguém vai decidir com base nele. Todo o resto da apostila (idempotência, testes, contratos, observabilidade) existe para tornar essa confiança verificável.
1.2 Quem faz o quê: o time de dados
| Papel | Pergunta que responde | Ferramentas típicas |
|---|---|---|
| Engenheiro de Dados | "Como faço esse dado chegar limpo, no horário, todo dia, sem eu precisar acordar de madrugada?" | SQL, Python, dbt, Airflow/Dagster, Spark, Kafka, warehouse/lakehouse, Terraform |
| Analytics Engineer | "Como transformo as tabelas cruas em modelos de negócio que o time de BI entende?" | dbt, SQL, warehouse, ferramenta de BI |
| Analista de Dados / BI | "O que os dados dizem sobre o negócio?" | SQL, BI (Power BI, Looker, Metabase), planilha |
| Cientista de Dados | "Consigo prever / explicar / segmentar isso?" | Python, notebooks, scikit-learn, estatística |
| ML / AI Engineer | "Como coloco esse modelo/agente em produção e o mantenho?" | Python, feature store, MLOps/LLMOps, APIs |
| Platform / DataOps Engineer | "Como o time inteiro roda isso com segurança, custo controlado e sem reinventar a roda?" | Kubernetes, Terraform, CI/CD, IAM, observabilidade |
Em empresas pequenas, uma pessoa faz vários desses papéis (o famoso "time de dados de 1"). Em empresas grandes, são times separados. O engenheiro de dados fica na fronteira entre os sistemas de produção e o mundo analítico/IA — é quem conhece os dois lados.
1.3 A stack de dados moderna, em uma imagem mental
FONTES INGESTÃO ARMAZENAMENTO TRANSFORMAÇÃO CONSUMO ┌───────────────────┐ ┌──────────────┐ ┌──────────────────┐ ┌───────────────┐ ┌────────────────┐ Banco OLTP (Postgres) ─▶ CDC/Debezium ─▶ APIs SaaS (Stripe...) ─▶ Airbyte/ELT ─▶ Data Lake (S3) + dbt / Spark SQL BI / dashboards Eventos de app ─▶ Kafka ─▶ Lakehouse (Iceberg/ ─▶ modelagem + ─▶ ML / feature store Arquivos (CSV/Parquet)─▶ batch load ─▶ Delta) ou Warehouse testes + docs Reverse ETL → SaaS Logs ─▶ Fluent Bit ─▶ (BigQuery/Snowflake) Agentes de IA / RAG └───────────────────┘ └──────────────┘ └──────────────────┘ └───────────────┘ └────────────────┘ ▲ ▲ ▲ Orquestração (Airflow / Dagster / Prefect) coordena tudo: agenda, dependências, retries, backfill Governança & observabilidade (catálogo, lineage, testes de qualidade, custo) cobre todas as etapas
Essa é a espinha dorsal de praticamente todo sistema analítico sério em 2026. Cada módulo desta apostila aprofunda uma dessas caixas. Note dois blocos transversais, que atravessam todas as etapas: orquestração (Módulo 6) e governança/observabilidade (Módulo 9).
1.4 As competências de base (o que assumir daqui pra frente)
- SQL de verdade: não só
SELECT ... WHERE, masJOIN,GROUP BY, window functions, CTEs, e entender plano de execução. SQL é a língua franca de dados e não vai a lugar nenhum. - Python: manipular dados (pandas/Polars), chamar APIs, escrever scripts robustos com tratamento de erro, empacotar código. Não precisa ser cientista da computação, precisa ser confiável.
- Linha de comando + Git: você vai viver no terminal e todo pipeline sério mora num repositório com CI.
- Modelagem de dados: saber desenhar tabelas que fazem sentido para consulta analítica (Módulo 5).
- Noções de sistemas distribuídos e cloud: o porquê de particionar, o que é um objeto no S3, o que custa dinheiro.
Engenharia de Dados é uma das áreas de dados com melhor relação oferta/demanda: há mais vagas do que gente com a combinação "SQL forte + Python + orquestração + um pouco de cloud". Diferente de Ciência de Dados (saturada no nível júnior), Engenharia de Dados raramente é primeira vaga — costuma vir de quem já foi analista, dev backend ou de infra. A boa notícia: quase tudo é aprendível com projeto prático, e o ferramental (dbt, Airflow, DuckDB, Spark) roda de graça na sua máquina.
✏️ Exercício 1 — De quem é a tarefa?
Classifique cada tarefa como Engenharia de Dados (ED), Analytics Engineering (AE), Análise/BI (BI) ou Ciência de Dados (DS): (a) montar um job diário que copia a base de pedidos do Postgres para o lakehouse; (b) construir o modelo dim_clientes com regras de negócio a partir das tabelas cruas; (c) descobrir por que a receita caiu 8% em março; (d) treinar um modelo de churn; (e) garantir que o job de (a) avise no Slack quando o volume de linhas cair 50%.
Gabarito: (a) ED — ingestão e pipeline; (b) AE — transformação/modelagem de negócio, tipicamente em dbt; (c) BI — análise exploratória; (d) DS — modelagem preditiva; (e) ED — observabilidade de pipeline. Note que (b) e (e) são as fronteiras onde os papéis se misturam.
Fundamentos: batch × streaming, ETL × ELT, OLTP × OLAP e formatos de arquivo
Objetivo: fixar os quatro pares conceituais que estruturam todas as decisões de arquitetura de dados — e reconhecer os formatos de arquivo que fazem a diferença entre um pipeline barato e um caro.
2.1 Batch × Streaming
| Batch (lote) | Streaming (fluxo) | |
|---|---|---|
| Unidade | Um conjunto de registros processado de uma vez (a "carga das 3h") | Um registro (ou micro-lote) processado assim que chega |
| Latência | Minutos a horas | Milissegundos a segundos |
| Complexidade | Baixa: começa, termina, tem resultado | Alta: processo sempre ligado, precisa lidar com dado atrasado, fora de ordem, reprocessamento |
| Custo | Menor e previsível | Maior: infraestrutura 24/7 |
| Use quando | A decisão pode esperar até a próxima carga (relatórios, ML de treino, faturamento) | A decisão é agora (detecção de fraude, alerta operacional, dashboard ao vivo, feature em tempo real) |
A grande maioria dos casos de negócio é resolvida por batch — muitas vezes por micro-batch de 5 a 15 minutos, que já parece "quase tempo real" para o usuário e custa uma fração do streaming verdadeiro. Só vá para streaming (Módulo 8) quando o requisito de latência for real e alguém puder articular o custo de esperar 10 minutos. "O CEO quer ver ao vivo" raramente sobrevive à pergunta "e se atualizar a cada 5 minutos?".
2.2 ETL × ELT
As três letras: Extract (tirar da fonte), Transform (limpar, juntar, aplicar regra de negócio), Load (gravar no destino analítico). O que mudou foi a ordem:
- ETL (clássico, anos 1990–2010): transforma antes de carregar, num servidor de processamento dedicado. Nasceu quando armazenamento em data warehouse era caríssimo — você só guardava o dado já refinado.
- ELT (padrão moderno): carrega o dado cru no lake/warehouse primeiro e transforma lá dentro, com SQL, usando o poder de processamento elástico da própria plataforma. Virou padrão porque armazenamento ficou barato e warehouses/engines ficaram absurdamente rápidos.
Vantagens do ELT que caem em entrevista: você preserva o dado bruto (pode reprocessar com lógica nova sem re-extrair da fonte), a transformação vira código SQL versionado e testável (dbt), e você não mantém um servidor de ETL separado.
2.3 OLTP × OLAP
| OLTP (transacional) | OLAP (analítico) | |
|---|---|---|
| Para quê | Rodar o negócio: inserir pedido, atualizar saldo | Entender o negócio: somar vendas por região por mês |
| Consultas | Muitas, pequenas, por chave, leem/escrevem poucas linhas | Poucas, grandes, varrem milhões de linhas, agregam |
| Armazenamento | Orientado a linha (row-store): a linha inteira junta no disco | Orientado a coluna (columnar): cada coluna junta no disco |
| Exemplos | PostgreSQL, MySQL, SQL Server | BigQuery, Snowflake, Redshift, ClickHouse, DuckDB, Spark |
Uma consulta analítica típica toca 3 colunas de uma tabela de 200. Num row-store, ler essas 3 colunas obriga a carregar as 200 (a linha inteira). Num columnar store, você lê só os 3 arquivos de coluna relevantes — e como valores de uma mesma coluna são parecidos, a compressão é brutal (run-length, dictionary encoding). Menos I/O + menos bytes = consultas 10–100× mais rápidas e mais baratas. É por isso que o formato Parquet (seção 2.4) é columnar.
2.4 Formatos de arquivo: o detalhe que decide o custo
| Formato | Tipo | Bom para | Observações |
|---|---|---|---|
| CSV | Texto, linha | Troca simples, inspeção humana | Sem tipos, sem compressão, sem esquema, lento. Aceite na borda, converta imediatamente. |
| JSON / JSONL | Texto, linha | APIs, dados aninhados, logs | Verboso e lento para analítica. JSONL (um objeto por linha) é o mínimo aceitável para processar em paralelo. |
| Parquet | Binário, colunar | Analítica — o padrão de fato | Compressão forte, esquema embutido, predicate pushdown e leitura só das colunas necessárias. Use por padrão. |
| ORC | Binário, colunar | Ecossistema Hive/Hadoop | Parecido com Parquet; comum em stacks legadas da Cloudera. |
| Avro | Binário, linha | Streaming, eventos Kafka | Ótimo para escrita registro a registro e evolução de esquema; ruim para varredura analítica. |
Regra prática: Avro (ou Protobuf) no transporte de eventos; Parquet no armazenamento analítico. CSV e JSON são formatos de borda — tolere na entrada, nunca deixe virar a base do seu lake.
Perguntas reais deste módulo: "Qual a diferença entre ETL e ELT e por que ELT virou padrão?", "Por que Parquet é mais rápido que CSV para analítica?", "Quando você usaria streaming em vez de batch?" (a boa resposta menciona custo e o requisito real de latência, não "porque é mais moderno"). Saber explicar columnar vs row-store com o exemplo das 3 colunas de 200 já sinaliza que você entende o porquê, não só os nomes.
✏️ Exercício 2 — Escolhas de arquitetura
(a) Um time quer um dashboard de vendas atualizado "de manhã cedo, todo dia". Batch ou streaming? (b) Você recebe um dump diário de 4 GB em CSV de um parceiro. O que faz com ele? (c) Um sistema antifraude precisa bloquear cartão em até 2 segundos. Batch ou streaming? (d) Por que carregar o dado cru antes de transformar (ELT) ajuda quando a regra de negócio muda seis meses depois?
Gabarito: (a) Batch — uma carga noturna resolve. (b) Ingere o CSV na camada bronze, converte para Parquet particionado por data, valida esquema, e daí pra frente só trabalha com o Parquet. (c) Streaming — latência é requisito duro. (d) Porque o dado bruto histórico ainda está lá: você reescreve o SQL de transformação e reprocessa (backfill) sem precisar pedir os dados de novo à fonte, que pode nem tê-los mais.
Ingestão de dados: batch, CDC, APIs e filas
Objetivo: dominar as quatro formas de tirar dado de uma fonte e trazê-lo para dentro, com as ferramentas de mercado e os padrões que evitam duplicar ou perder registros.
3.1 As quatro fontes e como se lê cada uma
| Fonte | Como extrair | Ferramentas |
|---|---|---|
| Banco relacional de produção | Full load (cópia total) + incremental por coluna de data, ou CDC (seção 3.3) | Airbyte, Fivetran, Meltano, dlt, Debezium |
| API de SaaS (Stripe, HubSpot, GA4) | Chamadas HTTP paginadas, respeitando rate limit, guardando o cursor | Airbyte/Fivetran (conectores prontos), dlt, código próprio |
| Eventos de aplicação | App publica em um tópico; consumidor grava no lake | Kafka, Kinesis, Pub/Sub, RabbitMQ |
| Arquivos (SFTP, bucket, e-mail) | Sensor que detecta arquivo novo e dispara a carga | Airflow sensors, Lambda/Cloud Function, rclone |
3.2 Full load × incremental
- Full load: copia a tabela inteira toda vez. Simples e sempre correto, mas caro e lento acima de alguns milhões de linhas. Ótimo para tabelas pequenas e dimensões.
- Incremental por marca d'água (watermark): guarda o maior
updated_at(ouid) já carregado e, na próxima execução, pega sóWHERE updated_at > :ultima_marca. Barato, mas exige que a fonte tenha uma coluna confiável de atualização e não perde deletes (a linha apagada some da fonte sem deixar rastro).
# Ingestão incremental com a biblioteca dlt (Python) — padrão enxuto e testável import dlt from dlt.sources.sql_database import sql_database pipeline = dlt.pipeline( pipeline_name="vendas", destination="filesystem", # grava Parquet no lake dataset_name="bronze", ) source = sql_database().with_resources("pedidos", "itens_pedido") # incremental: dlt guarda o estado do cursor entre execuções source.pedidos.apply_hints(incremental=dlt.sources.incremental("updated_at")) info = pipeline.run(source, write_disposition="merge", primary_key="id") print(info)
3.3 CDC — Change Data Capture o jeito profissional de ler um banco
Em vez de consultar a tabela periodicamente, o CDC lê o log de transações do banco (o WAL do Postgres, o binlog do MySQL) e emite um evento para cada INSERT, UPDATE e DELETE. Vantagens decisivas:
- Captura deletes e updates — o incremental por watermark não captura.
- Não pesa na fonte: ler o log não compete com as consultas da aplicação.
- Baixa latência: a mudança aparece no destino em segundos.
- Histórico completo: você vê a sequência de estados de cada linha, não só o valor atual.
// Evento CDC do Debezium para um UPDATE (formato resumido) { "op": "u", // c=create, u=update, d=delete, r=snapshot "ts_ms": 1738000000000, "before": { "id": 42, "status": "pendente" }, "after": { "id": 42, "status": "pago" }, "source": { "table": "pedidos", "lsn": 987654321 } }
Arquitetura clássica: Debezium (conector) lê o WAL → publica no Kafka → um consumidor (ou Kafka Connect com sink) grava no lakehouse, onde um MERGE aplica os eventos na tabela final. É assim que se mantém uma réplica analítica quase em tempo real de um banco de produção.
"Vou fazer a aplicação gravar no banco e publicar no Kafka ao mesmo tempo." Isso quebra: se a segunda escrita falha, os sistemas divergem silenciosamente e não há transação entre eles. As soluções corretas são CDC (deriva os eventos do próprio log do banco) ou o transactional outbox (a aplicação grava o evento numa tabela outbox na mesma transação do dado, e um processo separado publica dali). Citar isso numa entrevista de system design vale pontos.
3.4 Idempotência e entrega: os dois problemas que sempre voltam
- Idempotência: rodar a mesma carga duas vezes deve dar o mesmo resultado que rodar uma. Consegue-se com
MERGE/upsertpor chave (em vez deINSERT), ou escrevendo em partições que são substituídas inteiras (overwrite da partição do dia). Sem idempotência, todo retry vira dado duplicado. - Semânticas de entrega: at-most-once (pode perder), at-least-once (pode duplicar — o padrão realista), exactly-once (não perde nem duplica — caro, exige transações do fim ao fim). Na prática você projeta para at-least-once + consumidor idempotente, que dá o efeito de exactly-once sem o custo.
Vagas de Engenharia de Dados testam ingestão o tempo todo: "Como você replicaria o banco de produção para o warehouse?" (resposta forte: CDC com Debezium+Kafka, snapshot inicial + stream, MERGE no destino, e o cuidado com deletes). "Sua carga incremental rodou duas vezes por causa de um retry. E agora?" (resposta: se o pipeline é idempotente, nada acontece — e você explica como garantiu isso). Ferramenta que aparece muito em job description hoje: Airbyte e Fivetran para conectores, dlt ganhando espaço em times que querem código versionado.
✏️ Exercício 3 — Desenhe a ingestão
Você precisa trazer três fontes para o lake: (a) a tabela clientes (80 mil linhas, muda pouco); (b) a tabela eventos_clique (500 milhões de linhas, só cresce, nunca atualiza); (c) a API do Stripe para pagamentos. Descreva a estratégia de cada uma.
Gabarito: (a) Full load diário — é pequena, e full load nunca erra; ou CDC se precisar de baixa latência. (b) Incremental por id/created_at (append-only, sem updates nem deletes, então watermark basta), particionando por data no destino. (c) Conector (Airbyte/dlt) com paginação por cursor, respeitando rate limit, incremental por created; tratar reembolsos, que chegam como novos objetos, não como update.
Armazenamento: Data Warehouse, Data Lake e Lakehouse
Objetivo: entender as três arquiteturas de armazenamento analítico, por que o lakehouse surgiu, e o que os formatos de tabela abertos (Iceberg, Delta, Hudi) resolvem.
4.1 Data Warehouse o clássico que funciona
Um banco analítico gerenciado, colunar, otimizado para SQL sobre grandes volumes: BigQuery, Snowflake, Redshift, Databricks SQL. Você joga dados estruturados dentro, escreve SQL, e a plataforma cuida de armazenamento, índice, escala e otimização.
- Prós: SQL puro, rapidíssimo, governança madura (permissões, auditoria), zero infra para gerir.
- Contras: dado semiestruturado e não estruturado (imagem, áudio, texto livre, logs brutos) cabe mal; você fica preso ao formato interno do fornecedor; custo de armazenamento é mais alto que object storage cru.
4.2 Data Lake barato e flexível, perigoso se largado
Object storage (S3, GCS, Azure Blob) cheio de arquivos — Parquet, JSON, CSV, imagens — organizados em pastas. Barato, guarda qualquer coisa, qualquer engine lê (Spark, Trino, DuckDB).
Um lake sem catálogo, sem esquema forçado e sem controle vira um pântano: ninguém sabe o que tem, qual arquivo está certo, se aquele vendas_final_v2_ok.parquet é confiável. Além disso, arquivos soltos não têm transações: se um job falha no meio da escrita, você fica com dados parciais e leitores enxergam lixo.
4.3 Lakehouse o padrão de 2026
A ideia: ficar com o armazenamento barato e aberto do lake (Parquet no S3) e ganhar por cima dele as garantias do warehouse — transações ACID, esquema forçado, evolução de esquema, versionamento (time travel), UPDATE/DELETE/MERGE. Isso é feito por uma camada de formato de tabela aberto que fica entre os arquivos e as engines:
| Formato de tabela | Origem | Notas |
|---|---|---|
| Apache Iceberg | Netflix | O que mais cresce; adotado por quase todos os fornecedores como padrão neutro. Bom em evolução de esquema e particionamento oculto (hidden partitioning). |
| Delta Lake | Databricks | Maduro, forte no ecossistema Spark/Databricks; open source, mas historicamente com recursos de ponta primeiro no Databricks. |
| Apache Hudi | Uber | Nasceu focado em upserts e CDC com baixa latência; nicho forte em ingestão incremental pesada. |
Como funciona por baixo (vale para os três): além dos arquivos Parquet de dados, existe uma camada de metadados — arquivos de manifesto que listam quais Parquet compõem a tabela naquela versão. Um commit é trocar o ponteiro de metadados atomicamente. Daí saem: transações (leitores veem sempre uma versão consistente), time travel (basta apontar para um snapshot antigo), e DELETE sem reescrever tudo (marca arquivos/linhas como removidos).
-- Com uma tabela Iceberg, isto simplesmente funciona sobre arquivos no S3: MERGE INTO silver.clientes t USING bronze.clientes_cdc s ON t.id = s.id WHEN MATCHED AND s.op = 'd' THEN DELETE WHEN MATCHED THEN UPDATE SET * WHEN NOT MATCHED THEN INSERT *; -- time travel: como estava a tabela ontem? SELECT * FROM silver.clientes FOR TIMESTAMP AS OF '2026-08-29 00:00:00';
4.4 Arquitetura Medallion (bronze / silver / gold)
A forma canônica de organizar as camadas dentro do lakehouse:
| Camada | Conteúdo | Quem consome |
|---|---|---|
| 🥉 Bronze (raw) | Cópia fiel da fonte, sem transformação, só tipada e particionada. Append-only, imutável, com _ingested_at. | Só pipelines. É o seu "backup reprocessável". |
| 🥈 Silver (cleaned/conformed) | Dado limpo, deduplicado, com tipos corretos, chaves resolvidas, joins básicos entre fontes. Uma linha = um fato/entidade real. | Analytics engineers, cientistas de dados. |
| 🥇 Gold (business/marts) | Modelos agregados e prontos para consumo: fct_vendas_diarias, dim_cliente, KPIs. Modelagem dimensional (Módulo 5). | BI, dashboards, relatórios executivos, feature store. |
Por que essa separação importa: quando uma regra de negócio muda, você reescreve silver/gold e faz backfill a partir do bronze, sem tocar na fonte. Quando um dashboard está errado, você sabe em qual camada investigar.
4.5 Warehouse ou lakehouse? a decisão real
| Prefira Warehouse (BigQuery/Snowflake) quando… | Prefira Lakehouse (Iceberg/Delta + engine) quando… |
|---|---|
| Dados majoritariamente estruturados e o time vive de SQL | Há muito dado semi/não estruturado (logs, JSON, texto, mídia para IA) |
| Time pequeno, quer zero infra e time-to-value rápido | Volume é enorme e o custo de armazenamento do warehouse dói |
| Governança e permissões finas são prioridade imediata | Quer evitar lock-in e usar várias engines sobre o mesmo dado |
| Orçamento tolera o custo de compute do fornecedor | Cargas de ML/Spark convivem com as de SQL sobre a mesma tabela |
Na prática, muitos times fazem híbrido: lake/lakehouse para bronze/silver e ingestão bruta, warehouse (ou a camada SQL do próprio lakehouse) para gold e BI. E a fronteira está se dissolvendo: Snowflake e BigQuery leem Iceberg nativamente hoje.
Termos que precisam estar na ponta da língua: medallion (bronze/silver/gold), Iceberg vs Delta, time travel, "schema-on-read vs schema-on-write", data swamp. Pergunta comum: "O que é um lakehouse e que problema ele resolve em relação a lake e warehouse?" — resposta: traz ACID, esquema e MERGE para cima de arquivos baratos e abertos, evitando o pântano do lake sem o lock-in e o custo do warehouse. Databricks e Snowflake dominam as job descriptions; saber os dois conceitualmente cobre a maioria.
✏️ Exercício 4 — Onde mora cada coisa
Classifique em bronze, silver ou gold: (a) o JSON cru da API do Stripe, exatamente como veio; (b) uma tabela pagamentos deduplicada, com valores em centavos convertidos para reais e status normalizado; (c) fct_receita_mensal_por_plano usada no dashboard do board; (d) eventos CDC brutos do Debezium.
Gabarito: (a) bronze; (b) silver; (c) gold; (d) bronze. Dica: se um humano de negócio consome direto, é gold; se é cópia da fonte, é bronze; o meio de campo limpo é silver.
Transformação com SQL, dbt e modelagem dimensional
Objetivo: transformar dado cru em modelo de negócio de forma versionada, testada e documentada — e modelar as tabelas gold do jeito que BI e mercado esperam.
5.1 dbt: transformação como engenharia de software
dbt (data build tool) é o padrão de fato da camada T do ELT. A ideia é simples e poderosa: você escreve cada transformação como um SELECT num arquivo .sql, e o dbt cuida de criar a tabela/view, resolver a ordem de execução (grafo de dependências), rodar testes e gerar documentação.
-- models/silver/stg_pedidos.sql with fonte as ( select * from {{ source('bronze', 'pedidos') }} ), limpo as ( select id as pedido_id, cliente_id, lower(trim(status)) as status, valor_centavos / 100.0 as valor_reais, cast(criado_em as timestamp) as criado_em from fonte where id is not null ) select * from limpo
# models/silver/stg_pedidos.yml — testes e documentação junto do modelo models: - name: stg_pedidos description: "Pedidos limpos e tipados, um por linha." columns: - name: pedido_id description: "Chave primária do pedido." tests: [unique, not_null] - name: status tests: - accepted_values: { values: ['pago', 'pendente', 'cancelado'] } - name: cliente_id tests: - relationships: { to: ref('stg_clientes'), field: cliente_id }
O que o dbt te dá "de graça": dbt run materializa tudo na ordem certa; dbt test valida os contratos; dbt docs gera um site navegável com o lineage (de qual fonte cada coluna veio); ref() torna o grafo de dependências explícito; ambientes (dev/prod) isolados; e versionamento git com pull request e CI como qualquer código.
5.2 Materializações
| Materialização | O que faz | Use quando |
|---|---|---|
view | Cria uma view (recalcula a cada consulta) | Modelo leve, dado sempre fresco, staging |
table | Recria a tabela inteira a cada dbt run | Modelo pesado consultado muitas vezes; tabela pequena/média |
incremental | Só processa e insere as linhas novas/alteradas | Tabelas de fatos grandes onde reprocessar tudo é caro |
ephemeral | Vira um CTE embutido, não cria objeto | Lógica intermediária reutilizada, sem valor de persistir |
5.3 Modelagem dimensional: star schema o que BI espera
A camada gold quase sempre segue o esquema estrela (Kimball): uma tabela fato no centro, cercada de tabelas dimensão.
- Fato (fct_): os eventos mensuráveis do negócio. Cada linha é uma ocorrência (uma venda, um clique, um pagamento). Colunas = chaves para as dimensões + métricas numéricas aditivas (valor, quantidade). Cresce sem parar.
- Dimensão (dim_): o contexto descritivo pelo qual você fatia as métricas —
dim_cliente,dim_produto,dim_data,dim_loja. Colunas = atributos textuais para filtrar e agrupar. Muda devagar.
┌─────────────┐
│ dim_data │
└──────┬──────┘
┌─────────────┐ ┌─────┴───────┐ ┌──────────────┐
│ dim_cliente ├───┤ fct_vendas ├───┤ dim_produto │
└─────────────┘ │ (grão: 1 │ └──────────────┘
│ item de │
│ pedido) │
└─────┬───────┘
┌──────┴──────┐
│ dim_loja │
Antes de escrever qualquer coluna, defina em uma frase o que uma linha da tabela fato representa: "uma linha = um item de um pedido". Todo o resto decorre disso. Misturar grãos (algumas linhas por pedido, outras por item) é o erro que estraga qualquer dashboard depois.
5.4 Slowly Changing Dimensions (SCD) avançado
Um cliente muda de cidade. A dimensão deve refletir só o valor atual, ou preservar o histórico?
- SCD Tipo 1: sobrescreve. Simples; perde história. Bom quando o passado não importa (corrigir um erro de digitação).
- SCD Tipo 2: cria uma nova linha para o novo estado, com
valido_de,valido_atee uma flagis_current. Preserva o histórico — a venda de janeiro continua ligada à cidade que o cliente tinha em janeiro. É o tipo que cai em entrevista; o dbt tem a macrosnapshotpronta para isso.
dbt é quase obrigatório em vagas de Analytics/Data Engineering hoje. O que testam: escrever um modelo com CTEs legíveis, configurar um teste (unique, not_null, relationships), explicar materialização incremental e sua cláusula is_incremental(), e desenhar um star schema a partir de um enunciado ("modele as vendas de uma rede de lojas"). Saber articular grão, fato vs dimensão e SCD tipo 2 separa o pleno do júnior.
✏️ Exercício 5 — Modele e teste
(a) Defina o grão de uma tabela fct_pagamentos de um app de assinatura. (b) Liste três dimensões plausíveis. (c) O plano do cliente mudou de "Pro" para "Enterprise" em maio — que tipo de SCD você usa em dim_cliente e por quê? (d) Que teste dbt garante que todo cliente_id em fct_pagamentos existe em dim_cliente?
Gabarito: (a) "uma linha = uma cobrança processada". (b) dim_cliente, dim_plano, dim_data (poderia ter dim_metodo_pagamento). (c) SCD Tipo 2 — para que a receita de abril continue atribuída ao plano "Pro" e a de junho ao "Enterprise"; um relatório de MRR por plano fica errado com Tipo 1. (d) o teste relationships apontando de fct_pagamentos.cliente_id para dim_cliente.cliente_id.
Orquestração: Airflow, Dagster e Prefect
Objetivo: coordenar dezenas de tarefas interdependentes com agendamento, retries, backfill e alertas — e conhecer as três ferramentas que o mercado usa, com seus trade-offs.
6.1 Por que orquestração existe
Um pipeline real não é um script. É: "às 2h, extraia 6 fontes (algumas dependem de VPN); quando todas terminarem, rode 40 modelos dbt na ordem certa; se o modelo de receita passar nos testes, atualize o dashboard e o feed de ML; se qualquer coisa falhar, tente de novo 3 vezes com espera crescente e, se ainda falhar, chame alguém no Slack; e se eu precisar recalcular março inteiro, me deixe reprocessar dia a dia sem duplicar nada".
Um orquestrador resolve isso: você descreve o DAG (grafo acíclico dirigido) de tarefas e dependências, e ele cuida de agendamento, execução, paralelismo, retries, estado, histórico e observabilidade.
6.2 As três ferramentas
| Apache Airflow | Dagster | Prefect | |
|---|---|---|---|
| Modelo mental | Tarefas e dependências ("faça isto, depois aquilo") | Ativos de dados ("esta tabela é feita a partir daquelas") + tarefas | Fluxos Python com o mínimo de cerimônia |
| Maturidade / mercado | Padrão dominante; está em quase toda job description | Cresce rápido; forte em times dbt e "data as assets" | Nicho; adorado por quem quer DX leve |
| Ponto forte | Ecossistema gigante de providers, maturidade, comunidade | Lineage nativo, testabilidade local, tipagem, developer experience | Escrever pipeline parece escrever Python normal; dynamic mapping fácil |
| Ponto fraco | Boilerplate, teste local chato, scheduler historicamente pesado | Base instalada menor; curva do modelo de assets | Menos conectores prontos; comunidade menor |
# Airflow 2/3 — DAG com a TaskFlow API from airflow.decorators import dag, task from pendulum import datetime @dag( schedule="0 2 * * *", # todo dia às 2h start_date=datetime(2026, 1, 1), catchup=False, # não reprocessa o passado ao ligar default_args={"retries": 3, "retry_delay": 300}, tags=["vendas"], ) def pipeline_vendas(): @task def extrair(fonte: str) -> str: # idempotente: sobrescreve a partição da data de execução return carregar_bronze(fonte, particao="{{ ds }}") @task def transformar(caminhos: list): dbt_run(select="silver+ gold+") @task def publicar(): atualizar_marts_e_feature_store() dados = extrair.expand(fonte=["pedidos", "itens", "clientes"]) transformar(dados) >> publicar() pipeline_vendas()
6.3 Os conceitos que você tem que dominar
- Agendamento e data interval: o Airflow pensa em janelas. O run "de 29/08" processa os dados do dia 29 e roda depois que o dia fecha. A variável
{{ ds }}(data do intervalo, não "hoje") é o que torna o pipeline reprodutível. - Idempotência + backfill: como cada run é ligado a uma data e sobrescreve a partição daquela data, você pode reexecutar qualquer intervalo (backfill) e o resultado é o mesmo. Pipeline não idempotente + backfill = dados duplicados.
- catchup: ao ativar um DAG com
start_dateno passado,catchup=Truefaz ele rodar todos os intervalos perdidos. Quase sempre você quercatchup=Falsee faz backfill manual e controlado. - Retries com backoff: falhas transitórias (rede, rate limit) se resolvem sozinhas em 3 tentativas com espera crescente. Falhas de lógica não — não mascare bug com retry.
- Sensores e event-driven: esperar um arquivo chegar, uma tabela ficar pronta, um evento externo. Prefira sensores em modo reschedule/deferrable para não segurar um worker parado.
- Idempotência de side effects: se a tarefa manda e-mail ou dispara pagamento, retry pode reenviar. Proteja com chave de idempotência.
- SLA e alertas: "esse dado tem que estar pronto até 6h". O orquestrador dispara alerta se estourar, mesmo sem erro.
O Airflow/Dagster deve coordenar, não processar. Rodar um pandas de 30 GB dentro de uma task do Airflow trava o worker. O padrão certo: a task dispara um job no Spark/warehouse/Kubernetes e monitora; o processamento acontece lá, com recursos próprios. O orquestrador segura só o fio, não o balde.
Airflow é a aposta segura — aparece em ~70% das vagas de Engenharia de Dados e é o que você deve saber com profundidade. Dagster vale conhecer conceitualmente (o modelo de "assets" e o argumento a favor dele). Perguntas típicas: "O que é idempotência e por que ela importa num pipeline agendado?", "Explique backfill e catchup", "Sua DAG tem uma task que falha 1 vez a cada 20 execuções, sempre por timeout de rede. O que você faz?" (retry com backoff, e investigar se o timeout é ajustável — não só empilhar retries).
✏️ Exercício 6 — Conserte o pipeline
Um colega escreveu uma DAG que roda às 6h e faz INSERT INTO fct_vendas SELECT ... WHERE data = current_date - 1. Aponte dois problemas e como corrigir.
Gabarito: (1) Não é idempotente: se a DAG rodar duas vezes (retry, backfill), duplica as linhas do dia. Correção: DELETE da partição do dia antes do INSERT, ou MERGE, ou INSERT OVERWRITE da partição. (2) Usa current_date em vez da data do intervalo ({{ ds }}): um backfill de 30 dias atrás processaria "ontem" 30 vezes, nunca os dias corretos. Correção: parametrizar pela data lógica do run.
Processamento distribuído: Spark e as alternativas modernas
Objetivo: entender quando o dado deixa de caber em uma máquina, como o Spark divide o trabalho, o que é shuffle, e por que DuckDB/Polars/Trino roubaram uma parte grande do território do Spark.
7.1 Quando você realmente precisa de distribuído
Uma máquina na nuvem hoje tem 128+ vCPUs e 1 TB+ de RAM. DuckDB e Polars processam dezenas a centenas de GB de Parquet numa única máquina, mais rápido e mais barato que um cluster Spark, porque não pagam o custo de coordenação de rede. A pergunta profissional não é "qual framework de big data?", é "esse dado cabe numa máquina grande?" — e a resposta hoje é "sim" para a maioria dos times. Spark entra quando o volume passa de ~1 TB por job, quando você já tem um cluster, ou quando precisa de streaming/ML integrados.
7.2 Como o Spark divide o trabalho
- Partições: o Spark quebra o dataset em pedaços (partições); cada core processa uma partição por vez, em paralelo, entre os nós do cluster.
- Transformações lazy:
filter,select,joinnão executam na hora — constroem um plano. Só uma ação (write,count,collect) dispara a execução, e aí o otimizador (Catalyst) reescreve o plano inteiro. - Narrow vs wide transformations: narrow (map, filter) processa cada partição isolada — barato. Wide (
groupBy,join,distinct) precisa redistribuir dados entre nós pela chave — isso é o shuffle.
Shuffle: o gargalo que você tem que conhecer avançado
O shuffle escreve dados em disco e os transfere pela rede entre executores para reagrupá-los por chave. É a operação mais cara do Spark e a causa nº 1 de jobs lentos. Você reduz shuffle:
- Filtrando cedo (menos dado entra no shuffle) e selecionando só as colunas necessárias.
- Broadcast join: se um lado do join é pequeno (< ~10–100 MB), o Spark envia uma cópia dele para todos os nós e evita o shuffle do lado grande. O Adaptive Query Execution faz isso sozinho em versões recentes.
- Evitando data skew: se 80% das linhas têm a mesma chave, uma partição fica gigante e o job todo espera por ela. Técnicas: salting da chave, ou deixar o AQE dividir a partição quente.
# PySpark: leitura de Parquet particionado, filtro cedo, join com broadcast from pyspark.sql import functions as F from pyspark.sql.functions import broadcast vendas = (spark.read.parquet("s3://lake/silver/vendas/") .filter(F.col("data").between("2026-01-01", "2026-06-30")) # predicate pushdown .select("data", "loja_id", "valor")) lojas = spark.read.parquet("s3://lake/silver/dim_loja/") # pequena resultado = (vendas.join(broadcast(lojas), "loja_id") # sem shuffle do lado grande .groupBy("regiao", "data").agg(F.sum("valor").alias("receita"))) resultado.write.mode("overwrite").partitionBy("data").parquet("s3://lake/gold/receita_regiao/")
7.3 O ecossistema de engines em 2026
| Engine | Modelo | Use quando |
|---|---|---|
| Apache Spark (via Databricks, EMR, Glue) | Distribuído, batch + streaming + ML | Volumes de TB+, pipelines pesados, você já tem cluster, precisa de MLlib/Structured Streaming |
| DuckDB | Uma máquina, colunar, SQL, embutido no processo | Transformações de até centenas de GB, dev local, dentro de um container/Lambda, testes de dbt |
| Polars | Uma máquina, DataFrame em Rust, lazy | Substituir pandas em ETL Python; velocidade e uso de memória muito melhores |
| Trino / Presto | Distribuído, SQL federado, sem armazenamento próprio | Consulta interativa cruzando lake + várias fontes; camada de query do lakehouse |
| ClickHouse | Colunar, OLAP em tempo real | Analytics de baixa latência sobre eventos, dashboards que respondem em < 1 s |
Spark ainda é o item mais pedido em vagas sênior de Engenharia de Dados — e o que mais reprova em entrevista técnica, porque muita gente "usou" sem entender. O que testam: "O que é um shuffle e como você reduz?", "O que é data skew e como trata?", "Diferença entre transformação e ação", "Quando você não usaria Spark?" (resposta madura: quando o dado cabe numa máquina — cite DuckDB/Polars). Mostrar que você conhece as alternativas leves sinaliza maturidade, não ignorância.
✏️ Exercício 7 — Diagnóstico de job lento
Um job Spark que agrega 200 GB por cliente_id demora 40 min; 39 dos 40 minutos são gastos em uma única task no fim. As outras tasks terminam em 2 min. O que está acontecendo e o que você tenta?
Gabarito: data skew — provavelmente um cliente_id nulo ou um cliente "sistema" concentra a maioria das linhas, então uma partição do shuffle fica enorme. Tentativas: filtrar/tratar o valor nulo à parte; aplicar salting na chave (prefixo aleatório para dividir a chave quente e reagregar depois); habilitar/ajustar o skew join optimization do AQE; se um lado for pequeno, usar broadcast join.
Streaming em tempo real: Kafka, Flink e Spark Structured Streaming
Objetivo: entender o log distribuído do Kafka, o que muda ao processar um fluxo infinito (tempo de evento, janelas, watermarks, estado) e como se alcança exactly-once na prática.
8.1 Kafka: o log distribuído, append-only
Kafka não é uma fila que apaga a mensagem depois de lida. É um log particionado e durável: produtores anexam registros ao fim de um tópico; consumidores leem em seu próprio ritmo, guardando um offset (posição). Os dados ficam por um tempo de retenção configurável (dias, semanas ou "para sempre" com compaction).
- Tópico → partições: o paralelismo. A ordem é garantida dentro de uma partição, não entre partições. A chave do registro decide a partição (mesma chave → mesma partição → ordem preservada para aquela entidade).
- Consumer group: N consumidores dividem as partições de um tópico entre si; escala horizontal de leitura até o nº de partições.
- Replicação: cada partição tem réplicas em brokers diferentes; um leader atende, os followers copiam.
acks=all+min.insync.replicas=2= escrita durável. - Schema Registry: contratos de esquema (Avro/Protobuf) versionados para produtores e consumidores não quebrarem um ao outro.
8.2 O que muda ao processar um fluxo infinito
| Batch | Streaming |
|---|---|
| O dataset tem começo e fim | O fluxo nunca acaba; você processa "até agora" |
"Contar por dia" = GROUP BY data | "Contar por dia" exige definir janelas e decidir quando fechá-las |
| Dado atrasado: estava no arquivo | Dado atrasado chega depois da janela que ele pertencia — e agora? |
| Reprocessar = rodar de novo | Reprocessar = rebobinar offsets e lidar com estado acumulado |
Tempo de evento × tempo de processamento
Um clique aconteceu às 20h59 (event time) mas chegou ao seu processador às 21h03 (processing time), porque o celular estava sem sinal. Se você agrega por tempo de processamento, esse clique cai na janela errada. Sistemas sérios agregam por event time — e para isso precisam de watermarks.
Watermark
Um watermark é a afirmação do sistema: "acredito que já não vou receber eventos com timestamp anterior a T". Ele avança conforme o tempo de evento observado, com uma folga configurável (ex.: "tolero 5 min de atraso"). Quando o watermark passa do fim de uma janela, a janela fecha e emite o resultado. Eventos que chegam depois disso são late data: descartados, enviados para um side output, ou aplicados como atualização, conforme a política.
Tipos de janela
- Tumbling (fixa): blocos contíguos sem sobreposição — "a cada 1 min".
- Sliding (deslizante): janelas que se sobrepõem — "últimos 5 min, atualizando a cada 1 min".
- Session (sessão): agrupa eventos separados por menos de um gap de inatividade — sessão de navegação de um usuário.
8.3 Flink × Spark Structured Streaming × Kafka Streams
| Apache Flink | Spark Structured Streaming | Kafka Streams | |
|---|---|---|---|
| Modelo | Streaming nativo, evento a evento; latência ms | Micro-batch (e um modo contínuo); latência sub-segundo a segundos | Biblioteca Java/Scala embutida na sua app, Kafka-para-Kafka |
| Estado | Gestão de estado robusta (RocksDB), grandes estados, checkpoints incrementais | Estado ok; melhor se você já vive no ecossistema Spark | Estado local + changelog em tópico Kafka; sem cluster separado |
| Use quando | Latência real e lógica de streaming complexa (CEP, joins de streams, ML online) | Já tem Spark; latência de segundos é suficiente; unificar batch e stream no mesmo código | Transformação simples entre tópicos, sem querer operar mais um sistema |
-- Flink SQL: contagem de eventos por minuto, por tipo, tolerando 30 s de atraso SELECT window_start, tipo, COUNT(*) AS total FROM TABLE( TUMBLE(TABLE eventos, DESCRIPTOR(ts_evento), INTERVAL '1' MINUTE) ) GROUP BY window_start, tipo; -- ts_evento é event time; o watermark foi definido no DDL da tabela: -- WATERMARK FOR ts_evento AS ts_evento - INTERVAL '30' SECOND
8.4 Exactly-once na prática avançado
"Exactly-once" de verdade não significa que cada mensagem é fisicamente processada uma vez — significa que o efeito observável é como se fosse. Consegue-se combinando:
- Checkpointing: o processador salva periodicamente seu estado + os offsets lidos, de forma atômica. Em falha, ele volta ao último checkpoint e reprocessa a partir dali.
- Sink transacional ou idempotente: a escrita no destino participa da mesma transação do checkpoint (Kafka transactions, two-phase commit no Flink) ou o destino aceita a mesma linha duas vezes sem efeito (upsert por chave).
Um pipeline de streaming roda 24/7, precisa de gestão de estado, replay, monitoramento de lag de consumo, e é bem mais difícil de depurar que um batch. Muitos "requisitos de tempo real" são atendidos por micro-batch de 1–5 min a uma fração do custo operacional. Vá para streaming verdadeiro quando a latência de minutos gerar perda concreta (fraude, preços, alertas de segurança, features de recomendação ao vivo).
Kafka aparece em muitas vagas; Flink é diferencial sênior e em alta. Perguntas reais: "Explique partição, offset e consumer group no Kafka", "Qual a diferença entre event time e processing time, e o que é um watermark?", "Como você garante exactly-once de ponta a ponta?", "O que você monitora num consumidor Kafka?" (resposta: consumer lag acima de tudo, throughput, taxa de rebalanceamento, erros de desserialização). Saber dizer quando não usar streaming continua valendo pontos aqui também.
✏️ Exercício 8 — Janela e atraso
Você conta pedidos por janela tumbling de 10 minutos, por event time, com watermark de 2 minutos. Um pedido com timestamp 10:08 chega ao processador às 10:13. (a) Em qual janela ele entra? (b) E se chegasse às 10:15? (c) Por que não usar simplesmente processing time e acabar com a complicação?
Gabarito: (a) Na janela [10:00, 10:10). Às 10:13 o watermark está em ~10:11 (10:13 menos a folga de 2 min, aproximando), a janela [10:00,10:10) ainda não fechou (fecha quando o watermark passa de 10:10 + 2 min = 10:12... na prática ela fecha por volta de 10:12), então provavelmente ainda entra. (b) Às 10:15 o watermark já passou de 10:12; a janela fechou e o evento é late data — descartado ou mandado para tratamento à parte. (c) Com processing time, um lote de eventos atrasados por uma queda de rede seria todo contabilizado no minuto em que a rede voltou, distorcendo completamente a série temporal do negócio.
Qualidade, governança, observabilidade e DataOps
Objetivo: transformar um pipeline que "roda" num pipeline em que se pode confiar — com contratos, testes, monitoramento de saúde do dado, linhagem, catálogo, controle de custo e cultura de operação.
9.1 Data quality: as seis dimensões
| Dimensão | Pergunta | Como se testa |
|---|---|---|
| Completude | Faltam valores/linhas que deveriam existir? | not_null, contagem esperada de linhas, cobertura por chave |
| Unicidade | Há duplicatas na chave? | unique na PK, checagem de grão |
| Validade | Os valores estão no domínio permitido? | accepted_values, ranges, regex, tipos |
| Consistência | Bate entre tabelas/sistemas? | relationships (FK), reconciliação de totais com a fonte |
| Atualidade (freshness) | O dado é recente o suficiente? | idade do max(updated_at) vs SLA |
| Acurácia | Reflete o mundo real? | a mais difícil: amostragem, comparação com fonte-verdade, regras de negócio |
# Great Expectations / dbt tests / Soda — a ideia é a mesma: contratos executáveis # exemplo em dbt: teste custom de reconciliação (a soma tem que bater com a origem) # tests/assert_receita_bate_com_origem.sql — o teste passa se retornar 0 linhas with destino as (select sum(valor_reais) v from {{ ref('fct_vendas') }} where data = '{{ var("run_date") }}'), origem as (select sum(valor_centavos)/100.0 v from {{ source('bronze', 'pedidos') }} where data = '{{ var("run_date") }}') select destino.v, origem.v from destino, origem where abs(destino.v - origem.v) > 0.01
9.2 Data contracts avançado
O maior gerador de incidentes de dados: um time de produto renomeia uma coluna, muda um tipo ou o significado de um enum, sem avisar — e três pipelines quebram silenciosamente rio abaixo. Um data contract é um acordo versionado e validado em CI entre quem produz e quem consome o dado: esquema, tipos, semântica, garantias de qualidade, SLA de atualização, e política de mudança (o que exige major version). Na prática: um arquivo YAML/JSON Schema no repositório do produtor, checado no pipeline de deploy dele — a mudança incompatível falha o build do time de origem, não o seu dashboard às 8h.
9.3 Observabilidade de dados
Monitoramento de infra (CPU, "o job terminou?") não detecta o pior tipo de falha: o pipeline rodou com sucesso e entregou dado errado. Observabilidade de dados acrescenta cinco sinais (os "pilares", popularizados por ferramentas como Monte Carlo, e hoje também open source com Elementary, re_data, Soda):
- Freshness: a tabela atualizou dentro do esperado?
- Volume: o nº de linhas está dentro da faixa histórica? (queda de 40% = alerta)
- Schema: colunas adicionadas/removidas/retipadas sem aviso?
- Distribuição: % de nulos, cardinalidade, min/max, média — desviaram do normal?
- Lineage: quando algo quebra, quais tabelas e dashboards a jusante são afetados? (impacto) e de onde veio? (causa raiz)
9.4 Catálogo, lineage e governança
- Catálogo de dados (DataHub, OpenMetadata, Unity Catalog, Amundsen): inventário pesquisável de todas as tabelas — descrição, dono, freshness, colunas, tags de PII, popularidade. Responde "que tabela eu uso para receita e posso confiar nela?".
- Lineage em nível de coluna: "esta métrica do dashboard vem de qual coluna de qual fonte, passando por quais modelos?". Essencial para auditoria e análise de impacto.
- Governança e privacidade: classificar PII, mascarar/tokenizar dado sensível, controle de acesso por linha/coluna, políticas de retenção, e o direito ao esquecimento (LGPD/GDPR) — que num lake imutável exige desenho específico (ex.: crypto-shredding: apagar a chave de criptografia daquele titular).
9.5 FinOps de dados: o custo é um requisito
- Storage: compacte arquivos pequenos (o "small files problem" mata performance e custo), use formatos colunares, defina políticas de tiering e expiração, remova snapshots antigos do Iceberg/Delta (
VACUUM/expire_snapshots). - Compute: particione para que consultas leiam menos; materialize incremental em vez de full-refresh; desligue clusters ociosos; monitore as 10 consultas mais caras do mês.
- Alertas de custo: um bug de
JOINcartesiano pode gerar uma fatura de milhares num fim de semana — tenha limite e alerta.
9.6 DataOps: rodar isso como time
- Tudo em git + CI/CD: modelos dbt, DAGs e configs em repositório; PR com review; CI roda testes de dados e
dbt buildnum ambiente efêmero antes do merge. - Ambientes isolados: dev / staging / prod com dados e permissões separados.
- Idempotência e replay em todo lugar (Módulos 3 e 6) — é o que torna incidente recuperável.
- On-call e runbooks: alerta acionável (não ruído), com dono claro e passo a passo de diagnóstico. Postmortem sem culpa após incidente.
- SLAs/SLOs de dados publicados: "a tabela X está pronta até 7h em 99% dos dias".
Este módulo é o que distingue um sênior. Perguntas: "Um dashboard mostrou receita 30% menor hoje. Como você investiga?" (freshness da fonte → volume da bronze → mudança de esquema → teste de reconciliação → lineage para ver o que mudou), "O que é um data contract e que problema resolve?", "Como você garante qualidade num pipeline?" (testes em CI + observabilidade em prod + contratos na origem — as três camadas). Ferramentas atuais para citar: dbt tests, Great Expectations/Soda, Elementary, DataHub/OpenMetadata.
✏️ Exercício 9 — Incidente de produção
Às 8h05, o time de finanças diz que o relatório de faturamento do dia anterior está com metade do valor esperado. O pipeline rodou "com sucesso" às 3h. Descreva sua sequência de diagnóstico.
Gabarito (uma boa resposta): (1) Freshness: a fonte (banco de pedidos) tinha os dados completos às 3h, ou a carga da fonte atrasou? (2) Volume na camada bronze do dia: nº de linhas vs média — se caiu, o problema é ingestão. (3) Schema: alguma coluna de valor mudou de tipo/nome (talvez centavos↔reais)? (4) Rodar o teste de reconciliação bronze × gold para localizar em que camada o valor diverge. (5) Lineage: ver se algum modelo dbt intermediário mudou no último deploy. (6) Causa provável a investigar: janela de ingestão incremental que pegou só parte do dia por causa de fuso/marca d'água, ou um JOIN que virou inner e derrubou linhas. Comunicar ETA de correção ao time de finanças enquanto investiga.
Mercado de trabalho: roadmap, entrevistas e portfólio
Objetivo: converter o conteúdo dos módulos anteriores em aprovação — roadmap de estudo, banco de perguntas reais com respostas e projetos que geram entrevista.
10.1 Roadmap de estudo (12 semanas, para quem já sabe SQL e Python básico)
| Semanas | Foco | Entregável prático |
|---|---|---|
| 1–2 | Módulos 1–2 + SQL analítico a fundo (window functions, CTEs) + Parquet/DuckDB local | Converter um dataset CSV público em Parquet particionado e consultar com DuckDB |
| 3–4 | Módulo 3 (ingestão) + Módulo 4 (lakehouse) com Iceberg ou Delta local | Pipeline de ingestão (dlt ou Airbyte) de uma API pública para a camada bronze em Iceberg |
| 5–6 | Módulo 5 — dbt do zero: staging, marts, testes, docs, star schema | Projeto dbt com bronze→silver→gold, ≥ 10 testes, star schema documentado |
| 7–8 | Módulo 6 — Airflow: DAGs, idempotência, backfill, sensores, alertas | Orquestrar o pipeline inteiro (ingestão + dbt + validação) numa DAG agendada |
| 9 | Módulo 7 — Spark (ou aprofundar DuckDB/Polars); shuffle, skew, broadcast | Reescrever a transformação mais pesada em PySpark e comparar tempos |
| 10 | Módulo 8 — Kafka + um processador de streaming (Flink SQL ou Spark SS) | Produzir eventos sintéticos no Kafka e agregar por janela de tempo |
| 11 | Módulo 9 — testes em CI, observabilidade (Elementary), catálogo, custo | Adicionar CI com dbt build + relatório de qualidade ao projeto |
| 12 | Cloud (1 provedor) + IaC básico (Terraform) + revisão de entrevistas | Subir o projeto na nuvem com Terraform; escrever o README de portfólio |
Escolha uma nuvem para aprofundar (AWS é a mais pedida; GCP tem o BigQuery, muito comum; Azure aparece em corporações). Os conceitos transferem; o que muda são nomes de serviços.
10.2 Banco de perguntas de entrevista (com a resposta que aprova)
Júnior/pleno — "Qual a diferença entre ETL e ELT?"
Explique a ordem (transformar antes ou depois de carregar) e por que ELT venceu: armazenamento barato + engines rápidas permitem guardar o cru e transformar com SQL versionado (dbt) dentro do warehouse/lakehouse, preservando a capacidade de reprocessar quando a regra muda. Feche dizendo que ETL ainda faz sentido quando há restrição de compliance para não carregar o dado bruto (ex.: mascarar PII antes de persistir).
Pleno — "O que é idempotência e por que importa num pipeline agendado?"
Idempotente = rodar N vezes tem o mesmo efeito de rodar 1 vez. Importa porque retries, backfills e reprocessamentos são inevitáveis; sem idempotência, cada um deles duplica ou corrompe dados. Consegue-se com MERGE/upsert por chave, ou INSERT OVERWRITE da partição da data lógica do run (nunca current_date). Dê o exemplo do DELETE da partição antes do INSERT.
Pleno — "Modele um star schema para uma rede de lojas"
Primeiro declare o grão: "uma linha = um item de um pedido". Fato fct_vendas com chaves (data_id, loja_id, produto_id, cliente_id) e métricas aditivas (quantidade, valor_bruto, desconto, valor_liquido). Dimensões dim_data, dim_loja, dim_produto, dim_cliente. Mencione SCD Tipo 2 em dim_produto (preço/categoria mudam e o histórico importa) e por que o desconto fica no fato, não na dimensão.
Pleno/sênior — "Como você replicaria o banco de produção para o ambiente analítico?"
CDC: Debezium lê o WAL/binlog → Kafka → sink no lakehouse; snapshot inicial + stream contínuo; MERGE no destino tratando op = 'd' para deletes. Justifique contra o polling incremental: CDC captura updates/deletes, não pesa na fonte e tem baixa latência. Cite o antipadrão dual-write e a alternativa outbox. Mencione monitorar o replication lag.
Sênior — "O que é um shuffle no Spark e como você reduz?"
É a redistribuição de dados entre executores por chave, necessária em operações wide (groupBy, join, distinct); escreve em disco e trafega pela rede — o maior custo do job. Reduz-se filtrando e projetando cedo, usando broadcast join quando um lado é pequeno, tratando data skew (salting, AQE skew handling) e escolhendo um nº de partições de shuffle adequado ao volume. Bônus: dizer quando não usar Spark (dado cabe numa máquina → DuckDB/Polars).
Sênior — "Event time vs processing time; o que é watermark?"
Event time = quando o fato ocorreu; processing time = quando seu sistema o viu. Agregações de negócio devem usar event time para não distorcer séries temporais quando há atraso de rede. Watermark é a estimativa "não espero mais eventos anteriores a T"; quando ele ultrapassa o fim de uma janela, a janela fecha e emite; eventos posteriores são late data (descarte, side output ou update). Cite a folga configurável (trade-off latência × completude).
Sênior — "Como você garante qualidade de dados num pipeline?"
Três camadas: (1) testes em CI — unique, not_null, relationships, accepted_values, reconciliação, rodando em dbt build num ambiente efêmero antes do merge; (2) observabilidade em produção — freshness, volume, schema, distribuição e lineage com alerta acionável; (3) data contracts na origem — mudança incompatível quebra o build do time produtor, não o seu relatório. Mencione política de "circuit breaker": se um teste crítico falha, o pipeline não publica dado ruim a jusante.
Armadilha — "Vamos fazer tudo em streaming, é mais moderno"
Resposta madura: streaming é uma decisão de custo e latência, não de status. Roda 24/7, exige gestão de estado, replay e monitoramento de lag, e é mais difícil de depurar. A maioria dos "requisitos de tempo real" é atendida por micro-batch de 1–5 min a uma fração do custo operacional. Streaming verdadeiro se justifica quando esperar minutos gera perda concreta: fraude, precificação, alertas de segurança, features de recomendação ao vivo.
10.3 Projetos de portfólio que geram entrevista
- Lakehouse ponta a ponta (o projeto-âncora): ingestão de uma API pública (ou dataset como o de táxis de NYC) → bronze em Iceberg/Delta → dbt para silver/gold com star schema → Airflow orquestrando tudo → testes em CI → um dashboard simples (Metabase/Streamlit). README explicando cada escolha. Este projeto sozinho já cobre 80% de uma entrevista.
- CDC em tempo quase real: Postgres + Debezium + Kafka + consumidor que faz
MERGEnum Iceberg; painel mostrando o lag. Diferencial: demonstrar deletes propagando corretamente. - Comparativo de engines: a mesma transformação de ~50 GB em pandas, Polars, DuckDB e Spark, com tabela de tempo/memória/custo e uma conclusão honesta sobre quando cada uma vale a pena.
- Pipeline de streaming: gerador de eventos → Kafka → Flink SQL com janela por event time e watermark → sink agregado; documentar o comportamento com dados atrasados.
Em todos: o README é o que o recrutador técnico lê. Explique o porquê de cada ferramenta, os trade-offs aceitos, e mostre um diagrama da arquitetura. Código limpo com decisões mal justificadas perde para arquitetura simples bem explicada.
10.4 Certificações e leituras que pesam
- Certificações com valor real: dbt Analytics Engineering Certification; Astronomer Certified: Airflow Fundamentals; Databricks Certified Data Engineer Associate/Professional; Google Professional Data Engineer; AWS Data Engineer Associate. Escolha a que casa com a stack das vagas que você quer.
- Leituras de referência: Fundamentals of Data Engineering (Reis & Housley) — o mapa da disciplina, leitura quase obrigatória; Designing Data-Intensive Applications (Kleppmann) — os fundamentos de sistemas distribuídos que sustentam os Módulos 3, 7 e 8; The Data Warehouse Toolkit (Kimball) — a bíblia da modelagem dimensional do Módulo 5.
- Prática contínua: tudo desta apostila roda local e de graça —
duckdb,dbt-core,airflow standalone,docker composecom Kafka + Debezium, Iceberg/Delta via PySpark. Monte um repositório-laboratório e vá acumulando.
Se você levar quatro ideias, esta apostila cumpriu o papel: (1) o produto da Engenharia de Dados é confiança, e ela se constrói com testes, contratos e observabilidade, não com boa vontade; (2) idempotência é o que torna todo pipeline recuperável — projete para reprocessar; (3) a stack moderna é ELT sobre lakehouse (bronze/silver/gold), com orquestração coordenando e não processando; (4) escolha de ferramenta é sempre trade-off — batch antes de streaming, uma máquina antes de um cluster, o simples que você entende antes do sofisticado que você copia. O ferramental muda de nome a cada dois anos; esses princípios não.