A comunidade de engenharia de dados gastou anos debatendo ETL vs ELT, batch vs streaming, e Airflow vs todo o resto. A maioria desses debates produz talks de conferência e blog posts que ficam ótimos no papel mas colapsam sob o peso da realidade de produção.
Depois de construir e manter pipelines que processam milhões de registros diariamente em múltiplos ambientes de clientes, estabelecemos padrões que sobrevivem ao contato com dados reais. Este artigo documenta o que funciona, o que falha silenciosamente, e o que todo time aprende da maneira difícil.
ELT Sobre ETL: O Argumento Estrutural
O padrão tradicional ETL (Extract, Transform, Load) transforma dados antes de chegarem ao warehouse. O padrão moderno ELT (Extract, Load, Transform) carrega dados brutos primeiro, depois transforma dentro do warehouse.
Optamos por ELT por uma razão estrutural: preservação de dados brutos.
Quando você transforma antes de carregar, perde o dado original. Se sua lógica de transformação tem um bug — e terá — você precisa re-extrair do sistema fonte. Dependendo da fonte (APIs com rate limits, bancos legado sem change tracking, plataformas SaaS de terceiros), re-extração varia de “irritante” a “impossível.”
Com ELT, o dado bruto fica em uma camada de staging. Quando a lógica de transformação muda, você reprocessa do staging. Sem necessidade de re-extração. O sistema fonte nunca é tocado duas vezes.
A Exceção Prática
ELT quebra quando o volume de dados brutos é genuinamente enorme e o modelo de custo do warehouse torna armazenar dados brutos proibitivo. Se você está ingerindo 500GB/dia de telemetria IoT bruta e precisa apenas de 5GB de output agregado, transformar antes de carregar faz sentido econômico.
O framework de decisão:
| Fator | Favorece ELT | Favorece ETL |
|---|---|---|
| Custo de re-extração da fonte | Alto | Baixo |
| Volume bruto vs output | Similar | 10x+ diferença |
| Estabilidade da lógica de transformação | Evoluindo | Estável |
| Custo de armazenamento warehouse | Baixo (ClickHouse, S3) | Alto (BigQuery on-demand) |
| Requisitos de auditoria regulatória | Sim | Não |
Orquestração: Mage.ai em Produção
Usamos Mage.ai como nosso orquestrador primário para pipelines de produção client-facing. A decisão foi deliberada e veio após avaliar Airflow, Prefect e Dagster em múltiplas dimensões.
Por Que Não Airflow
Apache Airflow é o padrão da indústria, e com razão. É battle-tested, tem suporte massivo de comunidade, e todo engenheiro de dados o conhece. Mas Airflow tem três custos operacionais que se acumulam em ambientes de consultoria:
- Overhead de infraestrutura: Airflow requer banco de metadados, web server, scheduler e workers. Para um deploy de cliente único, são 4+ containers antes de rodar um único pipeline.
- Debug de DAG: Quando uma task falha, debug requer navegar logs em múltiplos componentes. O feedback loop entre “mudança no código” e “ver resultado” é medido em minutos, não segundos.
- Somente Python: Toda transformação precisa ser Python. Para transformações SQL-heavy (que a maioria dos pipelines analíticos são), você empacota SQL em strings Python — um desastre ergonômico.
O Que Mage.ai Acerta
Mage.ai endereça esses pontos de fricção:
- Blocks como unidades first-class: Cada passo do pipeline (data loader, transformer, exporter) é um bloco isolado com seu próprio contexto de execução. Blocos SQL executam SQL nativamente. Blocos Python executam Python. Sem wrapping.
- Desenvolvimento interativo: Blocos podem ser executados individualmente durante desenvolvimento com preview imediato do output. O feedback loop cai de minutos para segundos.
- Footprint menor: Uma única instância Mage serve como UI, scheduler e executor. Para deploys de clientes onde orçamento de infraestrutura é restrito, isso importa.
Padrão de Estrutura de Pipeline
Todo pipeline de produção segue uma arquitetura consistente de blocos:
Pipeline: client_sales_ingestion
├── [Loader] api_extract_sales # Extração API paginada
├── [Transformer] validate_schema # Enforcement de schema + null handling
├── [Transformer] enrich_dimensions # Join de dados de referência (regiões, categorias)
├── [Transformer] compute_metrics # Cálculos de negócio
├── [Exporter] load_clickhouse # Batch insert no store analítico
└── [Exporter] notify_status # Slack/webhook em sucesso ou falha
A disciplina chave: loaders nunca transformam, transformers nunca carregam. Cada bloco tem exatamente uma responsabilidade. Quando um pipeline falha em enrich_dimensions, você sabe que a extração sucedeu e o problema está na lógica de enriquecimento. Sem adivinhação.
O Problema da Falha Silenciosa
As falhas de pipeline mais perigosas são as que não levantam exceções. O pipeline completa com sucesso, reporta status verde, mas os dados de output estão errados.
Falhas Silenciosas Comuns
1. Schema drift sem detecção
A API fonte adiciona um novo campo ou muda um tipo de campo. Seu loader sucede porque está lendo JSON. Seu transformer sucede porque só acessa campos conhecidos. Seu exporter sucede porque ClickHouse aceita o insert. Mas o novo campo — que contém dados de negócio críticos — é silenciosamente descartado.
Correção: Validação explícita de schema no boundary do loader:
from pydantic import BaseModel, ConfigDict
class SalesRecord(BaseModel):
model_config = ConfigDict(extra='forbid') # Rejeitar campos desconhecidos
transaction_id: str
amount: Decimal
currency: str
customer_id: int
created_at: datetime
def validate_batch(raw_records: list[dict]) -> list[SalesRecord]:
valid, rejected = [], []
for record in raw_records:
try:
valid.append(SalesRecord(**record))
except ValidationError as e:
rejected.append({'record': record, 'error': str(e)})
if rejected:
log_rejected_records(rejected) # Não perca visibilidade
if len(rejected) / len(raw_records) > 0.05:
raise SchemaViolationThreshold(
f"{len(rejected)}/{len(raw_records)} registros falharam validação"
)
return valid
A configuração extra='forbid' é crítica. Força o pipeline a falhar ruidosamente quando o schema fonte muda, ao invés de silenciosamente ignorar novos campos.
2. Registros duplicados por falhas de idempotência
Paginação de API com offsets baseados em cursor pode produzir duplicatas quando registros são inseridos na fonte durante extração. Seu pipeline roda diariamente, extrai páginas 1-100, mas a página 50 desloca porque 200 novos registros foram adicionados no meio da extração.
Correção: Deduplicação no exporter com chaves determinísticas:
-- ClickHouse ReplacingMergeTree lida com isso nativamente
CREATE TABLE analytics.sales_events
(
transaction_id String,
amount Decimal(18, 2),
-- ... outros campos
_loaded_at DateTime DEFAULT now()
)
ENGINE = ReplacingMergeTree(_loaded_at)
ORDER BY transaction_id;
ReplacingMergeTree mantém apenas a versão mais recente de cada linha (por transaction_id). Duplicatas são automaticamente colapsadas durante merges de background.
3. Incompatibilidades de timezone
A API fonte retorna timestamps em UTC. Sua transformação assume hora local. Revenue atribuído a “segunda-feira” é na verdade domingo à noite. Seus relatórios semanais estão consistentemente errados por 8-16 horas dependendo do timezone do cliente.
Correção: Todos os timestamps internos são UTC. Conversão de timezone acontece exclusivamente na camada de apresentação (Superset/Flutter). Nunca no pipeline.
Padrões de Retry e Dead Letter Queues
Pipelines de produção falham. Redes dão timeout. APIs fazem rate-limit. Bancos travam. A questão não é se falhas acontecem — é como o sistema recupera.
Exponential Backoff com Jitter
Para falhas transientes (HTTP 429, connection timeouts), retentamos com exponential backoff mais jitter aleatório:
import random
import time
def retry_with_backoff(func, max_retries=5, base_delay=1.0):
for attempt in range(max_retries):
try:
return func()
except TransientError:
if attempt == max_retries - 1:
raise
delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
O jitter previne problemas de thundering herd quando múltiplos pipelines retentam contra a mesma API simultaneamente.
Padrão Dead Letter
Para registros que falham validação ou transformação, nunca os descartamos silenciosamente. Registros falhados são roteados para uma tabela dead letter:
CREATE TABLE staging.dead_letters
(
pipeline_name String,
block_name String,
record String, -- JSON serializado do registro original
error_message String,
failed_at DateTime DEFAULT now(),
resolved Boolean DEFAULT false
)
ENGINE = MergeTree()
ORDER BY (pipeline_name, failed_at);
Uma query de monitoramento diária expõe dead letters não resolvidas. Engenheiros revisam, corrigem a causa raiz e reprocessam. Nenhum dado é perdido.
Monitoramento: As Três Métricas Que Importam
Monitoramento de pipelines tipicamente gera ruído. Centenas de métricas, dashboards com 30 painéis, alertas disparando a cada hora. Reduzimos monitoramento operacional a três métricas core:
1. Freshness
Quão antigo é o registro mais recente na tabela destino? Se o pipeline roda a cada hora e o registro mais novo tem 3 horas, algo está errado.
SELECT
dateDiff('minute', max(created_at), now()) AS freshness_minutes
FROM analytics.sales_events
WHERE tenant_id = {tenant_id}
Threshold de alerta: 2x o intervalo esperado do pipeline.
2. Anomalia de Volume
O pipeline de hoje processou um número razoável de registros comparado à média móvel?
SELECT
today_count,
avg_7d,
abs(today_count - avg_7d) / avg_7d AS deviation_pct
FROM (
SELECT
countIf(event_date = today()) AS today_count,
countIf(event_date >= today() - 7) / 7 AS avg_7d
FROM analytics.sales_events
)
Se volume cai >50% ou dispara >200%, investigue. Ou a fonte mudou ou o pipeline está filtrando incorretamente.
3. Taxa de Dead Letter
Que porcentagem dos registros entrantes falhou processamento?
Se a taxa de dead letter excede 5%, o pipeline está quebrado. Entre 1-5%, há um problema de qualidade de dados na fonte. Abaixo de 1% é ruído operacional normal.
A Realidade Operacional
Engenharia de pipeline é 20% construção e 80% manutenção. O pipeline inicial leva um dia para escrever. O monitoramento, lógica de retry, validação de schema, tratamento de dead letter e gestão de timezone levam uma semana. Mas essa semana de engenharia defensiva previne meses de investigações “por que os números não batem?” que consomem tempo de analistas, erodem confiança de stakeholders, e eventualmente levam ao pipeline sendo substituído por uploads manuais de Excel.
Construa a infraestrutura chata primeiro. O trabalho empolgante de data science se torna possível somente quando o encanamento é confiável.