Lab 66 — Consultar o lake e pagar pouco: Athena
O problema, e a empresa que o tem
A Cadência é um marketplace de 900 lojistas, com um time de dados de três pessoas. O lake que sustenta os relatórios vem dos L62 a L65: dado bruto em S3, organizado em camadas, particionado e catalogado. A pergunta mais repetida do time é sempre a mesma forma: "pedidos de ontem, por status" — receita e contagem, o primeiro número que qualquer reunião de manhã pede.
O que ninguém tinha medido era o que essa pergunta custava. A tabela de pedidos acumula dois anos de histórico — 730 dias — num único prefixo de S3 sem partição, guardado como CSV de exportação diária. Toda consulta, mesmo filtrando por um único dia, varre a tabela inteira: cerca de 400 GB abertos para devolver uma dúzia de linhas.
A fatura do mês passado veio com uma linha de Athena que ninguém sabia explicar. Ninguém rodou uma consulta "errada" — cada consulta individual parecia inofensiva, pequena, rápida de escrever. O problema não está em nenhuma consulta isolada: está em quantas vezes a mesma pergunta cara foi repetida, e em quanto byte cada repetição varreu sem que ninguém tivesse como ver isso antes de rodar.
O que este laboratório NÃO é
Não é onde o dado vira Parquet nem onde o catálogo aprende a existência de partições — isso é o L64 e o L65. Aqui o dado já chega otimizado; o que este módulo constrói é a camada de GOVERNANÇA de custo em cima dele: limite por consulta, alerta por workgroup e materialização do relatório repetido. Refazer a conversão de formato aqui duplicaria conteúdo que pertence aos dois laboratórios anteriores.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um número na seção de implantação, não com a sensação de ter otimizado.
- Explicar por que o Athena cobra por byte varrido e não por byte devolvido, e o que isso muda na forma de otimizar uma consulta.
- Medir, em byte varrido, o custo da mesma consulta contra a tabela bruta e contra a tabela otimizada, e derivar a razão entre os dois números.
- Explicar por que partition pruning, formato colunar e compressão atacam três partes diferentes da mesma fórmula de custo, e não a mesma coisa três vezes.
- Configurar um limite por consulta que corta uma consulta cara antes de ela terminar de rodar, calibrado a partir de um caso real medido.
- Distinguir o que o limite por consulta faz do que o alerta por workgroup faz, e explicar por que confundir os dois deixa a equipe se achando protegida sem estar.
- Materializar um agregado caro com CTAS e explicar por que isso reduz o custo do relatório repetido sem mudar o SQL que o analista digita.
- Diagnosticar uma consulta cancelada pelo limite de byte, e uma consulta que retorna zero linhas por erro de configuração de partição, distinguindo os dois sintomas.
- Calcular, a partir de dimensões e não de preço fixo, em qual cenário vale a pena trocar Athena por um data warehouse dedicado.
"Entender bytes varridos" não é objetivo verificável
Cada item da lista acima termina em algo que se aponta com um número ou um comando — explicar, medir, configurar, distinguir, materializar, diagnosticar, calcular. Nenhum termina em "compreender" sozinho, porque compreensão sem prova é exatamente o tipo de conteúdo que este laboratório existe para não entregar.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Modelo de cobrança do Athena | MLA-C01, SAA-C03 | byte varrido, arredondado ao MB, com piso de 10 MB por consulta | não confundir bytes varridos com tempo de execução ou linhas retornadas |
| Partition pruning vs. partition projection | MLA-C01 | poda tradicional via consulta ao Glue Catalog vs. poda calculada sem consultar o catálogo | projection ignora qualquer partição registrada; é configuração de tabela, não de consulta |
| Formato colunar e pushdown de predicado | MLA-C01, DVA-C02 | Parquet carrega só a coluna pedida e pula bloco usando estatística de mín/máx | por que CSV não permite pular coluna nem bloco, não importa o filtro |
| Controle de custo por workgroup | MLA-C01 | limite por consulta cancela; limite por workgroup só alerta | qual dos dois evita gasto acontecer e qual só avisa depois que já aconteceu |
| CTAS | MLA-C01, DVA-C02 | materializa resultado, evita reconsultar o dado bruto | CTAS é tratado como DML para cota de consultas simultâneas, não como DDL isolado |
| Problema do arquivo pequeno | MLA-C01 | overhead de listagem no S3 penaliza mais o formato colunar que o CSV | limite prático de 1.000 objetos por operação de listagem do S3 |
| Consulta cancelada ainda é cobrada | MLA-C01 | ProcessedBytes inclui o que foi varrido até o cancelamento | o limite por consulta impede o RESTO do dano, não devolve o que já rodou |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve uma tabela particionada com consulta lenta e pede a causa. A resposta errada mais comum é "falta índice" — Athena não tem índice de linha. A resposta certa é sobre o CATÁLOGO: se a tabela tem dezenas de milhares de partições e a consulta não filtra por igualdade em todas as chaves, o planejador gasta tempo listando partição, não lendo dado. Partition projection ou partition index resolvem o sintoma; adicionar mais nó de computação, não.
Requisitos, e como cada um muda o desenho
Requisito que não aparece numa linha de configuração é intenção. A coluna da direita é onde cada um deixou marca no Terraform.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Custo de consulta previsível | nenhuma consulta individual passa de um teto combinado | exige limite por consulta no workgroup, com enforce_workgroup_configuration = true |
| Nenhuma consulta legítima bloqueada | falso positivo raro, calibrado por medição | o teto vem do pior caso real (relatório histórico mais pesado) mais folga de 3x — não um número arbitrário |
| Relatório mensal recorrente | consultado várias vezes ao dia, por pessoas diferentes | exige CTAS materializado, para não repagar o mesmo scan pesado a cada leitura |
| Rastreabilidade de gasto por uso | consulta interativa separada de job automatizado na fatura | dois workgroups distintos — `analytics` e `automacao` — cada um com seu próprio teto |
| Time de 3 pessoas, sem orçamento para infra sempre ligada | zero infraestrutura provisionada | descarta Redshift e Provisioned Capacity como solução deste estágio |
| Alerta no mesmo dia, não na fatura do mês seguinte | detectar estouro em até 24 h | alarme CloudWatch sobre ProcessedBytes acumulado do workgroup, com notificação SNS |
| Base já otimizada pelo L64 e pelo L65 | Parquet, particionado por dia, catalogado | este módulo assume a peça de formato/partição/catálogo pronta — o ganho medido aqui é o que sobra depois dela |
O requisito mais fácil de esquecer é o último da tabela
Se a tabela de origem ainda não estiver em Parquet particionado e catalogado, nada neste módulo funciona como descrito — o workgroup e o CTAS otimizam o CUSTO de uma base que já é rápida de podar, eles não criam a poda. Confirme com `SHOW PARTITIONS` antes de aplicar qualquer coisa daqui.
Arquitetura mínima: a consulta sem nada no meio
- → SELECT status, soma WHERE data = ontem
- → resolve schema da tabela pedidos_cru
- → devolve 1 tabela, 0 partições registradas
- → lista e abre todo o prefixo
- → 400 GB lidos para 2 colunas úteis
- → byte varrido vira linha na fatura consolidada
- Fora da AWS
- Armazenamento
- Analytics
- Gestão e governança
Este desenho responde à pergunta, e é por isso que ninguém o questiona: a consulta volta com o status certo e o valor certo. O defeito não está na resposta — está em quanto byte foi aberto para produzi-la, e isso só aparece na fatura, semanas depois. Percorra os passos e repare que a lacuna entre byte varrido e byte útil é aritmética, não falta de sorte.
- A pergunta é sobre um dia; nada trava antes de rodar. O workgroup `primary` não tem limite de byte configurado. Qualquer consulta, do filtro mais preciso ao `SELECT *` digitado por engano, roda até o fim sem nenhum freio entre a intenção do analista e o que o motor efetivamente abre.
- O catálogo tem schema, não fronteira. O Glue Data Catalog devolve os tipos de coluna corretamente — isso não é o problema. O problema é que a tabela não tem coluna de partição, então não há nada no catálogo que diga ao planejador "ignore os outros 729 dias".
- Zero partição registrada é zero poda possível. O catálogo responde honestamente: existe uma tabela, e ela não tem partição. Sem partição, o planejador do Athena não tem nenhuma fronteira lógica para decidir "estes arquivos não interessam" antes de abrir o prefixo inteiro.
- Sem fronteira, a lista é o prefixo inteiro. O Athena lista e abre todos os objetos sob o caminho S3 da tabela. Não existe um "WHERE" que filtre arquivo antes de lê-lo quando não há partição — o filtro de data só é aplicado DEPOIS que a linha já foi lida.
- CSV não deixa pular coluna nem linha. Formato por linha: para ler o valor de uma coluna, o motor lê a linha inteira. Das 16 colunas da tabela, a consulta usa 2 — e ainda assim as 16 são lidas em cada uma das linhas de 2 anos de histórico.
- O preço só existe na fatura consolidada. Sem CloudWatch nem alarme por workgroup configurado, o único lugar em que o custo desta consulta aparece é o Cost Explorer, semanas depois, misturado com o resto da conta da AWS — tarde demais para qualquer correção no momento certo.
Arquitetura de produção: freio antes, dono depois
- → SELECT status, soma WHERE data = ontem
- → resolve schema e as 730 partições
- → devolve só a partição de ontem
- → abre só os arquivos daquela partição
- → poucos MB, já sem as colunas não pedidas
- → byte varrido soma no acumulado do dia
- → teto do workgroup ultrapassado
- → aciona 1x por dia
- → DROP + CTAS via StartQueryExecution
- → escreve a tabela nova, comprimida
- → relatório recorrente lê o resumo pronto
- → custo aparece separado, por workgroup
- Fora da AWS
- Armazenamento
- Analytics
- Gestão e governança
- Integração de apps
- Compute
A pergunta do analista não mudou uma vírgula — o SQL é o mesmo. O que muda é tudo o que existe embaixo dele: partição para podar, formato para ler só a coluna certa, um limite que corta consulta cara ANTES de terminar, um alerta que soma o gasto do dia, e uma tabela pronta para quem pergunta a mesma coisa de novo. Percorra os passos: cada peça nova aqui rastreia a um requisito da seção anterior.
- O catálogo agora sabe podar. O crawler do L65 mantém as 730 partições registradas. Quando a consulta filtra por `data_pedido`, o planejador consulta o catálogo e recebe de volta só a localização da partição de ontem — as outras 729 nunca entram no plano.
- Abrir só a partição certa. Com a partição resolvida, o Athena lista e abre apenas os arquivos daquele único prefixo de data — não o prefixo da tabela inteira. É a mesma operação de listagem do diagrama anterior, sobre uma fatia 730 vezes menor.
- Formato colunar corta o que sobrou. Parquet carrega só as colunas pedidas na projeção da consulta, e o byte cobrado é o comprimido em disco — antes da descompressão. As duas coisas juntas reduzem o que resta depois da poda de partição, não apenas o volume bruto.
- O limite por consulta é um freio, não um reembolso. Se alguém rodar uma consulta sem filtro de partição dentro deste workgroup, o motor cancela ao ultrapassar o `BytesScannedCutoffPerQuery`. O que já foi varrido até o corte continua sendo cobrado — o limite impede o resto do dano, não apaga o que já aconteceu.
- O alerta por workgroup avisa; não cancela nada. O limite por workgroup soma o consumo de TODAS as consultas do dia. Ao ultrapassar o teto, ele dispara uma notificação SNS — nenhuma consulta em andamento ou futura é bloqueada por causa dele. É aviso, não disjuntor.
- O relatório repetido para de pagar o mesmo escaneamento. Um job diário refaz a tabela de resumo dentro do MESMO workgroup governado — então até a automação respeita o limite por consulta. Depois disso, quem lê o resumo lê uma tabela de poucos megabytes, nunca mais o lake inteiro.
- O custo agora tem dono. Com o workgroup separado do padrão, o Cost Explorer mostra o gasto de consulta interativa isolado do gasto do job de atualização — a pergunta "quem gastou o quê" deixa de exigir arqueologia na fatura.
Como funciona ponta a ponta
O ciclo de vida de uma consulta no Athena tem seis fases, e o byte varrido só cresce em uma delas. Entender onde cada custo e cada controle atua evita o erro mais comum de diagnóstico: culpar o tempo de execução por um problema que é de planejamento, ou culpar o formato do dado por um problema que é de configuração de workgroup.
O SQL que o analista escreve não muda entre a tabela bruta e a otimizada — é a mesma pergunta, contra tabelas diferentes:
-- consulta.sql — o SQL da pergunta recorrente. É IDÊNTICO nas duas tabelas;
-- o que muda é só o que existe embaixo delas.
SELECT
status AS status_pedido,
SUM(valor_total_centavos) AS receita,
COUNT(*) AS pedidos
FROM pedidos -- ou pedidos_cru, na arquitetura mínima
WHERE data_pedido = DATE '2026-08-07' -- "ontem"
GROUP BY status
ORDER BY receita DESC;
-- Em pedidos_cru (sem partição), o WHERE só é aplicado DEPOIS que a linha é
-- lida — então ele filtra o RESULTADO, não o que é varrido.
-- Em pedidos (particionado por data_pedido), data_pedido é uma chave
-- de partição virtual: o planejador usa o WHERE para decidir quais arquivos
-- sequer abrir, antes de gastar 1 byte de leitura.
E o retorno de `GetQueryExecution` ao final de uma execução bem-sucedida contra a tabela otimizada — é daqui que vem cada número da tabela de derivação da próxima seção:
{
"QueryExecution": {
"QueryExecutionId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"Query": "SELECT status, SUM(valor_total_centavos) ... WHERE data_pedido = DATE '2026-08-07'",
"WorkGroup": "cadencia-analytics",
"Status": {
"State": "SUCCEEDED",
"SubmissionDateTime": "2026-08-08T09:14:02Z",
"CompletionDateTime": "2026-08-08T09:14:04Z"
},
"Statistics": {
"DataScannedInBytes": 19625000,
"EngineExecutionTimeInMillis": 812,
"QueryPlanningTimeInMillis": 96,
"QueryQueueTimeInMillis": 41,
"TotalExecutionTimeInMillis": 1043
}
}
}
A otimização mora no dado, não na consulta
Repare que `consulta.sql` é literalmente o mesmo texto para as duas tabelas. Isso não é coincidência de exemplo: o custo do Athena é decidido pela FORMA como o dado está guardado — partição, formato, compressão — não pela forma como o SQL é escrito. Reescrever a consulta sem mudar o dado por baixo raramente move a agulha.
As decisões
📋 Um time de 3 analistas na Cadência (marketplace de 900 lojistas) precisa parar de ser surpreendido pela fatura do Athena, sem orçamento para infraestrutura sempre ligada e sem poder bloquear análise ad-hoc legítima.
As três peças são serverless e só cobram pelo que rodam — nenhuma fica ligada esperando consulta, o que importa para 3 pessoas com uso esporádico. O limite por consulta ataca o pior caso (consulta sem filtro) no momento em que ele acontece, não semanas depois na fatura. O CTAS ataca o caso mais comum e mais caro em volume acumulado: a mesma pergunta de relatório, repetida várias vezes ao dia por pessoas diferentes, pagando o mesmo scan pesado de novo a cada vez. Nenhuma das duas peças exige prever carga com antecedência, que é exatamente o que o time de 3 pessoas não tem experiência para fazer.
Alt: Redshift (data warehouse dedicado) — Resolve melhor o caso de painel pesado consultado o dia inteiro, mas troca "pago por byte varrido" por "pago por hora de cluster ligado" — para uma carga pequena e esparsa como a de 3 analistas, o cluster fica ocioso a maior parte do tempo. É o desenho certo quando o painel de 40 s do Athena já não resolve, e é exatamente o L68.
Alt: Athena com capacidade reservada (Provisioned Capacity) — Remove a variabilidade de custo por byte trocando por DPU-hora reservada, mas exige prever o volume de consulta com antecedência para não pagar reserva ociosa. Faz sentido quando o padrão de uso é previsível e constante — não é o caso de um time pequeno com pico de consulta imprevisível.
Alt: Cache de aplicação na frente do painel (ElastiCache) — Resolve a repetição de uma consulta IDÊNTICA, mas não resolve a consulta ad-hoc nova que ninguém cacheou ainda — que é justamente onde mora o risco do `SELECT *` sem filtro. É complementar ao CTAS deste laboratório, não substituto: o CTAS resolve o lado do dado, o cache resolve o lado do tráfego repetido de aplicação.
Alt: Deixar sem limite e revisar a fatura mensalmente — É o estado descrito na arquitetura mínima deste módulo, e o motivo de ele existir é sempre o mesmo: ninguém tinha medido o tamanho do problema até a fatura chegar. Continua sendo o ponto de partida honesto para qualquer time que ainda não rodou a conta que este laboratório ensina a rodar.
| Decisão | Alternativa considerada | Motivo da escolha | Trade-off aceito |
|---|---|---|---|
| Limite por consulta ligado (enforce=true) | deixar o cliente configurar seu próprio limite | sem enforce, ferramenta de BI ou notebook pode ignorar o teto do workgroup | exige recalibrar o teto sempre que um novo tipo de relatório legítimo aparecer |
| Dois workgroups (interativo e automação) | um workgroup único para tudo | separa gasto humano de gasto de job, e permite tetos diferentes para cada padrão de uso | mais um recurso Terraform para manter e documentar |
| CTAS recria a tabela (DROP + CREATE) | INSERT INTO incremental | recriar é idempotente por natureza — rodar duas vezes não duplica linha | cada atualização reprocessa o período inteiro, não só o dado novo |
| Alerta por workgroup só notifica | desabilitar o workgroup automaticamente ao estourar | desabilitar pararia consultas legítimas de outros analistas no mesmo dia | exige alguém reagir ao SNS; sem reação, o teto é só um número que ninguém olha |
Construir: o workgroup que trava consulta cara
Duas peças: o workgroup interativo, com limite por consulta imposto, e um segundo workgroup dedicado só à automação — para que o job noturno nunca compita pelo mesmo teto que os analistas usam durante o dia.
# workgroup.tf — o freio que corta consulta cara ANTES de ela terminar
resource "aws_athena_workgroup" "analytics" {
name = "${var.projeto}-analytics"
description = "Consultas interativas do time de dados, sob limite de byte varrido"
configuration {
# A trava central deste laboratório. Sem isto, uma configuracao de cliente
# (notebook, ferramenta de BI, CLI com --result-configuration proprio) pode
# SOBRESCREVER as regras do workgroup — o limite existiria so no papel.
enforce_workgroup_configuration = true
publish_cloudwatch_metrics_enabled = true
result_configuration {
output_location = "s3://${var.bucket_resultados}/analytics/"
encryption_configuration {
encryption_option = "SSE_KMS"
kms_key_arn = aws_kms_key.athena.arn
}
}
# O piso e o teto de 10 MB e 7 EB sao do proprio Athena; o valor abaixo e
# o que a Cadência mediu como pior caso RAZOAVEL de consulta legitima: o
# relatorio historico mais pesado que a equipe roda, mais uma folga de
# 3x. Nao copie este numero — meça o seu.
bytes_scanned_cutoff_per_query = 1500000000 # 1,5 GB
}
# Consultas sao canceladas ao ultrapassar o limite, e o motor JA cobra pelo
# que varreu ate o corte. Isso nao e um bug do laboratorio — e o comportamento
# documentado da metrica ProcessedBytes, e o motivo de o limite ser calibrado
# com folga em vez de justo no osso.
force_destroy = false
}
# Segunda tabela de contas: separa o gasto do job de atualizacao automatica do
# gasto da consulta interativa dos analistas. Sem isto, um pico do job noturno
# e um pico de consulta humana aparecem como a mesma linha na fatura.
resource "aws_athena_workgroup" "automacao" {
name = "${var.projeto}-automacao"
configuration {
enforce_workgroup_configuration = true
publish_cloudwatch_metrics_enabled = true
result_configuration {
output_location = "s3://${var.bucket_resultados}/automacao/"
}
# O CTAS de resumo mensal varre ate 13 meses de particoes de uma vez; o
# limite aqui e maior do que o do workgroup interativo de proposito — e a
# razao fica escrita, nao decorada: e o unico consumidor deste workgroup.
bytes_scanned_cutoff_per_query = 8000000000 # 8 GB
}
}
enforce_workgroup_configuration = false é dinheiro sem freio
Com esta flag desligada (o padrão, se você não a declarar), qualquer cliente que se conecte ao workgroup pode enviar sua PRÓPRIA configuração — incluindo ausência total de limite — e o Athena obedece o cliente, não o workgroup. O teto que você acabou de configurar em Terraform vira decoração. Confirme sempre que esta linha está presente e como `true`.
Construir: o alerta que soma o dia inteiro
O alarme mede o ACUMULADO do workgroup, não uma consulta isolada — é o que cobre o caso em que cinco consultas, cada uma dentro do limite individual, somam mais do que o orçamento do dia.
# alerta.tf — o aviso que soma o dia inteiro, e nao cancela nada sozinho
resource "aws_sns_topic" "custo_athena" {
name = "${var.projeto}-athena-custo"
}
resource "aws_sns_topic_subscription" "time_de_dados" {
topic_arn = aws_sns_topic.custo_athena.arn
protocol = "email"
endpoint = var.email_time_dados
}
# Mede o ACUMULADO do workgroup no dia, nao uma consulta so. E por isso que o
# limite por consulta (workgroup.tf) e este alarme resolvem problemas
# diferentes: um corta a consulta individual mais cara; este avisa quando a
# SOMA de consultas dentro do limite individual ainda assim estoura o dia.
resource "aws_cloudwatch_metric_alarm" "byte_diario_workgroup" {
alarm_name = "${var.projeto}-athena-byte-diario"
namespace = "AWS/Athena"
metric_name = "ProcessedBytes"
statistic = "Sum"
period = 86400 # 1 dia — agregado diario, nao por consulta
evaluation_periods = 1
threshold = 5000000000 # 5 GB acumulados no dia, no workgroup interativo
comparison_operator = "GreaterThanThreshold"
treat_missing_data = "notBreaching"
dimensions = {
WorkGroup = aws_athena_workgroup.analytics.name
}
# Acao e SOMENTE notificar. Nao existe "cancelar consultas futuras" aqui —
# esse comportamento e do limite POR CONSULTA, configurado no workgroup.
alarm_actions = [aws_sns_topic.custo_athena.arn]
}
Por que o limite por consulta não substitui o alarme
Um limite de 1,5 GB por consulta não impede 50 consultas de 1 GB cada no mesmo dia — cada uma passa individualmente, e a soma ainda assim é 50 GB. O limite por consulta protege contra o PIOR CASO isolado; o alarme por workgroup protege contra o volume agregado, que é um problema diferente com uma causa diferente.
Construir: a consulta materializada, para não pagar duas vezes
O CTAS transforma o agregado mais consultado do mês numa tabela pequena, recriada uma vez por dia. Depois disso, quem lê o resumo nunca mais varre o lake inteiro para produzir o mesmo número que a pessoa ao lado já pediu uma hora antes.
-- ctas.sql — materializa o agregado caro UMA vez; os relatórios leem daqui
DROP TABLE IF EXISTS pedidos_resumo_mensal;
-- O DROP acima remove só a entrada no Glue Catalog. Os arquivos Parquet do
-- resumo anterior continuam no S3 até o job apagar o prefixo explicitamente
-- (ver automacao.tf) — senão o CTAS abaixo falha, porque a localização já
-- não está mais vazia.
CREATE TABLE pedidos_resumo_mensal
WITH (
format = 'PARQUET',
write_compression = 'ZSTD',
external_location = 's3://cadencia-lake-ouro/resumo_mensal/',
partitioned_by = ARRAY['mes']
) AS
SELECT
loja_id AS loja,
status AS status_pedido,
SUM(valor_total_centavos) AS receita,
COUNT(*) AS pedidos,
date_trunc('month', data_pedido) AS mes
FROM pedidos
-- 13 meses cobre "os últimos 12 meses fechados + o mês corrente" — é o
-- horizonte real dos relatórios da Cadência, medido com o time, não um número
-- redondo escolhido por conveniência.
WHERE data_pedido >= date_add('month', -13, current_date)
GROUP BY loja_id, status, date_trunc('month', data_pedido);
-- CTAS é tratado como DML para fins de Service Quotas do Athena — vale a
-- mesma cota de consultas simultâneas de um SELECT comum, não uma cota à parte.
# automacao.tf — a atualizacao diaria do resumo, dentro do workgroup governado
resource "aws_scheduler_schedule" "atualizar_resumo" {
name = "${var.projeto}-atualizar-resumo-mensal"
schedule_expression = "cron(0 3 * * ? *)" # 3h, fora do horario de consulta interativa
flexible_time_window {
mode = "OFF"
}
target {
arn = aws_lambda_function.atualizador_resumo.arn
role_arn = aws_iam_role.scheduler_invoca_lambda.arn
}
}
resource "aws_lambda_function" "atualizador_resumo" {
function_name = "${var.projeto}-atualizador-resumo"
runtime = "dotnet8"
handler = "AtualizadorResumo::AtualizadorResumo.Function::FunctionHandler"
role = aws_iam_role.lambda_atualizador.arn
timeout = 300 # o CTAS varre ate 13 meses de particao; 300s cobre com folga medida
filename = data.archive_file.atualizador.output_path
source_code_hash = data.archive_file.atualizador.output_base64sha256
environment {
variables = {
WORKGROUP_AUTOMACAO = aws_athena_workgroup.automacao.name
BANCO_DADOS = var.database_glue
TABELA_ORIGEM = "pedidos"
TABELA_RESUMO = "pedidos_resumo_mensal"
LOCAL_S3_RESUMO = "s3://${var.bucket_lake_ouro}/resumo_mensal/"
}
}
}
# A funcao so precisa operar DENTRO do workgroup de automacao, e so sobre a
# tabela de resumo — nao sobre o lake inteiro. GetQueryExecution e
# StopQueryExecution nao aceitam recurso especifico de consulta (sao
# operacoes por conta/regiao), e e por isso que aparecem com "*" comentado.
data "aws_iam_policy_document" "lambda_atualizador" {
statement {
effect = "Allow"
actions = [
"athena:StartQueryExecution",
"athena:GetQueryExecution",
"athena:StopQueryExecution",
]
resources = [aws_athena_workgroup.automacao.arn]
}
statement {
effect = "Allow"
actions = [
"glue:GetTable",
"glue:CreateTable",
"glue:DeleteTable",
"glue:BatchCreatePartition",
"glue:GetDatabase",
]
# Restrito a UMA tabela de destino, nao ao catalogo inteiro. A tabela de
# origem (pedidos) so precisa de GetTable, que ja esta coberto
# pelo padrao "table/*" do database — CreateTable e DeleteTable ficam
# implicitamente restritos porque o CTAS so cria a tabela de resumo.
resources = [
"arn:aws:glue:${var.regiao}:${var.conta}:catalog",
"arn:aws:glue:${var.regiao}:${var.conta}:database/${var.database_glue}",
"arn:aws:glue:${var.regiao}:${var.conta}:table/${var.database_glue}/pedidos",
"arn:aws:glue:${var.regiao}:${var.conta}:table/${var.database_glue}/pedidos_resumo_mensal",
]
}
statement {
effect = "Allow"
actions = ["s3:GetObject", "s3:ListBucket"]
resources = ["arn:aws:s3:::${var.bucket_lake_ouro}", "arn:aws:s3:::${var.bucket_lake_ouro}/*"]
}
statement {
effect = "Allow"
actions = ["s3:PutObject", "s3:DeleteObject", "s3:ListBucket"]
# O DROP TABLE do Glue nao apaga o dado no S3 — por isso a funcao precisa
# de permissao para limpar o prefixo antigo antes do CTAS recriar a tabela.
resources = [
"arn:aws:s3:::${var.bucket_lake_ouro}",
"arn:aws:s3:::${var.bucket_lake_ouro}/resumo_mensal/*",
]
}
}
// Function.cs — recria o resumo mensal, dentro do workgroup governado
using Amazon.Athena;
using Amazon.Athena.Model;
using Amazon.Lambda.Core;
using Amazon.S3;
using Amazon.S3.Model;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace AtualizadorResumo;
public class Function
{
private readonly IAmazonAthena _athena = new AmazonAthenaClient();
private readonly IAmazonS3 _s3 = new AmazonS3Client();
// Le do ambiente, nunca hardcoded — o mesmo pacote roda em qualquer conta
// sem recompilar, e o Terraform e quem decide os valores por ambiente.
private readonly string _workgroup = Environment.GetEnvironmentVariable("WORKGROUP_AUTOMACAO")!;
private readonly string _database = Environment.GetEnvironmentVariable("BANCO_DADOS")!;
private readonly string _localS3 = Environment.GetEnvironmentVariable("LOCAL_S3_RESUMO")!;
public async Task FunctionHandler(object evento, ILambdaContext contexto)
{
// Idempotencia primeiro: se o job rodar duas vezes no mesmo dia (retry
// de scheduler, reprocessamento manual), o prefixo antigo tem de sumir
// ANTES do CTAS, senao a segunda tentativa falha porque a localizacao
// ja tem objeto — ou pior, sem este passo, um CTAS mal configurado com
// INSERT INTO em vez de recriacao duplicaria os numeros do relatorio.
await EsvaziarPrefixoAsync(_localS3);
var sql = File.ReadAllText("ctas.sql");
var execucaoId = await IniciarConsultaAsync(sql);
var estadoFinal = await EsperarConclusaoAsync(execucaoId, contexto);
if (estadoFinal.State != QueryExecutionState.SUCCEEDED)
{
// O motivo do estado (StateChangeReason) e o que diferencia
// "estourou o limite de byte do workgroup de automacao" de
// "erro de sintaxe no CTAS" — ambos chegam aqui como FAILED/CANCELLED,
// e tratar os dois como a mesma coisa perde o diagnostico.
throw new InvalidOperationException(
$"CTAS terminou em {estadoFinal.State}: {estadoFinal.StateChangeReason}");
}
// Bytes varridos por ESTA execucao do resumo — logado para comparar
// com o teto de 8 GB do workgroup de automacao ao longo do tempo.
contexto.Logger.LogInformation(
$"resumo atualizado: {estadoFinal.Statistics.DataScannedInBytes:N0} bytes varridos, " +
$"{estadoFinal.Statistics.EngineExecutionTimeInMillis} ms de execucao");
}
private async Task<string> IniciarConsultaAsync(string sql)
{
var resposta = await _athena.StartQueryExecutionAsync(new StartQueryExecutionRequest
{
QueryString = sql,
WorkGroup = _workgroup,
QueryExecutionContext = new QueryExecutionContext { Database = _database },
});
return resposta.QueryExecutionId;
}
private async Task<QueryExecutionStatus> EsperarConclusaoAsync(string execucaoId, ILambdaContext contexto)
{
// Sem SDK "waiter" nativo para Athena: poll simples com backoff curto.
// 300s de timeout na funcao (automacao.tf) e o teto real deste laco.
while (true)
{
var status = (await _athena.GetQueryExecutionAsync(
new GetQueryExecutionRequest { QueryExecutionId = execucaoId })).QueryExecution;
if (status.Status.State is QueryExecutionState.SUCCEEDED
or QueryExecutionState.FAILED
or QueryExecutionState.CANCELLED)
{
return new QueryExecutionStatus(status.Status.State, status.Status.StateChangeReason, status.Statistics);
}
await Task.Delay(TimeSpan.FromSeconds(3));
}
}
private async Task EsvaziarPrefixoAsync(string localizacaoS3)
{
var (bucket, prefixo) = SepararBucketEPrefixo(localizacaoS3);
var objetos = await _s3.ListObjectsV2Async(new ListObjectsV2Request { BucketName = bucket, Prefix = prefixo });
if (objetos.S3Objects.Count == 0) return;
await _s3.DeleteObjectsAsync(new DeleteObjectsRequest
{
BucketName = bucket,
Objects = objetos.S3Objects.Select(o => new KeyVersion { Key = o.Key }).ToList(),
});
}
private static (string bucket, string prefixo) SepararBucketEPrefixo(string s3Uri)
{
var semEsquema = s3Uri.Replace("s3://", string.Empty).TrimEnd('/');
var partes = semEsquema.Split('/', 2);
return (partes[0], partes.Length > 1 ? partes[1] + "/" : string.Empty);
}
private record QueryExecutionStatus(QueryExecutionState State, string? StateChangeReason, QueryExecutionStatistics Statistics);
}
CTAS não sobrescreve — e um refresh mal feito duplica número
Uma segunda tentativa de `CREATE TABLE` sobre uma localização que já tem objeto falha. A tentação é trocar por `INSERT INTO`, que funciona sem apagar nada antes — e é exatamente isso que duplica cada linha se o job rodar duas vezes no mesmo dia (retry de scheduler, reprocessamento manual). O `Function.cs` acima esvazia o prefixo S3 antes do CTAS por este motivo: recriar do zero é idempotente por natureza; inserir incrementalmente não é, a menos que você controle deduplicação.
Implantar e provar
Cinco provas. Nenhuma delas é "rodou sem erro" — cada uma tem um número que aprova ou reprova, e o que a reprovação significa.
- Prova 1 — o custo real da tabela bruta. Rode a consulta contra `pedidos_cru` e leia `Statistics.DataScannedInBytes` do `GetQueryExecution`. Esperado: um valor na casa dos 400 GB (≈429.000.000.000 bytes), porque não há partição para podar. Um valor muito menor significa que a tabela, sem querer, já tem alguma forma de filtro de arquivo — revise a definição antes de continuar.
- Prova 2 — o custo real da tabela otimizada. Rode a MESMA consulta contra `pedidos`. Esperado: um valor na casa de poucas dezenas de MB (o payload de exemplo desta seção mostra 19.625.000 bytes ≈ 18,7 MB). Divida o valor da prova 1 pelo desta: é a razão real da sua base, não a estimativa da seção seguinte.
- Prova 3 — o limite por consulta corta, e cobra o que já rodou. Rode um `SELECT *` sem `WHERE` contra `pedidos`, dentro do workgroup `analytics`. Esperado: estado final `CANCELLED`, com `StateChangeReason` citando o corte por byte varrido — e `ProcessedBytes` maior que zero no CloudWatch, confirmando que o cancelamento não zerou o que já foi lido.
- Prova 4 — o CTAS elimina o re-scan do relatório repetido. Rode a consulta de resumo mensal (a que soma 13 meses) direto contra `pedidos`, e depois a mesma pergunta contra `pedidos_resumo_mensal`. Esperado: a segunda variante varre uma fração pequena e estável do byte da primeira — e, diferente da primeira, não cresce à medida que o lake acumula mais meses de histórico.
- Prova 5 — o alerta por workgroup avisa e não bloqueia. Rode consultas suficientes para ultrapassar o teto de 5 GB do dia (por exemplo, dez execuções da prova 3, cada uma cancelada perto de 1,5 GB). Esperado: o alarme `byte_diario_workgroup` muda para `ALARM` e o e-mail do SNS chega — e uma consulta legítima seguinte, dentro do workgroup, continua rodando normalmente.
PROJETO=cadencia; REGIAO=us-east-1; DB=lake_cadencia
# ── Prova 1: custo real da tabela bruta ───────────────────────────────────
ID1=$(aws athena start-query-execution --region "$REGIAO" \
--work-group "${PROJETO}-analytics" --query-execution-context Database="$DB" \
--query-string "SELECT status, SUM(valor_total_centavos), COUNT(*) FROM pedidos_cru \
WHERE data_pedido = DATE '2026-08-07' GROUP BY status" \
--output text --query QueryExecutionId)
aws athena get-query-execution --region "$REGIAO" --query-execution-id "$ID1" \
--query 'QueryExecution.Statistics.DataScannedInBytes'
# ── Prova 2: custo real da tabela otimizada ───────────────────────────────
ID2=$(aws athena start-query-execution --region "$REGIAO" \
--work-group "${PROJETO}-analytics" --query-execution-context Database="$DB" \
--query-string "SELECT status, SUM(valor_total_centavos), COUNT(*) FROM pedidos \
WHERE data_pedido = DATE '2026-08-07' GROUP BY status" \
--output text --query QueryExecutionId)
aws athena get-query-execution --region "$REGIAO" --query-execution-id "$ID2" \
--query 'QueryExecution.Statistics.DataScannedInBytes'
# ── Prova 3: o limite corta, e o corte ainda cobra ────────────────────────
ID3=$(aws athena start-query-execution --region "$REGIAO" \
--work-group "${PROJETO}-analytics" --query-execution-context Database="$DB" \
--query-string "SELECT * FROM pedidos" \
--output text --query QueryExecutionId)
sleep 20
aws athena get-query-execution --region "$REGIAO" --query-execution-id "$ID3" \
--query 'QueryExecution.{estado:Status.State,motivo:Status.StateChangeReason,bytes:Statistics.DataScannedInBytes}'
Rode a prova 3 sabendo o que ela custa
O `SELECT *` sem filtro da prova 3 é, de propósito, a consulta mais cara deste laboratório — o cancelamento acontece DEPOIS de já ter varrido byte até o teto de 1,5 GB, não antes. É barato o suficiente para ser seguro rodar uma vez como prova, mas não rode em loop nem agende para repetir sozinho.
Quebrar de propósito
Três falhas plantadas de propósito. Cada uma tem sintoma, onde olhar, e correção.
| Falha plantada | Sintoma | Onde olhar | Correção |
|---|---|---|---|
| Remover `enforce_workgroup_configuration` do workgroup | uma consulta de teste enviada com configuração própria de cliente ignora o teto e varre a tabela inteira sem ser cortada | a própria definição do workgroup no console ou no Terraform state | restaurar `enforce_workgroup_configuration = true` e reaplicar |
| Configurar `partition projection` com faixa de data desatualizada | consulta para o dia de hoje roda, termina em SUCCEEDED, e devolve zero linhas — sem nenhum erro | propriedades da tabela no Glue Catalog (`projection.data_pedido.range`) | atualizar a faixa para incluir `NOW`, ou trocar por partição registrada manualmente |
| Trocar `DROP + CTAS` por `INSERT INTO` no job de atualização, e deixar o scheduler reprocessar após uma falha de rede | a receita do mês aparece dobrada no resumo, sem nenhum erro no log do job | contagem de linhas em `pedidos_resumo_mensal` comparada à contagem esperada por mês | voltar para o padrão recriar-do-zero, e adicionar trava de execução única por dia |
Zero linhas não é o mesmo defeito que erro
Uma consulta com `partition projection` mal configurada não falha — ela termina bem e devolve um resultado vazio, porque, segundo a documentação oficial, valores fora da faixa projetada simplesmente não retornam linha, sem erro. É o defeito mais silencioso dos três, porque o painel continua "funcionando".
Duas consultas idênticas rodam contra a mesma tabela particionada e catalogada: uma demora 2 segundos e varre 300 MB; a outra demora 15 segundos, por causa de uma função complexa no SELECT, mas também varre 300 MB. O custo cobrado pelo Athena para as duas é:
Segurança
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Consulta acidental sem filtro de partição varre o lake inteiro numa única execução | média | financeiro, médio-alto | limite por consulta no workgroup | alarme ProcessedBytes + Cost Explorer por tag de workgroup | a consulta já é cancelada automaticamente; revisar quem a disparou |
| Muitos analistas simultâneos, cada um dentro do limite individual, somam mais que o orçamento do dia | média | financeiro, médio | limite por workgroup (soma diária) | alarme CloudWatch com notificação SNS | dividir em workgroups menores por time, revisar cadência de relatório |
| Consulta cancelada deixa upload multipart incompleto no bucket de resultados | baixa-média | custo de armazenamento residual, baixo | lifecycle policy no bucket de resultados abortando multipart incompleto | S3 Storage Lens ou relatório de custo por bucket | aplicar a lifecycle policy retroativamente; limpeza manual se necessário |
| Tabela de resumo (CTAS) com permissão ampla demais expõe dado agregado de todas as lojas a um analista de uma loja só | baixa | exposição de dado entre lojistas, alto | IAM restrito à tabela específica no workgroup de automação (ver `automacao.tf`); permissão fina por linha/coluna fica para o L69 | CloudTrail de consultas Athena por identidade | revisar a policy imediatamente; tratar isolamento completo é o escopo do L69 |
Dado agregado de todas as lojas na mão de uma loja só é vazamento, não bug
Numa plataforma multi-tenant como a Cadência, a tabela de resumo agrega TODOS os lojistas por padrão — qualquer identidade com acesso de leitura enxerga a receita de todos os concorrentes uns dos outros. Isso não é um detalhe de performance: é exposição de dado comercial sensível entre clientes da própria plataforma. Enquanto o L69 não trata permissão por linha, restrinja o acesso à tabela de resumo à equipe interna, nunca diretamente aos lojistas.
Observabilidade
As perguntas que o painel deste laboratório responde, com a métrica e o limiar de cada uma.
| Pergunta | Métrica | Limiar inicial |
|---|---|---|
| Quanto byte foi varrido hoje, por workgroup? | ProcessedBytes (Sum, dimensão WorkGroup) | informativo; base para o alarme diário de 5 GB |
| Alguma consulta foi cancelada por estourar o limite hoje? | contagem de execuções com State=CANCELLED e motivo de byte | qualquer ocorrência vale investigação — não é erro, mas é sinal de consulta sem filtro |
| O resumo mensal está atualizado? | idade do último objeto em `resumo_mensal/`, ou log de sucesso do Lambda | alertar se não houver execução bem-sucedida nas últimas 27 horas |
| O planejamento da consulta está demorando mais que o esperado? | QueryPlanningTimeInMillis | acima de poucos segundos sugere catálogo com partições demais sem projection ou index |
| Qual workgroup está gastando mais este mês? | ProcessedBytes por WorkGroup, cruzado com Cost Explorer por tag | usado para decidir se um time precisa de workgroup e teto próprios |
Use soma, não média, em ProcessedBytes
A pergunta que importa é "quanto byte o workgroup varreu no total hoje", não "qual foi o tamanho médio de consulta". Uma média esconde o outlier que estourou o teto diluída entre dezenas de consultas pequenas — e o outlier é exatamente o que o alarme desta seção precisa enxergar.
Escala
Athena, S3, Glue e CloudWatch são serviços regionais e gerenciados — nenhum deles expõe ao usuário o conceito de zona de disponibilidade que os labs de aplicação tratam, então a coluna "falha de AZ" do padrão da série não se aplica aqui sem adaptação. A fronteira de disponibilidade real é a REGIÃO, e é isso que a última coluna descreve.
| Dimensão | 10 consultas/dia | 10 mil partições | 1 milhão de arquivos pequenos | Indisponibilidade regional |
|---|---|---|---|---|
| O que muda | workgroup único basta; custo desprezível mesmo sem nenhuma otimização | busca de partição no Glue Catalog começa a pesar no `QueryPlanningTimeInMillis` | overhead de listagem no S3 domina o tempo de planejamento; formato colunar sofre mais que CSV | Athena, Glue Catalog e S3 ficam indisponíveis na região; não há failover automático entre regiões |
| Onde o risco aparece | nenhum risco novo — é o cenário do dia 1 | consultas sem predicado de igualdade em todas as chaves de partição ficam lentas | risco de atingir o limite prático de 5.500 requisições/segundo por prefixo do S3 | todo o pipeline de relatório para, incluindo o job de atualização diário |
| O que corrige | nada a fazer ainda | partition projection (evita a consulta ao catálogo) ou partition index (acelera a consulta) | compactar arquivos pequenos via CTAS periódico, revisando a estratégia de partição | estratégia de DR multi-região — fora do escopo deste módulo; ver o laboratório dedicado a isso |
Onde o DR deste cenário é tratado de verdade
Replicar o lake entre regiões, decidir RTO/RPO para um pipeline de dados e recriar o catálogo numa região secundária é decisão de arquitetura própria, com trade-off específico de custo de replicação contínua. O laboratório `lab-dr-multiregiao-quatro-estrategias` cobre as quatro estratégias de DR aplicáveis — este módulo não tenta resumir aquele conteúdo aqui.
Custo
Três cenários, pelas mesmas dimensões — nenhum valor absoluto, porque preço muda por região e por acordo comercial. Use o AWS Pricing Calculator para transformar as dimensões abaixo num número da sua conta.
| Dimensão | Protótipo (consulta manual sobre o bruto) | Produção pequena (workgroup + CTAS) | Alta escala (múltiplos workgroups, muitas partições) |
|---|---|---|---|
| Byte varrido por consulta | proporcional ao tamanho TOTAL da tabela, sempre | proporcional só à partição e às colunas pedidas | idem, mas multiplicado pelo número de times consultando em paralelo |
| Armazenamento de resultado | cresce sem limite, sem lifecycle configurada | lifecycle policy limitando retenção do bucket de resultados | lifecycle por workgroup, com retenção diferente para automação e interativo |
| Armazenamento do Glue Catalog | poucas tabelas, custo desprezível | idem, mais a tabela de resumo | cresce com o número de partições e de tabelas derivadas (CTAS de outros times) |
| Capacidade provisionada | nenhuma — sob demanda, sempre | nenhuma — ainda faz sentido sob demanda para 3 pessoas | ponto em que vale reavaliar Provisioned Capacity ou migrar para Redshift (L68) |
O custo oculto: o piso de 10 MB por consulta
Cada consulta é cobrada por, no mínimo, 10 MB varridos — mesmo que o dado real seja menor. Um painel que dispara centenas de consultas pequenas por dia (uma por widget, por exemplo) paga esse piso centenas de vezes. A correção não é reduzir o dado: é reduzir o NÚMERO de consultas, agrupando widgets numa única execução ou usando o resumo materializado deste módulo.
Well-Architected nos seis pilares
| Pilar | Situação | Risco | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | atualização do resumo depende de um job com retry sem trava de idempotência garantida por padrão | reprocessamento duplica dado sem erro visível | trava de execução única por dia, mais alerta de falha do job | alta |
| Segurança | workgroup de automação com IAM restrito a uma tabela; permissão fina por linha/coluna ainda não existe | analista de uma loja pode ler o resumo agregado de todas | Lake Formation com permissão por coluna — tema do L69 | média, para este estágio |
| Confiabilidade | sem estratégia de DR entre regiões para o lake nem para o catálogo | indisponibilidade regional para todo o pipeline de relatório | ver `lab-dr-multiregiao-quatro-estrategias` quando o requisito de disponibilidade justificar o custo | baixa, até haver requisito de RTO explícito |
| Eficiência de performance | partições de tamanho saudável, mas sem partition index para o caso de dezenas de milhares de partições | planejamento lento se o histórico crescer sem revisão da estratégia | monitorar QueryPlanningTimeInMillis e migrar para projection ou index quando necessário | média |
| Otimização de custo | limite por consulta calibrado por medição, CTAS reduzindo relatório repetido | nenhum imediato — é o objeto central deste laboratório | revisar o teto do workgroup a cada trimestre, conforme o padrão de consulta muda | já tratada |
| Sustentabilidade | byte varrido reduzido em ordens de grandeza reduz também o cômputo consumido por consulta | nenhum risco novo — é consequência direta da otimização de custo | nenhuma ação adicional necessária além do que este módulo já entrega | baixa, benefício automático |
Três pessoas não revisam seis pilares de uma vez
A prioridade da última coluna existe para ser lida em ordem: excelência operacional (idempotência do job) primeiro, porque um dado duplicado silenciosamente é pior do que uma consulta cara e visível. Confiabilidade multi-região fica para quando o requisito de disponibilidade justificar o custo — tratar os seis pilares como fila igual é como um time pequeno se afoga em trabalho que ainda não é prioridade.
Evolução em níveis
Não é um desenho novo: é a mesma decisão, sob volume e maturidade crescentes. Cada nível declara o que muda, o novo risco, e o impacto de custo.
| Nível | O que muda | Novo risco | Impacto de custo |
|---|---|---|---|
| 1. Protótipo | consulta manual do Athena sobre o S3 bruto, workgroup `primary` sem nenhum limite | qualquer analista pode rodar um `SELECT *` sem querer e gerar uma fatura de centenas de dólares numa tarde | baixo em volume de consulta, mas altíssimo por byte — cada consulta paga o teto da tabela inteira |
| 2. Aplicação básica | conversão manual para Parquet via CTAS avulso, sem particionamento formal nem automação | conversão esquecida quando chega dado novo — o dado bruto se acumula sem tratamento, e ninguém percebe | cai bastante por consulta convertida, mas continua alto para tudo que ainda não foi convertido |
| 3. Produção (este laboratório) | particionamento por dia (L64) + catálogo automatizado (L65) + workgroup com limite por consulta e alerta por workgroup + CTAS de agregado recorrente | falso senso de segurança: se ninguém reage ao alerta SNS, o limite por workgroup não corta nada sozinho — é aviso, não disjuntor | previsível e mensurável; cai de ordens de grandeza para a consulta típica, medido nas provas |
| 4. Alta escala | partition projection para tabelas com dezenas de milhares de partições (ex.: partição por dia e por loja), bucketing por chave de alta cardinalidade para lookup pontual | partição esparsa: projection projeta partição vazia sem erro, e isso pode mascarar um problema real de ingestão | cresce sublinear ao volume do lake, mas cresce com o número de consultas simultâneas — cada workgroup precisa de teto próprio |
| 5. Plataforma | múltiplos workgroups por time com orçamento e chargeback por tag, pipelines de CTAS agendados via orquestrador (Step Functions ou similar), Lake Formation para permissão fina por coluna | contenção de taxa de requisição no S3 quando muitos times batem nas mesmas partições ao mesmo tempo | exige rateio explícito — um workgroup compartilhado sem tag esconde quem gasta o quê |
| 6. Dados e IA | o mesmo lake otimizado vira fonte de treinamento: um pipeline em lote lê os DADOS via Athena/CTAS para treinar um MODELO de propensão de compra por lojista, e o placar do modelo volta como nova tabela no catálogo, enriquecendo consultas futuras com um sinal de IA | job de treino automatizado rodando num workgroup sem teto pode reescanear o lake inteiro a cada re-treino, sem que ninguém perceba porque "é um job", não uma consulta de analista | exige workgroup e orçamento PRÓPRIOS para o pipeline de treino, isolados do workgroup de consulta interativa — senão o custo de ML se esconde dentro da conta de BI |
Onde IA entra, e onde não entra
Este laboratório não usa IA, e a razão é aritmética, não hype
Prever quanto uma consulta vai custar antes de rodá-la é uma soma de partições vezes tamanho médio de arquivo — aritmética determinística, que a fórmula da seção 7 já resolve. Colocar um modelo para "estimar o custo de uma consulta" trocaria uma conta exata por uma aproximação, exatamente no lugar em que a exatidão é barata de obter. A CLAUDE.md desta escola existe para evitar essa troca.
Onde IA entra de verdade nesta cadeia é DEPOIS de o dado ficar barato de consultar, não antes: o nível 6 da evolução da seção anterior mostra o mesmo lake, já otimizado por este laboratório, servindo de fonte para um pipeline de treinamento — banda 8 desta série (ML com disciplina de engenharia, L71–L80). Um RAG ou um agente que precise consultar este lake em linguagem natural pertence à banda 9 (IA generativa, L81–L90), e herda exatamente a mesma lição: consultar um lake caro em byte varrido é caro do mesmo jeito, seja a consulta escrita por uma pessoa ou gerada por um modelo.
Anti-padrões
| Anti-padrão | Por que alguém faz isso | Sintoma em produção | Forma correta |
|---|---|---|---|
| `SELECT *` sem `WHERE` numa tabela particionada, "só para ver o que tem" | é o hábito de explorar dado novo; parece inofensivo porque o LIMIT visual do editor esconde que a consulta já varreu tudo antes de mostrar as primeiras linhas | consulta rápida de visualizar, mas cara de rodar — byte varrido não tem relação com linhas exibidas | sempre filtrar pela chave de partição, mesmo em consulta exploratória; usar `LIMIT` não substitui `WHERE` |
| Configurar CTAS sem estratégia de atualização definida | o CTAS inicial resolve o problema do dia, e ninguém pensa no dia seguinte até o número ficar visivelmente desatualizado | a tabela de resumo existe, roda rápido, e mente — ninguém percebe porque não quebra, só envelhece | todo CTAS de produção nasce com o job de atualização já definido, não como tarefa futura |
| Desligar `enforce_workgroup_configuration` "temporariamente" para debugar uma consulta | é o caminho mais rápido para testar uma configuração de cliente sem editar o workgroup | o "temporário" fica esquecido, e o limite deixa de valer para todo mundo, não só para quem debugava | debugar com um SEGUNDO workgroup de teste, nunca desligando a trava do workgroup de produção |
| Confundir "a tabela está registrada no Glue Catalog" com "a tabela está particionada" | registrar no catálogo é o passo visível; a decisão de dividir por coluna de data é invisível até alguém medir o byte varrido | consulta continua varrendo a tabela inteira mesmo depois do crawler rodar, porque nunca existiu coluna de partição | confirmar com `SHOW PARTITIONS` ou `DESCRIBE` que a tabela tem partição real antes de assumir que o Glue resolveu isso |
| Anunciar "otimizamos o Athena" sem medir bytes varridos antes e depois | a mudança de formato ou configuração é fácil de fazer e fácil de assumir que funcionou | ninguém sabe se a fatura caiu por causa da mudança ou por uma variação normal de volume de consulta | toda otimização de custo neste domínio nasce com a prova 1 e a prova 2 desta seção, comparadas |
| Copiar o valor de `bytes_scanned_cutoff_per_query` de outro workgroup sem recalcular | é mais rápido copiar um número que já existe do que medir o caso de uso próprio | ou bloqueia consulta legítima que era maior no time de origem, ou não protege nada porque era menor | calibrar o teto a partir do pior caso REAL do próprio workgroup, como no comentário de `workgroup.tf` |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Consulta some sem erro e retorna zero linhas | partition projection com faixa de data desatualizada | conferir a propriedade `projection.data_pedido.range` da tabela | Glue Catalog, propriedades da tabela | atualizar a faixa para incluir `NOW`, ou usar partição registrada manualmente |
| Consulta é cancelada sem aviso claro para o analista | ultrapassou o `BytesScannedCutoffPerQuery` do workgroup | ler `StateChangeReason` em `GetQueryExecution` | console do Athena, histórico de consultas | revisar se a consulta esqueceu o filtro de partição, ou recalibrar o teto se o caso for legítimo |
| Custo do workgroup sobe sem nenhuma consulta individual grande | soma de muitas consultas pequenas simultâneas | agregar `ProcessedBytes` por período no CloudWatch | CloudWatch, métrica ProcessedBytes, dimensão WorkGroup | dividir por workgroups menores por time, ou orçamento por hora do dia |
| Números do resumo mensal (CTAS) batem diferente do esperado | job de atualização rodou duas vezes ou falhou parcialmente no meio | comparar contagem de linhas com o esperado por mês, revisar log do Lambda | CloudWatch Logs da função de atualização | tornar o refresh idempotente — recriar do zero, nunca inserir incrementalmente sem controle de deduplicação |
| Consulta de análise histórica legítima é cancelada toda vez que roda | teto por consulta configurado abaixo do que o caso de uso real precisa | comparar o byte necessário do caso real (via `EXPLAIN`) com o teto configurado | definição do workgroup e o comentário que justifica o número atual | criar um segundo workgroup com teto maior para análise histórica, mantendo o do dia a dia restrito |
Comece pela causa mais simples, não pela mais interessante
Das cinco linhas acima, três nascem do mesmo hábito: alguém escreveu uma consulta sem filtro de partição. Antes de suspeitar de bug em Terraform, IAM ou no job de atualização, confira se a consulta que falhou tem `WHERE` na coluna de partição — é a causa mais comum, e a mais rápida de descartar.
Limpeza
PROJETO=cadencia; REGIAO=us-east-1
# 1. Remove o schedule antes da funcao, para nao disparar durante a limpeza
aws scheduler delete-schedule --name "${PROJETO}-atualizar-resumo-mensal" --region "$REGIAO"
# 2. Terraform destroy cuida do resto dos recursos GERENCIADOS por ele
terraform destroy -auto-approve
# 3. O DROP TABLE remove só a entrada no catalogo — os arquivos continuam no S3
aws glue delete-table --database-name lake_cadencia --name pedidos_resumo_mensal --region "$REGIAO"
aws s3 rm "s3://cadencia-lake-ouro/resumo_mensal/" --recursive
# 4. Resultados de consulta acumulados NAO sao apagados pelo destroy do workgroup
aws s3 rm "s3://cadencia-resultados/analytics/" --recursive
aws s3 rm "s3://cadencia-resultados/automacao/" --recursive
| O que foi criado | O `destroy` leva? | Continua cobrando depois? |
|---|---|---|
| Workgroups `analytics` e `automacao` | sim, via Terraform | não — sem custo fixo de existir |
| Alarme CloudWatch e tópico SNS | sim, via Terraform | não |
| Schedule do EventBridge e a função Lambda | sim, via Terraform | não |
| Arquivos de resultado de consulta em S3 | NÃO — bucket de resultados não é apagado pelo destroy do workgroup | sim — armazenamento até ser removido manualmente |
| Tabela `pedidos_resumo_mensal` no Glue Catalog | NÃO — criada por CTAS, não por Terraform | entrada de catálogo não cobra, mas o Parquet no S3 sim |
| Multipart uploads incompletos de consultas canceladas | não | sim, até a lifecycle policy ou remoção manual agir |
| Log group do Lambda no CloudWatch Logs | sim, se declarado no Terraform com o recurso explícito | sim, até a retenção expirar, se não for removido antes |
CTAS não é recurso Terraform — e isso muda a limpeza
A tabela de resumo e os arquivos que ela gera nascem de uma consulta SQL, não de um `aws_glue_catalog_table`. O Terraform nunca teve conhecimento dela, então `terraform destroy` simplesmente não a vê. A remoção tem de ser explícita, como no passo 3 do script acima — esquecer isso é a forma mais comum de o laboratório continuar cobrando armazenamento depois de "limpo".
Resumo
| Problema | Peça | Motivo |
|---|---|---|
| Custo de consulta imprevisível | workgroup com limite por consulta, enforce ligado | corta o pior caso antes de ele terminar de rodar |
| Soma de consultas pequenas estoura o orçamento do dia | alarme CloudWatch sobre ProcessedBytes do workgroup | ataca o agregado, que o limite por consulta não vê |
| Relatório caro repetido várias vezes ao dia | CTAS materializando o agregado | paga o scan pesado uma vez, não uma vez por leitura |
| Gasto de job de automação misturado com gasto de analista | segundo workgroup dedicado à automação | separa a linha na fatura e permite tetos diferentes |
| Falha possível | O que protege |
|---|---|
| Consulta sem filtro de partição | limite por consulta do workgroup |
| Volume agregado de consultas pequenas | alarme por workgroup |
| Job de atualização rodando duas vezes | padrão recriar-do-zero no CTAS |
| Configuração de cliente sobrescrevendo o teto | enforce_workgroup_configuration = true |
- O analista escreve a mesma consulta SQL de sempre.
- O workgroup resolve o schema e as partições no Glue Catalog.
- O catálogo devolve só a partição do dia pedido, não as outras 729.
- O Athena abre só os arquivos daquela partição.
- O formato colunar carrega só as colunas pedidas, já comprimidas.
- O byte varrido soma no acumulado do dia do workgroup.
- Se uma consulta individual estourar o teto, ela é cancelada — e o que já rodou é cobrado.
- Se o acumulado do dia estourar, o SNS avisa o time — sem bloquear nada.
- Um job diário recria o resumo mais consultado, dentro do mesmo workgroup governado.
- O relatório recorrente lê o resumo pronto, e nunca mais toca o lake inteiro.
A frase que resume o módulo inteiro
O Athena cobra por byte varrido — então toda otimização de custo aqui é, na prática, uma otimização de QUANTO BYTE ESTÁ ACESSÍVEL antes mesmo de a consulta rodar. Partição decide quais dias; formato colunar decide quais colunas; workgroup decide o que fazer quando alguém erra os dois primeiros.
Perguntas frequentes
❓ O Athena cobra pelo tempo que a consulta demora para rodar?
❓ Particionar a tabela já resolve o custo do Athena sozinho?
❓ Qual a diferença entre o limite por consulta e o limite por workgroup no Athena?
❓ Uma consulta cancelada por estourar o limite de byte ainda é cobrada?
❓ Preciso do Glue Data Catalog para o Athena funcionar?
❓ CTAS substitui a necessidade de particionar e converter para Parquet?
❓ Por que não usar Redshift em vez de configurar tudo isso no Athena?
❓ Partition projection e partition pruning são a mesma coisa?
Fixando
Um workgroup tem um limite por consulta (`BytesScannedCutoffPerQuery`) de 2 GB e um alarme de workgroup configurado para 10 GB acumulados por dia. Ao longo do dia, 20 analistas diferentes rodam, cada um, uma consulta de 1,8 GB. O que acontece?
Uma tabela tem partition projection configurada para a coluna `data_pedido`, com a faixa definida como `2024-01-01` até `2024-12-31`. Uma consulta filtra `WHERE data_pedido = DATE 2026-08-07`, um dia real, com dado de fato presente no S3. O que a consulta faz?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | lake em camadas no S3 (L62), dado em Parquet particionado (L64), Glue Catalog com crawler (L65) |
| Conhecimentos adquiridos | o modelo de cobrança por byte varrido; por que partição, formato colunar e compressão atacam três fatores diferentes da mesma fórmula de custo; a diferença entre limite por consulta (corta) e limite por workgroup (avisa); CTAS como ferramenta de materializar agregado caro |
| Limitação que fica | a permissão ainda é por tabela inteira, não por linha ou coluna — um analista com acesso à tabela de resumo vê o agregado de todas as lojas, não só da sua |
| Próximo exemplo recomendado | L68 — Redshift quando o painel de segundos do Athena não resolve mais. Reutiliza o lake otimizado deste módulo como fonte, e é onde o modelo de cobrança troca de byte varrido para hora de cluster |
| Também habilitado por este módulo | a banda 8 (ML com disciplina de engenharia, L71–L80) e a banda 9 (IA generativa, L81–L90) herdam a mesma disciplina de custo ao consultar este lake — treinar ou fazer RAG sobre dado caro de escanear é caro do mesmo jeito |
| Data da última validação técnica | 8 de agosto de 2026 |
Documentação oficial consultada: Amazon Athena pricing — o modelo de cobrança por byte varrido, o arredondamento ao megabyte e o piso de 10 MB por consulta; Configure per-query and per-workgroup data usage controls — a distinção entre o limite que cancela e o limite que só notifica, incluindo o intervalo de 10 MB a 7 EB do limite por consulta; Use columnar storage formats e Optimize data — os mecanismos de compressão por coluna, pushdown de predicado e o problema do arquivo pequeno; Create a table from query results (CTAS) — CTAS como DML para fins de cota de serviço; Use partition projection with Amazon Athena — a distinção entre partition pruning e partition projection, e o comportamento de zero linhas sem erro fora da faixa configurada; Monitor Athena query metrics with CloudWatch — a métrica `ProcessedBytes` e a confirmação oficial de que consulta cancelada por limite de dados ainda reporta o byte varrido até o corte; e a referência de API de QueryExecutionStatistics, para o campo `DataScannedInBytes` usado nas provas. Os valores de preço não aparecem neste módulo por decisão: use o AWS Pricing Calculator, porque preço varia por região e muda mais rápido que o conteúdo.
O que não foi verificado, e você deve conferir na sua base
Os números do estudo de caso — 400 GB brutos, fator de compressão de 4× do CSV para Parquet, e a razão final de ~21.600× entre a consulta bruta e a otimizada — são uma derivação ILUSTRATIVA a partir do enunciado do L64 no catálogo desta série, não uma medição de uma base real. O fator de compressão em especial depende inteiramente da cardinalidade e do tipo dos seus dados: meça o seu com as provas 1 e 2 desta seção, comparando `DataScannedInBytes` real antes e depois — o número vai ser diferente do exemplo, e o que importa é que a MECÂNICA de por que ele cai é a mesma.
Schema alinhado ao L64/L65 real, com uma diferença deliberada
Este módulo foi reconciliado com o `pedidos` real do L64 (`lab-parquet-particao-arquivo-pequeno.json`): mesma tabela, mesmas colunas `pedido_id, loja_id, status, valor_total_centavos, quantidade_itens`, mesma origem `pedidos_cru`. Uma diferença fica de propósito: o L64 particiona por três colunas físicas (`ano/mes/dia`); aqui a partition projection usa uma coluna lógica única, `data_pedido`, mapeada por um template de localização no Glue Catalog — um padrão igualmente real do Athena, mais simples para ensinar o MECANISMO de projection sem amarrar a lição aos detalhes do esquema de três colunas. Quem for consultar o `pedidos` do L64 usando este laboratório como referência precisa trocar `WHERE data_pedido = ...` por `WHERE ano = ... AND mes = ... AND dia = ...` — a mecânica de custo por byte varrido é idêntica nos dois esquemas.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…