Lab 68 — Redshift quando o BI dói
O problema, e a empresa que o tem
A Cadência — o mesmo marketplace de 900 lojistas do L66 — resolveu o problema do Athena caro: o workgroup com limite corta consulta sem filtro, e o resumo mensal materializado por CTAS deixa o relatório recorrente barato. O time de dados achou que o capítulo de custo estava fechado. Foi quando o time executivo pediu um painel novo: vendas consolidadas por loja e categoria, com o número de HOJE — não o resumo mensal de ontem à noite —, atualizado a cada clique de filtro.
O painel tem oito widgets: receita do dia, receita por categoria, top 10 lojas, tendência dos últimos 30 dias, e mais quatro recortes que o time de produto pediu "já que estamos construindo". Cada widget é uma consulta separada contra `pedidos_parquet` — a mesma tabela Parquet particionada por dia que o L64 e o L65 prepararam, e que o L66 aprendeu a consultar barato. Barato não é o problema aqui: o problema é o tempo até o número aparecer.
Com um único analista abrindo o painel, os oito widgets levam entre 4 e 40 segundos para renderizar — o mais lento é o que junta pedidos_parquet com a tabela de dimensão de lojas para calcular o top 10, porque cada consulta do Athena começa do zero: sem estado, sem dado quente, sem plano reaproveitado do clique anterior. Com cinco executivos abrindo o painel ao mesmo tempo — segunda de manhã, reunião de diretoria — os 40 segundos viram mais de um minuto, porque as consultas competem pelo mesmo workgroup e pela mesma capacidade elástica do Athena.
O que este laboratório NÃO é
Não é onde o dado vira Parquet nem onde o Athena aprendeu a cobrar pouco — isso é o L64, o L65 e o L66. Aqui o dado já chega barato de consultar via Athena; o que este módulo resolve é uma dimensão diferente do problema: LATÊNCIA sob CONCORRÊNCIA, que é exatamente o que um motor sem estado, por melhor que seja o formato do dado, não foi desenhado para entregar.
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 que "ficou mais rápido".
- Explicar por que um motor de consulta sem estado sobre S3 (Athena) e um MPP com dado quente e distribuído (Redshift) resolvem problemas de latência diferentes, mesmo lendo o mesmo Parquet.
- Escolher uma chave de distribuição (DISTKEY) a partir do padrão de JOIN real do painel, e justificar por escrito por que ela reduz redistribuição de dado entre nós.
- Escolher uma chave de ordenação (SORTKEY) a partir do filtro mais comum do painel, e explicar como ela reduz bloco lido via zone map.
- Medir, em milissegundos e em p95, a latência da mesma consulta contra a tabela local distribuída e contra a leitura via Spectrum, e derivar quando cada caminho é o certo.
- Configurar uma materialized view com autorrefresh que substitui o join repetido contra o lake por leitura de dado pré-agregado.
- Configurar Concurrency Scaling e uma fila WLM dedicada, e explicar por que cada um resolve um tipo diferente de pico de uso.
- Diagnosticar uma consulta lenta por redistribuição de dado, e distinguir esse sintoma de uma consulta lenta por fila cheia.
- Calcular, a partir de dimensões de uso e não de preço fixo, em que ponto o custo do Redshift Serverless supera o do Athena — e o inverso.
"Entender como o Redshift é rápido" não é objetivo verificável
Cada item termina em algo que se aponta com um número, uma configuração ou um comando — escolher, medir, configurar, diagnosticar, calcular. "Entender MPP" não é objetivo verificável; é o tipo de frase que este laboratório existe para não deixar sozinha.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Estilos de distribuição (DISTSTYLE) | MLA-C01, MLS-C01, SAA-C03 | KEY em pedidos_loja (por store_id), ALL na dimensão pequena de lojas, EVEN descartado por não co-localizar join | KEY co-localiza join na mesma fatia; ALL replica tabela pequena inteira em cada nó; EVEN espalha sem critério — é o padrão de escolha errada por padrão |
| Chaves de ordenação (SORTKEY) | MLA-C01, MLS-C01 | SORTKEY composta em (order_date, store_id) para o filtro mais comum do painel | composta favorece filtro em prefixo da chave; interleaved favorece filtro em qualquer coluna isolada, ao custo de VACUUM mais caro |
| MPP e redistribuição de dado | MLA-C01, SAA-C03 | EXPLAIN mostrando DS_DIST_NONE (sem redistribuição) vs DS_BCAST_INNER (broadcast) antes e depois do DISTKEY | redistribuição é o custo invisível que uma DISTKEY errada empurra para toda consulta que faz JOIN |
| Redshift Spectrum | MLA-C01, SAA-C03 | schema externo lendo pedidos_parquet direto do S3, sem copiar para o cluster | Spectrum paga por byte varrido no S3 (como o Athena) e não usa DISTKEY/SORTKEY do cluster — o dado externo não está distribuído nem ordenado pelo Redshift |
| Concurrency Scaling vs. escala manual | MLA-C01 | clusters de burst transientes acionados quando a fila do workgroup passa do limiar configurado | cobra por segundo de uso do cluster de burst, cai automaticamente quando a fila esvazia — não é a mesma coisa que aumentar a base RPU |
| Materialized view e autorrefresh | MLA-C01, MLS-C01 | mv_vendas_dia pré-agrega loja/categoria/dia, atualizada automaticamente pelo serviço, sem job próprio | autorrefresh do Redshift dispara sozinho quando o dado-fonte muda; diferente do CTAS do Athena, não precisa de scheduler externo |
| Multi-AZ do Redshift Serverless | SAA-C03, MLA-C01 | workgroup de produção com Multi-AZ habilitado; failover automático de computação para a segunda zona | Multi-AZ aqui protege a DISPONIBILIDADE do motor de consulta, não o dado — o dado já está durável no S3 e no armazenamento do Redshift independente disso |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve um painel de BI lento com JOIN entre fato e dimensão, e pede a causa. A resposta errada mais comum é "faltam mais nós" — aumentar o cluster sem mudar a distribuição só espalha o mesmo problema por mais máquinas. A resposta certa é olhar o `EXPLAIN`: se aparece `DS_BCAST_INNER` ou `DS_DIST_BOTH`, o motor está copiando ou redistribuindo dado entre nós a cada consulta, e isso se resolve com a chave de distribuição — não com mais capacidade.
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 ou no SQL de DDL.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Painel abaixo de 3 segundos por widget | p95 medido, não estimado | exige dado pré-agregado (materialized view) em vez de JOIN recalculado a cada clique |
| JOIN fato × dimensão sem redistribuir dado | cada consulta junta pedidos com a tabela de lojas | DISTKEY do fato em store_id, DISTSTYLE ALL na dimensão pequena de lojas |
| Filtro mais comum é por dia recente | 90% das consultas do painel filtram por order_date | SORTKEY composta iniciando em order_date, para o zone map pular bloco fora do intervalo |
| Dezenas de executivos simultâneos, pico de segunda de manhã | sem fila travando consulta rápida atrás de consulta cara | fila WLM dedicada ao workgroup do painel, mais Concurrency Scaling para absorver o pico |
| Drill-down além do horizonte local | analista às vezes precisa de histórico completo de 2 anos | schema externo via Spectrum sobre o mesmo pedidos_parquet do L66, sem duplicar 2 anos de dado no cluster |
| Motor de consulta sempre disponível em horário comercial | indisponibilidade do painel é visível para a diretoria | workgroup de produção com Multi-AZ — falha de computação numa zona não derruba o painel |
| Credencial do painel nunca fica hardcoded | rotação sem reimplantar a aplicação | Secrets Manager guarda o usuário de serviço; o backend do painel lê em runtime |
| Base já governada pelo L66 | workgroup Athena, limite por consulta e CTAS mensal continuam existindo | este módulo assume a governança de custo do Athena pronta — o Redshift não substitui o L66, atende um USO diferente do mesmo lake |
Nem todo dado merece virar tabela local
O requisito mais fácil de subestimar é o quinto da tabela: drill-down além do horizonte local. Se você carregar os 2 anos inteiros de `pedidos_parquet` como tabela local no Redshift, o custo de armazenamento e o tempo de carga sobem sem necessidade — a maioria das consultas do painel olha os últimos 90 dias. O Spectrum existe exatamente para o caso raro de análise histórica completa, sem pagar o preço dele no caso comum.
Arquitetura mínima: Redshift substituindo o Athena, sem tuning
- → abre o painel às 9h
- → SELECT loja, categoria, SUM(receita) — um SELECT por widget
- → resolve schema da tabela externa pedidos_parquet
- → devolve a localização das partições no S3
- → Spectrum lê o Parquet direto do S3, sem copiar para o cluster
- → linhas fora do horizonte local retornam ao nó líder
- → cada widget espera o anterior liberar espaço na fila padrão
- → painel renderiza depois de ~12 s, mais devagar a cada executivo simultâneo
- Fora da AWS
- Analytics
- Armazenamento
Este desenho já troca o motor — o painel sai do Athena sem estado para um MPP com nós dedicados, e isso sozinho já ajuda. O defeito não está em usar Redshift: está em usar Redshift exatamente como se usava o Athena, sem escolher chave de distribuição, sem fila dedicada e sem dado pré-agregado. Percorra os passos e note onde cada consulta ainda paga o preço de redescobrir tudo do zero.
- O painel dispara uma consulta por widget, sem fila. Cada um dos oito widgets é uma execução SQL independente contra o mesmo workgroup padrão. Não existe prioridade entre eles: o gráfico de receita do dia e o top 10 de lojas competem pela mesma capacidade base, na ordem em que chegaram.
- Sem chave de distribuição escolhida, o otimizador decide sozinho. DISTSTYLE AUTO deixa o Redshift observar o padrão de uso e reclassificar a distribuição com o tempo — mas até essa reclassificação acontecer (e ela não é instantânea), o JOIN entre pedidos e a tabela de lojas paga redistribuição de dado entre nós a cada execução.
- O catálogo é resolvido de novo a cada consulta externa. Toda vez que uma consulta toca a tabela externa, o Redshift confere com o Glue Data Catalog onde estão os arquivos — não existe cache do schema entre execuções.
- Spectrum lê o Parquet do S3, sem trazer para dentro do cluster. O caminho de leitura externa é o mesmo do L66: nenhuma cópia do lake mora no Redshift. Isso economiza armazenamento, mas significa que essa fatia da consulta paga a mesma latência de I/O contra o S3 que o Athena pagava.
- Sem dado pré-agregado, cada clique recalcula tudo. Não existe materialized view nem tabela de resumo local aqui — toda vez que o painel renderiza, o Redshift refaz o JOIN e a agregação do zero, mesmo que o número do widget não tenha mudado desde a última vez que alguém olhou.
- O painel chega, mas mais devagar a cada executivo simultâneo. Sem fila dedicada nem burst de capacidade, um segundo ou terceiro executivo abrindo o painel ao mesmo tempo não recebe uma fatia isolada de capacidade — todos disputam a mesma base RPU, e o tempo de resposta de todo mundo piora junto.
Arquitetura de produção: fila, réplica e dado pré-computado
- → abre o painel às 9h
- → busca a credencial de serviço antes de conectar
- → devolve credencial rotacionada, válida por horas
- → SELECT loja, categoria, SUM(receita) — mesma consulta, fila dashboard
- → lê o agregado pronto, sem recalcular o JOIN a cada widget
- → linhas pré-agregadas do dia, já na mesma distribuição do fato
- → fila do workgroup passa do limiar, aciona cluster de burst
- → estado do workgroup replica continuamente para a segunda AZ
- → assume a conexão automaticamente se a zona primária cair
- → job de refresh resolve schema da tabela externa pedidos_parquet
- → devolve a localização das partições no S3
- → refresh periódico lê o Parquet via Spectrum, uma vez por ciclo
- → linhas novas desde o último refresh entram na view
- → tempo de espera na fila soma na métrica do WLM
- → painel renderiza abaixo de 3 s, mesmo com vários executivos abertos
- Fora da AWS
- Analytics
- Segurança e identidade
- Armazenamento
- Gestão e governança
O SQL que o widget dispara não mudou — ainda é `SELECT loja, categoria, SUM(receita)`. O que mudou é tudo o que existe entre o clique e a resposta: dado pré-agregado numa view que se atualiza sozinha, uma chave de distribuição que evita redistribuir dado a cada JOIN, uma fila que isola o painel do resto da conta, burst de capacidade para o pico, réplica na segunda zona e credencial fora do código. Percorra os passos: cada peça nova aqui rastreia a um requisito da seção anterior.
- O painel busca a credencial antes de conectar. Nenhuma senha vive na string de conexão do backend. O Secrets Manager guarda o usuário de serviço do Redshift, e a rotação troca a senha sem exigir reimplantar o painel — só a próxima leitura do segredo já pega o valor novo.
- A consulta entra numa fila que só o painel usa. O workgroup `dashboard` tem sua própria fila WLM, separada de qualquer outro uso do Redshift na conta. Uma consulta pesada de outro time nunca faz o painel executivo esperar atrás dela.
- A resposta vem da view pré-agregada, não do lake inteiro. Em vez de recalcular o JOIN entre pedidos e lojas a cada clique, o widget lê `mv_vendas_dia` — já pré-agregada por loja, categoria e dia, e já na mesma distribuição do fato. É a peça que mais reduz a latência do painel.
- Pico de executivos aciona burst sem a fila travar. Segunda de manhã, quando a fila do workgroup `dashboard` ultrapassa o limiar configurado, o Concurrency Scaling sobe clusters de burst transientes que absorvem o excesso — e desaparecem sozinhos quando a fila esvazia.
- A segunda zona assume sozinha se a primária falhar. Com Multi-AZ habilitado no workgroup, uma falha de computação na zona primária não derruba o painel: a réplica na segunda AZ assume a conexão automaticamente, sem playbook manual do time de 3 pessoas.
- A view se atualiza sozinha, lendo o lake uma vez por ciclo. O autorrefresh do Redshift dispara o job de atualização da `mv_vendas_dia` sozinho, sem scheduler externo. Ele consulta o Glue Catalog e lê o Parquet via Spectrum uma vez por ciclo — não uma vez por widget, como na arquitetura mínima.
- O tempo de fila vira métrica, e o painel chega abaixo de 3 s. O CloudWatch acumula o tempo de espera na fila do WLM; se ele passar do limiar combinado, o time é avisado antes que um executivo reclame. O resultado combinado de todas as peças anteriores é o painel renderizando abaixo de 3 segundos, mesmo com concorrência.
Como funciona ponta a ponta
O ciclo de vida de uma consulta no Redshift Serverless passa por seis fases, e só uma delas é onde a chave de distribuição e a ordenação realmente decidem o tempo. Entender onde cada uma atua evita o erro mais comum de diagnóstico: culpar o tamanho do cluster por um problema que é de plano de execução.
A consulta que o painel dispara é a mesma nas duas arquiteturas — o que muda é contra o que ela roda:
-- consulta.sql — a mesma pergunta do widget "receita por loja e categoria";
-- na arquitetura mínima roda contra o JOIN completo, na de produção contra a view.
-- Arquitetura mínima: JOIN recalculado a cada clique
SELECT l.nome_loja, p.category AS categoria, SUM(p.total_amount) AS receita
FROM pedidos_local p -- tabela local, DISTSTYLE AUTO, sem SORTKEY explícita
JOIN lojas l ON l.store_id = p.store_id
WHERE p.order_date = CURRENT_DATE
GROUP BY l.nome_loja, p.category
ORDER BY receita DESC;
-- Arquitetura de produção: lê o agregado pronto
SELECT nome_loja, categoria, receita
FROM mv_vendas_dia -- materialized view, já agregada por loja/categoria/dia
WHERE dia = CURRENT_DATE
ORDER BY receita DESC;
E o `EXPLAIN` da versão mínima, mostrando exatamente o custo que a chave de distribuição elimina:
XN Merge (cost=1000000234.56..1000000234.60 rows=16 width=48)
-> XN Sort (cost=1000000234.56..1000000234.58 rows=16 width=48)
Sort Key: sum(p.total_amount) DESC
-> XN HashAggregate (cost=1000000234.10..1000000234.20 rows=16 width=48)
-> XN Hash Join DS_BCAST_INNER (cost=25.00..1000000198.40 rows=900000 width=40)
Hash Cond: ("outer".store_id = "inner".store_id)
-> XN Seq Scan on pedidos_local p (cost=0.00..180000.00 rows=900000 width=24)
-> XN Hash (cost=18.00..18.00 rows=900 width=20)
-> XN Seq Scan on lojas l (cost=0.00..18.00 rows=900 width=20)
-- DS_BCAST_INNER: a tabela "lojas" (900 linhas) é copiada inteira para TODOS os nós
-- a cada execução, porque nenhuma DISTKEY garante que ela já esteja lá. Pequena hoje,
-- mas é rede e CPU gastos numa consulta que deveria custar quase zero de coordenação.
A otimização mora no dado, de novo — só que agora é distribuição, não formato
O termo que mais surpreende é o de redistribuição, não o de I/O — a mesma lição do Athena com byte varrido: a otimização mora na FORMA como o dado está organizado (distribuição, ordenação), não em reescrever o SQL. O `SELECT` do widget é literalmente idêntico entre a versão lenta e a rápida da view.
As decisões
📋 O painel executivo da Cadência precisa renderizar abaixo de 3 segundos por widget, com dezenas de executivos abrindo simultaneamente às segundas de manhã, sem duplicar os 2 anos inteiros de histórico dentro do cluster.
A combinação ataca as três causas da lentidão em separado: a materialized view elimina o recálculo de JOIN e agregação a cada clique (é o maior ganho, medido nas provas); a DISTKEY elimina a redistribuição de dado que o `EXPLAIN` mostra como `DS_BCAST_INNER`; e manter só os últimos meses como tabela local, com Spectrum para o resto, evita pagar armazenamento e tempo de carga por dado que raramente é consultado. Nenhuma das três peças sozinha chegaria a 3 segundos — juntas, cada uma resolve a fração do tempo total que as outras não tocam.
Alt: Manter tudo no Athena, só aumentando o limite do workgroup — Resolve o custo por byte, não a latência sob concorrência: o Athena continua sem estado, sem dado quente e sem fila priorizada — o mesmo `SELECT` continuaria pagando planejamento e leitura do zero a cada widget, e o problema deste laboratório é justamente esse.
Alt: Redshift provisionado (cluster fixo, não Serverless) — Dá controle mais fino sobre o tipo de nó, mas troca a elasticidade automática do Serverless por dimensionamento manual — para um time de 3 pessoas sem rotina de capacity planning, errar o tamanho do cluster (caro demais ocioso, ou pequeno demais no pico) é o risco mais provável, não o mais raro.
Alt: Cache de aplicação (ElastiCache) na frente do painel, sem trocar de motor — Resolve a repetição de uma consulta IDÊNTICA, do mesmo jeito que resolveria no Athena — mas não resolve a primeira execução de cada widget, nem o drill-down com filtro novo, que é onde o painel realmente sofre. É complementar a este desenho, não substituto.
Alt: Carregar os 2 anos inteiros de pedidos como tabela local no Redshift — Eliminaria toda dependência do Spectrum, mas paga armazenamento e tempo de carga por um volume que o painel raramente consulta — 90% do uso real filtra os últimos 90 dias. É a troca errada de complexidade por desempenho que o requisito 5 da seção anterior já descarta.
| Decisão | Alternativa considerada | Motivo da escolha | Trade-off aceito |
|---|---|---|---|
| DISTKEY explícito em store_id | deixar DISTSTYLE AUTO decidir sozinho | AUTO só reclassifica depois de observar padrão de uso real — o painel precisa de latência baixa desde o primeiro dia em produção | se o padrão de JOIN mudar (nova dimensão, novo tipo de agregação), a chave pode precisar de revisão manual |
| Materialized view com autorrefresh, não CTAS agendado como no L66 | reaproveitar o padrão de scheduler + CTAS do Athena | o Redshift já tem autorrefresh nativo — reimplementar um scheduler externo duplicaria trabalho que o serviço resolve sozinho | autorrefresh consome RPU do próprio workgroup; um pico de refresh mal cronometrado pode competir com consulta interativa |
| Spectrum só para drill-down além do horizonte local | manter todo o histórico só como tabela local | evita pagar armazenamento e carga por dado raramente consultado, mantendo o caminho rápido para o caso comum | consulta de drill-down histórico é mais lenta que consulta contra tabela local — trade-off aceito porque é caso raro |
| Concurrency Scaling em vez de aumentar a base RPU permanentemente | provisionar RPU fixa alta o suficiente para o pico de segunda de manhã | RPU fixa alta cobra o pico o tempo todo, mesmo nas horas ociosas da tarde | burst tem latência de acionamento — não é instantâneo, então picos MUITO abruptos ainda sentem alguma fila |
Construir: distribuição e ordenação
A primeira peça é a mais barata de errar e a mais cara de corrigir depois: a distribuição do fato. Ela decide, para sempre até um `ALTER TABLE ALTER DISTKEY` custoso, se o JOIN mais comum do painel redistribui dado a cada execução ou não.
-- schema.sql — a tabela fato e a dimensão, com DISTKEY e SORTKEY escolhidos
-- a partir do padrão de uso real do painel, não de palpite.
-- Fato: distribuído por store_id, porque é a coluna do JOIN mais comum
-- (painel agrupa por loja em praticamente todo widget).
CREATE TABLE pedidos_local (
order_id BIGINT NOT NULL,
store_id INTEGER NOT NULL,
category VARCHAR(64) NOT NULL,
order_date DATE NOT NULL,
total_amount DECIMAL(12,2) NOT NULL
)
DISTSTYLE KEY
DISTKEY (store_id)
COMPOUND SORTKEY (order_date, store_id);
-- COMPOUND porque 90% das consultas do painel filtram por order_date PRIMEIRO,
-- e store_id em seguida (drill-down por loja dentro de um dia). Interleaved
-- serviria melhor se o painel filtrasse por qualquer coluna isoladamente com
-- frequência parecida — não é o caso medido aqui.
-- Dimensão pequena: replicada inteira em cada nó, então o JOIN nunca precisa
-- buscar a linha da loja em outro nó pela rede.
CREATE TABLE lojas (
store_id INTEGER NOT NULL,
nome_loja VARCHAR(120) NOT NULL,
regiao VARCHAR(60)
)
DISTSTYLE ALL;
-- 900 linhas: replicar em cada nó custa pouco armazenamento e elimina a
-- redistribuição inteira do lado da dimensão em todo JOIN do painel.
| DISTSTYLE | Como distribui | Quando escolher | O que sai errado ao escolher mal |
|---|---|---|---|
| KEY | todas as linhas com o mesmo valor da coluna vão para o mesmo nó | tabela fato grande, com um JOIN dominante numa coluna específica | se o JOIN real usa OUTRA coluna, a redistribuição acontece do mesmo jeito — a chave errada não ajuda |
| ALL | a tabela inteira é copiada para todos os nós | tabela pequena e relativamente estável (dimensão), consultada em muitos JOINs diferentes | usada numa tabela grande, multiplica armazenamento pelo número de nós e o custo de escrita explode |
| EVEN | linhas espalhadas em round-robin, sem critério de coluna | tabela sem padrão de JOIN claro, ou carga majoritariamente de leitura sequencial completa | escolhida por padrão em tabela com JOIN frequente — é a mesma redistribuição de AUTO sem nem tentar aprender |
| AUTO | o Redshift decide e pode reclassificar com o tempo, observando o uso | protótipo, ou fase inicial em que o padrão de consulta ainda não está claro | produção com requisito de latência desde o primeiro dia — a reclassificação não é instantânea, e o painel sofre enquanto isso |
Dado agregado de todos os lojistas na mão de um só é vazamento, não recorte de UX
Numa plataforma multi-tenant como a Cadência, a dimensão `lojas` e o fato `pedidos_local` agregados neste painel expõem receita de TODOS os 900 lojistas a qualquer identidade com acesso de leitura ao workgroup — exatamente o mesmo risco que o L66 já registrou para o resumo mensal do Athena. Enquanto o L69 não trata permissão por linha, restrinja o acesso ao painel à equipe interna, nunca diretamente aos lojistas.
Construir: a view que substitui o JOIN recalculado
A materialized view é a peça que mais reduz a latência medida: em vez de o painel juntar e agregar `pedidos_local` com `lojas` a cada clique, ele lê um resultado já pronto, atualizado automaticamente pelo próprio serviço.
-- mv_vendas_dia.sql — pré-agrega o widget mais consultado do painel.
-- AUTO REFRESH: o Redshift decide sozinho QUANDO atualizar, com base em
-- mudança no dado-fonte — não existe scheduler externo como no CTAS do L66.
CREATE MATERIALIZED VIEW mv_vendas_dia
AUTO REFRESH YES
AS
SELECT
p.order_date AS dia,
p.store_id,
l.nome_loja,
p.category AS categoria,
SUM(p.total_amount) AS receita,
COUNT(*) AS pedidos
FROM pedidos_local p
JOIN lojas l ON l.store_id = p.store_id
WHERE p.order_date >= DATEADD(day, -90, CURRENT_DATE)
-- 90 dias cobre o horizonte que o painel de fato consulta; além disso, o
-- drill-down cai para o schema externo via Spectrum (ver spectrum.sql).
GROUP BY p.order_date, p.store_id, l.nome_loja, p.category;
-- spectrum.sql — schema externo para o caso raro de drill-down além dos 90 dias
-- que a view local cobre. Lê o MESMO pedidos_parquet que o Athena consultava no L66.
CREATE EXTERNAL SCHEMA lake_spectrum
FROM DATA CATALOG
DATABASE 'lake_cadencia'
IAM_ROLE 'arn:aws:iam::<conta>:role/redshift-spectrum-lake'
CREATE EXTERNAL DATABASE IF NOT EXISTS;
-- Nenhum CREATE TABLE aqui: o Glue Data Catalog do L65 já descreve
-- pedidos_parquet, e o Spectrum lê essa definição diretamente.
-- Consulta de drill-down: histórico completo, sem tocar a tabela local
SELECT category AS categoria, SUM(total_amount) AS receita
FROM lake_spectrum.pedidos_parquet
WHERE order_date BETWEEN DATE '2024-01-01' AND DATE '2024-12-31'
GROUP BY category;
-- Este caminho NÃO usa a DISTKEY nem a SORTKEY do cluster — o dado externo
-- não está distribuído nem ordenado pelo Redshift, só filtrado e projetado.
# workgroup.tf — o workgroup de produção: fila dedicada, Multi-AZ, e o IAM
# role que autoriza o Spectrum a ler o mesmo lake do L66.
resource "aws_redshiftserverless_namespace" "cadencia" {
namespace_name = "${var.projeto}-dw"
admin_user_password = null # gerenciado via Secrets Manager, nunca aqui
manage_admin_password = true
iam_roles = [aws_iam_role.spectrum.arn]
default_iam_role_arn = aws_iam_role.spectrum.arn
}
resource "aws_redshiftserverless_workgroup" "dashboard" {
namespace_name = aws_redshiftserverless_namespace.cadencia.namespace_name
workgroup_name = "${var.projeto}-dashboard"
base_capacity = 32 # RPU — dimensionado pelo pior caso medido nas provas, com folga
# Multi-AZ: falha de computação numa zona não derruba o painel. Atributo
# dedicado do workgroup Serverless — o dado em si já é durável independente
# disso, é a DISPONIBILIDADE do motor de consulta que este campo protege.
multi_az = true
config_parameter {
parameter_key = "enable_user_activity_logging"
parameter_value = "true"
}
}
# IAM role que o Spectrum assume para ler o lake — restrito ao bucket e ao
# catálogo deste projeto, nunca a conta inteira.
data "aws_iam_policy_document" "spectrum" {
statement {
effect = "Allow"
actions = ["s3:GetObject", "s3:ListBucket"]
resources = [
"arn:aws:s3:::${var.bucket_lake_ouro}",
"arn:aws:s3:::${var.bucket_lake_ouro}/pedidos_parquet/*",
]
}
statement {
effect = "Allow"
actions = ["glue:GetTable", "glue:GetDatabase", "glue:GetPartitions"]
# Glue não aceita ARN de partição individual para esta ação — "*" aqui é o
# caso em que o recurso específico NÃO é suportado, não um atalho de preguiça.
resources = ["arn:aws:glue:${var.regiao}:${var.conta}:catalog",
"arn:aws:glue:${var.regiao}:${var.conta}:database/lake_cadencia",
"arn:aws:glue:${var.regiao}:${var.conta}:table/lake_cadencia/pedidos_parquet"]
}
}
Apagar o namespace sem snapshot é perda permanente da tabela local
Excluir o namespace do Redshift Serverless sem um snapshot final apaga a tabela local e a materialized view PARA SEMPRE — diferente do S3, que continua guardando o Parquet original intacto. O `aws_redshiftserverless_namespace` não tem `final_snapshot_name` configurado por padrão. Antes de qualquer `terraform destroy` em produção, confirme que existe um snapshot recente ou que o dado local é 100% reconstruível a partir do lake.
Construir: o backend do painel, com pool e credencial rotacionada
O painel não fala com o Redshift diretamente do navegador: um backend em .NET 8 mantém o pool de conexões, lê a credencial do Secrets Manager em runtime e expõe `/health` (sem tocar o banco) separado de `/ready` (que toca) — a mesma distinção que evita o ALB derrubar uma instância sadia quando o Redshift oscila.
// Program.cs — backend do painel: pool via Npgsql (Redshift fala protocolo
// Postgres), credencial lida do Secrets Manager, /health e /ready separados.
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;
using Npgsql;
var builder = WebApplication.CreateBuilder(args);
// A senha NUNCA vem de appsettings nem de variável de ambiente fixa — só do
// Secrets Manager, buscada uma vez na inicialização e recarregada por rotação.
var secretsClient = new AmazonSecretsManagerClient();
var segredo = await secretsClient.GetSecretValueAsync(new GetSecretValueRequest
{
SecretId = Environment.GetEnvironmentVariable("SECRET_ID_REDSHIFT")!,
});
var credencial = System.Text.Json.JsonSerializer.Deserialize<CredencialRedshift>(segredo.SecretString)!;
var connString = new NpgsqlConnectionStringBuilder
{
Host = Environment.GetEnvironmentVariable("REDSHIFT_ENDPOINT"),
Port = 5439,
Database = "cadencia_dw",
Username = credencial.Usuario,
Password = credencial.Senha,
Pooling = true,
MinPoolSize = 2,
MaxPoolSize = 20, // teto explícito: nunca uma conexão nova por requisição
Timeout = 5,
}.ToString();
builder.Services.AddSingleton(new NpgsqlDataSourceBuilder(connString).Build());
var app = builder.Build();
// /health responde sem tocar o Redshift — confirma só que o PROCESSO está vivo.
app.MapGet("/health", () => Results.Ok("ok"));
// /ready toca o banco de verdade — é o que o ALB usa para decidir se manda
// tráfego. Se o Redshift estiver oscilando, /ready falha e /health continua ok,
// então o processo não é reiniciado à toa por um problema que não é dele.
app.MapGet("/ready", async (NpgsqlDataSource ds) =>
{
await using var cmd = ds.CreateCommand("SELECT 1");
await cmd.ExecuteScalarAsync();
return Results.Ok("ready");
});
app.MapGet("/api/vendas-dia", async (NpgsqlDataSource ds) =>
{
// Lê a view pré-agregada — nunca o JOIN cru contra pedidos_local.
await using var cmd = ds.CreateCommand(
"SELECT nome_loja, categoria, receita FROM mv_vendas_dia " +
"WHERE dia = CURRENT_DATE ORDER BY receita DESC LIMIT 10");
var linhas = new List<object>();
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
linhas.Add(new { Loja = reader.GetString(0), Categoria = reader.GetString(1), Receita = reader.GetDecimal(2) });
return Results.Ok(linhas);
});
app.Run();
record CredencialRedshift(string Usuario, string Senha);
Por que o pool tem teto explícito, e não "o quanto der"
O pool com teto de 20 conexões não é um número arbitrário: o workgroup Serverless tem um limite de conexões concorrentes por RPU configurada, e um backend que abrisse conexão por requisição esgotaria esse limite muito antes de qualquer teto de RPU. Redshift, como Postgres, paga um custo real de memória por conexão aberta — pool pequeno e reutilizado vence conexão nova a cada clique.
Implantar e provar: painel abaixo de 3 s
Cinco provas. Nenhuma delas é "o painel abriu" — cada uma tem um número que aprova ou reprova.
- Prova 1 — a latência antes do tuning. Rode o widget "receita por loja e categoria" contra `pedidos_local` sem DISTKEY (DISTSTYLE AUTO recém-criado) e capture `EXPLAIN`. Esperado: presença de `DS_BCAST_INNER` ou `DS_DIST_BOTH` no plano, e tempo de execução na casa de 8 a 15 segundos com a tabela no volume atual.
- Prova 2 — a latência depois da DISTKEY. Rode a MESMA consulta contra `pedidos_local` com `DISTKEY (store_id)` aplicado e `lojas` em `DISTSTYLE ALL`. Esperado: `EXPLAIN` sem `DS_BCAST_INNER` no JOIN principal, e tempo de execução abaixo de 1 segundo — a redistribuição de dado, não o volume, era o gargalo.
- Prova 3 — o painel completo, com a view. Carregue o painel de 8 widgets contra `mv_vendas_dia` e meça o p95 de todos eles. Esperado: p95 abaixo de 3.000 ms — o entregável declarado na abertura deste módulo.
- Prova 4 — concorrência sem fila travando. Simule 10 execuções simultâneas do painel completo (80 consultas) contra o workgroup `dashboard`. Esperado: o tempo de fila médio (`WLM Queue Wait Time` no CloudWatch) permanece abaixo de 500 ms, e o Concurrency Scaling aparece ativo nas métricas se o pico ultrapassar a base RPU.
- Prova 5 — o drill-down histórico ainda funciona, mais lento e sabendo por quê. Rode uma consulta de 12 meses contra `lake_spectrum.pedidos_parquet`. Esperado: tempo de execução muito maior que contra a view local (segundos, não milissegundos) — confirmando que o caminho de Spectrum é o CERTO para esse caso raro, não um sintoma de configuração quebrada.
PROJETO=cadencia; REGIAO=us-east-1; DB=cadencia_dw
ENDPOINT=$(aws redshift-serverless get-workgroup --workgroup-name "${PROJETO}-dashboard" \
--region "$REGIAO" --query 'workgroup.endpoint.address' --output text)
# ── Prova 1: EXPLAIN antes da DISTKEY ─────────────────────────────────────
psql "host=$ENDPOINT port=5439 dbname=$DB" -c "
EXPLAIN SELECT l.nome_loja, p.category, SUM(p.total_amount)
FROM pedidos_local p JOIN lojas l ON l.store_id = p.store_id
WHERE p.order_date = CURRENT_DATE GROUP BY l.nome_loja, p.category;"
# ── Prova 3: p95 do painel completo, 8 widgets, 20 execuções ──────────────
for i in $(seq 1 20); do
/usr/bin/time -f "%e" psql "host=$ENDPOINT port=5439 dbname=$DB" -c \
"SELECT nome_loja, categoria, receita FROM mv_vendas_dia WHERE dia = CURRENT_DATE;" \
2>> tempos_widget1.txt
done
sort -n tempos_widget1.txt | awk 'BEGIN{c=0} {a[c++]=$1} END{print a[int(c*0.95)]}'
Rode a prova de concorrência sabendo o que ela aciona
A prova 4 consome RPU de burst de verdade — não rode em loop automatizado fora de uma janela de teste combinada, porque Concurrency Scaling é cobrado por segundo de cluster de burst ativo. É seguro rodar uma vez para validar o número; agendar para repetir sozinho é o mesmo erro do `SELECT *` sem filtro do L66, só que na conta do Redshift.
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 |
|---|---|---|---|
| Recriar `pedidos_local` sem repetir o `DISTKEY (store_id)` | o painel volta a levar segundos por widget, sem nenhum erro — só lento de novo | `SELECT "column", distkey FROM svv_table_info WHERE table = 'pedidos_local';` | reaplicar `ALTER TABLE pedidos_local ALTER DISTKEY store_id`, ou recriar a tabela com o DDL correto |
| Desabilitar `AUTO REFRESH` da materialized view e esquecer | o painel renderiza rápido, mas com número de dias atrás — sem erro, sem alerta visual | `SELECT is_stale FROM svv_mv_info WHERE name = 'mv_vendas_dia';` | reabilitar `ALTER MATERIALIZED VIEW mv_vendas_dia AUTO REFRESH YES`, ou forçar `REFRESH MATERIALIZED VIEW` manual |
| Rodar a consulta de drill-down direto contra `pedidos_local` em vez de `lake_spectrum` | consulta de 12 meses falha ou trava — a tabela local só guarda 90 dias | checar `WHERE order_date BETWEEN` contra o horizonte real da tabela local | apontar consultas de histórico longo para o schema externo `lake_spectrum`, nunca para a tabela local de 90 dias |
Rápido e errado passa despercebido mais fácil que lento e certo
Uma view desatualizada não é o mesmo defeito que uma consulta lenta. `is_stale = true` não gera erro nem alarme por padrão — o painel continua respondendo rápido, só que com o número errado. É o defeito mais silencioso dos três, porque "rápido" parece sucesso.
Um `EXPLAIN` de uma consulta que junta a tabela fato `pedidos_local` (grande) com a tabela `lojas` (900 linhas) mostra `DS_BCAST_INNER` no plano de execução. O que isso indica, e o que resolve?
Segurança
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Receita agregada de todos os 900 lojistas visível a uma identidade sem escopo | média | exposição de dado comercial entre concorrentes, alto | IAM restrito ao backend do painel; permissão fina por linha fica para o L69 | CloudTrail de conexões ao Redshift por identidade | revogar acesso amplo imediatamente; tratar isolamento completo é o escopo do L69 |
| Credencial do usuário de serviço vazada num log ou repositório | baixa-média | acesso não autorizado ao workgroup inteiro, alto | Secrets Manager com rotação automática; nunca logar a connection string completa | GuardDuty e alarme de conexão de IP fora do range esperado | rotacionar o segredo imediatamente; a rotação já invalida a credencial vazada |
| Concurrency Scaling acionado por consulta maliciosa ou mal escrita, gerando custo inesperado | baixa | financeiro, médio | fila WLM com limite de tempo de execução por consulta (query monitoring rule) | alarme de custo por workgroup no Cost Explorer, tag específica de `dashboard` | ajustar a query monitoring rule para abortar consulta acima do limiar de tempo |
| IAM role do Spectrum com permissão além do bucket do lake | baixa | acesso de leitura a dado fora do escopo do painel, médio | policy restrita ao prefixo `pedidos_parquet/*` e ao database específico do Glue | IAM Access Analyzer sobre a role do Spectrum | restringir o `Resource` da policy imediatamente e auditar uso recente via CloudTrail |
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 tempo uma consulta do painel espera na fila? | QueryDuration/WLM Queue Wait Time por workgroup | alertar acima de 500 ms sustentados |
| A view está atualizada? | svv_mv_info.is_stale | qualquer `true` fora da janela de refresh esperada vale investigação |
| O Concurrency Scaling está sendo acionado, e com que frequência? | ConcurrencyScalingSeconds por workgroup | informativo; base para decidir se a base RPU precisa subir |
| Alguma consulta está redistribuindo dado sem necessidade? | contagem de `DS_BCAST_INNER`/`DS_DIST_BOTH` em `stl_explain` | qualquer ocorrência recorrente no mesmo padrão de consulta sugere DISTKEY desalinhada |
| O failover Multi-AZ já foi testado de verdade? | log de eventos do workgroup + tempo de indisponibilidade observado | simular ao menos uma vez antes de depender dele em produção |
Fila e execução são dois problemas diferentes com a mesma sintoma visível
Meça o tempo de FILA separado do tempo de EXECUÇÃO. Um painel lento por fila cheia se resolve com WLM e Concurrency Scaling; um painel lento por execução se resolve com DISTKEY, SORTKEY ou a materialized view. Tratar os dois como o mesmo sintoma leva a ajustar a peça errada.
Escala
Redshift Serverless é regional e gerenciado; a fronteira de disponibilidade que importa aqui é a zona de disponibilidade dentro da região, coberta pelo Multi-AZ — não réplica entre regiões, fora do escopo deste módulo.
| Dimensão | 1 executivo | 50 executivos simultâneos | 900 lojas viram 5.000 | Falha de AZ |
|---|---|---|---|---|
| O que muda | base RPU nunca é tocada; qualquer configuração resolve | fila do WLM enche na base RPU sozinha; Concurrency Scaling precisa estar habilitado | a materialized view cresce, mas o padrão de agregação (por loja/categoria/dia) não muda de forma — o volume por linha é o que sobe | computação primária para de responder; workgroup Multi-AZ falha para a segunda zona |
| Onde o risco aparece | nenhum risco novo — é o cenário do dia 1 | burst mal configurado (limiar alto demais) deixa a fila crescer antes de acionar | refresh da materialized view demora mais a cada ciclo; pode exigir refresh incremental em vez de completo | sem Multi-AZ, o painel fica indisponível até intervenção manual |
| O que corrige | nada a fazer ainda | calibrar o limiar de Concurrency Scaling a partir do padrão real medido, não de palpite | monitorar tempo de refresh e considerar particionar a view por região de loja se o tempo subir demais | Multi-AZ habilitado no workgroup, testado ao menos uma vez de propósito |
Onde o DR entre regiões é tratado de verdade
Réplica entre regiões, RTO/RPO formal e recriação do namespace numa região secundária são decisão de arquitetura própria, com trade-off de custo de replicação contínua. O laboratório `lab-dr-multiregiao-quatro-estrategias` cobre as quatro estratégias aplicáveis — este módulo não resume esse 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 (base RPU mínima, sem tuning) | Produção pequena (workgroup dashboard, tunado) | Alta escala (múltiplos workgroups, burst frequente) |
|---|---|---|---|
| RPU-hora consumida | proporcional só ao tempo de consulta rodando, sem otimização | menor por consulta — DISTKEY e SORTKEY reduzem trabalho por RPU | cresce com o número de workgroups e a frequência de burst |
| Concurrency Scaling | não configurado — fila cresce em vez de custar | acionado só no pico, cobrado por segundo de uso | pode virar custo relevante se o limiar for calibrado baixo demais |
| Armazenamento local | tabela fato completa, sem limite de retenção | só ~90 dias locais; resto vive no S3 via Spectrum | cresce com o número de tabelas locais de outros times, se o padrão se replicar |
| Refresh da materialized view | inexistente nesta fase | consome RPU do próprio workgroup a cada ciclo automático | ponto em que vale avaliar refresh incremental ou horário fixo fora do pico |
Redshift cobra piso de 60s por retomada, não por estar ocioso
O custo oculto aqui não é o RPU-hora óbvio: é a cobrança MÍNIMA de 60 segundos por consulta, mesmo que ela termine em 2. O Redshift Serverless não cobra nada em período totalmente ocioso — a computação para de fato para quando não há consulta —, mas cada retomada de inatividade acorda o workgroup e cobra o piso de 60s inteiro, ainda que a resposta volte em milissegundos. Para um painel de uso constante em horário comercial isso dilui bem; para acesso raríssimo e espaçado, cada abertura paga o piso inteiro, e o Athena do L66 (sem piso de tempo, só por byte varrido) continua sendo a escolha mais barata.
Well-Architected nos seis pilares
| Pilar | Situação | Risco | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | materialized view depende de AUTO REFRESH, sem alerta se ficar `is_stale` | painel mostra número desatualizado sem sinal visual | alarme CloudWatch sobre `svv_mv_info.is_stale` | alta |
| Segurança | IAM do backend restrito, mas sem permissão fina por linha entre lojistas | analista de uma loja pode ver a receita agregada de todas | Lake Formation com permissão por linha — tema do L69 | média, para este estágio |
| Confiabilidade | Multi-AZ habilitado, mas nunca testado de propósito em produção | failover pode não se comportar como documentado sob carga real | simular falha de AZ controlada ao menos uma vez, fora do horário de pico | alta |
| Eficiência de performance | DISTKEY e SORTKEY calibrados para o padrão de uso ATUAL do painel | novo widget com padrão de filtro diferente pode voltar a redistribuir dado | revisar `EXPLAIN` sempre que um widget novo for adicionado ao painel | média |
| Otimização de custo | base RPU dimensionada pelo pico medido, Concurrency Scaling calibrado | nenhum imediato — é o objeto central deste laboratório | revisar o limiar de burst a cada trimestre, conforme o padrão de acesso muda | já tratada |
| Sustentabilidade | materialized view reduz trabalho recomputado a cada clique do painel | nenhum risco novo — consequência direta da otimização de performance | nenhuma ação adicional além do que este módulo já entrega | baixa, benefício automático |
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 | Redshift Serverless base RPU, DISTSTYLE AUTO, sem materialized view, painel direto contra o JOIN cru | qualquer widget novo pode redistribuir dado sem que ninguém perceba até o painel ficar lento | baixo em volume de uso, mas o piso de RPU já é maior que o custo do Athena para o mesmo uso esporádico |
| 2. Aplicação básica | DISTKEY e SORTKEY escolhidos manualmente uma vez, sem revisão quando o padrão de uso muda | chave que servia no lançamento fica desalinhada quando um widget novo introduz um filtro diferente | cai bastante por consulta tunada, mas o custo de recalibrar DISTKEY depois é uma operação pesada (reescreve a tabela inteira) |
| 3. Produção (este laboratório) | materialized view com autorrefresh, fila WLM dedicada, Concurrency Scaling, Multi-AZ, credencial via Secrets Manager | falso senso de atualização: view `is_stale` sem alerta visual mostra número errado com aparência de sucesso | previsível e mensurável; p95 abaixo de 3 s medido nas provas, com burst cobrando só sob pico real |
| 4. Alta escala | múltiplas materialized views por domínio de widget, refresh incremental em vez de completo, particionamento lógico por região de loja | refresh incremental mal configurado pode divergir silenciosamente do dado-fonte ao longo do tempo | cresce sublinear ao volume de dado, mas cresce com o número de views mantidas — cada uma tem seu próprio custo de refresh |
| 5. Plataforma | workgroups por domínio de negócio com chargeback por tag, catálogo de views compartilhado, RA3 provisionado para cargas muito constantes substituindo Serverless onde compensa | contenção de RPU entre workgroups quando muitos times competem pela mesma conta | exige rateio explícito por workgroup — sem tag, o custo de BI se mistura com o de outras cargas analíticas |
| 6. Dados e IA | o mesmo agregado que o painel consulta vira feature de entrada para um modelo de previsão: `CREATE MODEL previsao_demanda_loja` via Redshift ML treina, com SageMaker Autopilot por baixo, um modelo de demanda por loja e categoria, direto em SQL — e a inferência (`previsao_demanda_loja(...)`) some como coluna nova na própria consulta do painel, ao lado do dado real | modelo de previsão apresentado sem indicar incerteza é tomado como fato pela diretoria — decisão automatizada sem revisão humana quando o erro do modelo é caro | treino consome RPU adicional; sem workgroup isolado para o pipeline de ML, o custo de treinar se mistura com o custo de servir o painel interativo |
Onde IA entra, e onde não entra
Diferente do L66 — em que prever custo de consulta é aritmética exata que um modelo só pioraria —, aqui existe um problema genuinamente preditivo esperando do outro lado do mesmo dado: quanto cada loja vai vender por categoria na próxima semana. Uma regra fixa ("média dos últimos 30 dias") não captura sazonalidade por categoria nem efeito de promoção — é exatamente o tipo de padrão que um modelo aprende e uma regra escrita à mão não generaliza sem virar uma árvore de exceções.
Onde IA agrega de verdade: previsão de demanda por loja e categoria
Redshift ML treina um modelo via SQL (`CREATE MODEL`), usando SageMaker Autopilot por baixo, direto sobre o dado que já está pré-agregado neste laboratório — sem exportar para fora do warehouse. A inferência roda como função SQL na própria consulta do painel. O dado de treino é o mesmo `mv_vendas_dia` (ou seu histórico completo via Spectrum); quando o modelo erra — e prever demanda erra, sempre, em algum grau — a previsão aparece no painel como estimativa rotulada, ao lado do número real, nunca substituindo-o, e o time de compras revisa antes de decidir reposição de estoque.
Isso pertence à banda 8 desta série (ML com disciplina de engenharia, L71–L80), não a este módulo — o nível 6 da evolução acima só aponta a porta. Construir o pipeline de treino, validar contra um baseline simples e medir drift do modelo é o assunto de outros laboratórios; aqui a lição que fica é que a IA entra DEPOIS que o dado já está barato de consultar (L66) e rápido de servir (este módulo) — nunca antes, porque um modelo treinado sobre dado lento ou caro de acessar herda os dois problemas.
Anti-padrões
| Anti-padrão | Por que alguém faz isso | Sintoma em produção | Forma correta |
|---|---|---|---|
| Deixar DISTSTYLE AUTO em produção "porque o Redshift aprende sozinho" | é a opção que exige zero decisão no dia da criação da tabela, e funciona bem o suficiente em teste com pouco dado | latência inconsistente nas primeiras semanas, até o otimizador reclassificar — e o painel já está em produção quando isso acontece | medir o padrão de JOIN real ANTES de escolher DISTKEY, mesmo que isso signifique atrasar o lançamento por um dia de análise |
| Carregar o histórico inteiro como tabela local "para não depender do Spectrum" | Spectrum parece um componente extra e uma fonte de complexidade a menos de gerenciar | custo de armazenamento e tempo de carga crescem sem parar, para atender 5% do uso real (drill-down raro) | manter local só o horizonte que o padrão de uso realmente consulta, e usar Spectrum para o resto |
| Confundir materialized view atualizada com materialized view CORRETA | `is_stale = false` parece prova suficiente de que o dado está certo | refresh incremental mal configurado marca a view como atualizada mesmo divergindo do dado-fonte em algum ponto | validar periodicamente a soma da view contra uma consulta direta ao fato, não só o flag de staleness |
| Aumentar a base RPU permanentemente para resolver um pico pontual de segunda de manhã | é o ajuste mais simples de fazer no console, sem precisar entender Concurrency Scaling | a conta paga o pico o dia inteiro, mesmo nas 20 horas em que ninguém abre o painel | usar Concurrency Scaling para o pico transiente, e reservar aumento de base RPU para crescimento SUSTENTADO de uso |
| Testar o failover Multi-AZ só lendo a documentação, nunca simulando de propósito | simular falha em produção dá medo, e a AWS promete que "é automático" | a primeira falha real vira o primeiro teste, e ninguém sabe quanto tempo o failover realmente leva sob carga | simular a falha controlada, em horário de baixo uso, e medir o tempo real de recuperação |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Widget específico volta a levar segundos, os outros continuam rápidos | aquele widget faz JOIN numa coluna diferente de store_id, redistribuindo dado | `EXPLAIN` da consulta do widget, procurando `DS_BCAST_INNER` ou `DS_DIST_BOTH` | plano de execução no console do Redshift ou via `stl_explain` | avaliar se vale uma segunda DISTKEY (via projeção materializada) ou aceitar o custo para esse caso raro |
| Painel mostra número visivelmente desatualizado, sem erro nenhum | materialized view com AUTO REFRESH desabilitado ou falhando silenciosamente | `SELECT is_stale, last_refresh_time FROM svv_mv_info` | metadados da view no catálogo do Redshift | reabilitar AUTO REFRESH ou investigar por que o refresh automático está falhando |
| Painel fica lento só na segunda de manhã, o resto da semana está rápido | fila do WLM cheia porque Concurrency Scaling não acionou a tempo | métrica `ConcurrencyScalingSeconds` e `WLM Queue Wait Time` no CloudWatch, no horário do pico | CloudWatch, dashboard do workgroup `dashboard` | reduzir o limiar de acionamento do Concurrency Scaling, ou aumentar a base RPU se o pico for previsível |
| Consulta de drill-down histórico trava ou nunca termina | consulta caiu na tabela local de 90 dias em vez do schema externo Spectrum | conferir se a query usa `lake_spectrum.pedidos_parquet` ou `pedidos_local` | texto da consulta enviada pelo backend | corrigir o roteamento no backend para consultas com filtro de data além do horizonte local |
| Custo do workgroup sobe sem nenhum pico visível de uso do painel | Concurrency Scaling acionado por consulta de outro processo compartilhando o mesmo workgroup | agregar `ConcurrencyScalingSeconds` por identidade de conexão | CloudTrail de conexões ao Redshift + logs de query do workgroup | isolar o painel num workgroup próprio, sem outros processos competindo pela mesma fila |
Configuração de tuning envelhece junto com o padrão de uso
Das cinco linhas acima, três nascem do mesmo hábito: alguém assumiu que "configurado uma vez" significa "correto para sempre". DISTKEY, materialized view e workgroup dedicado precisam de revisão quando o padrão de uso do painel muda — não são configuração que se define e esquece.
Limpeza
PROJETO=cadencia; REGIAO=us-east-1
# 1. Snapshot final ANTES de qualquer destroy — o namespace não guarda o dado
# local em lugar nenhum além dele mesmo.
aws redshift-serverless create-snapshot --namespace-name "${PROJETO}-dw" \
--snapshot-name "${PROJETO}-dw-final-$(date +%Y%m%d)" --region "$REGIAO"
# 2. Terraform destroy cuida do workgroup, do namespace e das roles GERENCIADAS
terraform destroy -auto-approve
# 3. A materialized view e a tabela local morrem junto com o namespace — não
# precisam de remoção separada, diferente do CTAS do Athena no L66, que vive
# fora do Terraform. Aqui, tudo que é do Redshift é do namespace.
# 4. O que o destroy NÃO leva: o lake em S3 continua intacto — é o mesmo bucket
# do L66/L67, e este laboratório nunca o modifica, só lê via Spectrum.
aws secretsmanager delete-secret --secret-id "${PROJETO}/redshift/admin" \
--recovery-window-in-days 7 --region "$REGIAO"
| O que foi criado | O `destroy` leva? | Continua cobrando depois? |
|---|---|---|
| Namespace e workgroup `dashboard` | sim, via Terraform | não — sem custo fixo depois de removido |
| Tabela local `pedidos_local` e `mv_vendas_dia` | sim — morrem junto com o namespace | não, desde que o snapshot final tenha sido tirado antes |
| Snapshot final do namespace | NÃO — snapshot sobrevive ao destroy do namespace de propósito | sim, até ser removido manualmente |
| Secret do Secrets Manager | NÃO por padrão — precisa de remoção explícita | sim, com janela de recuperação de 7 a 30 dias mesmo após o delete |
| Schema externo `lake_spectrum` e a role de Spectrum | sim, via Terraform | não — schema externo não tem custo próprio, só o que consulta paga |
| Lake em S3 (`pedidos_parquet`) | NÃO — pertence ao L66/L67, este laboratório só lê | sim, mas é custo de outro laboratório, não deste |
Sem snapshot, o destroy é definitivo para o dado local
O Redshift Serverless não tem "lixeira" para a tabela local como o S3 tem versionamento. Sem o snapshot do passo 1, excluir o namespace apaga `pedidos_local` e `mv_vendas_dia` de forma definitiva — o único jeito de reconstruir depois é recarregar do lake em S3 e reprocessar tudo, o que custa tempo, não só dinheiro.
Resumo
| Problema | Peça | Motivo |
|---|---|---|
| JOIN recalculado a cada clique do widget | materialized view com autorrefresh | elimina o recálculo repetido; é o maior ganho de latência medido |
| Redistribuição de dado a cada JOIN entre nós | DISTKEY em store_id no fato, DISTSTYLE ALL na dimensão | co-localiza o JOIN mais comum do painel, sem tráfego de rede entre nós |
| Fila de consulta compartilhada com o resto da conta | workgroup dedicado com fila WLM própria | isola o painel de qualquer outra carga na mesma conta Redshift |
| Pico de segunda de manhã sem capacidade extra | Concurrency Scaling | absorve burst transiente sem exigir base RPU permanentemente alta |
| Falha de zona derrubando o painel inteiro | workgroup Multi-AZ | failover automático de computação, sem playbook manual |
| Falha possível | O que protege |
|---|---|
| DISTKEY desalinhada com o JOIN real | revisão de `EXPLAIN` sempre que um widget novo entra no painel |
| View desatualizada sem alerta | monitoramento de `svv_mv_info.is_stale` |
| Fila cheia no pico | Concurrency Scaling calibrado pelo padrão real medido |
| Falha de zona de disponibilidade | Multi-AZ testado de propósito, não só configurado |
| Perda do dado local no destroy | snapshot final antes de qualquer remoção do namespace |
- O painel dispara a mesma consulta SQL de sempre contra o backend.
- O backend busca a credencial rotacionada no Secrets Manager antes de conectar.
- A consulta entra na fila dedicada do workgroup `dashboard`, sem competir com outra carga.
- Na maioria dos casos, ela lê `mv_vendas_dia` — já pré-agregada, sem recalcular JOIN.
- Se o filtro pede histórico além de 90 dias, a consulta vai por Spectrum, direto no lake do L66/L67.
- Se a fila crescer no pico, Concurrency Scaling absorve o excesso automaticamente.
- Se a zona primária falhar, a réplica Multi-AZ assume sem intervenção manual.
- O painel renderiza abaixo de 3 segundos — medido, não estimado.
A frase que resume o módulo inteiro
O Athena do L66 e o Redshift deste laboratório não competem pelo mesmo problema: um resolve custo de consulta ad-hoc esparsa; o outro resolve latência de painel interativo sob concorrência. Os dois leem o mesmo lake em S3 — a escolha entre eles nunca é "qual é melhor", é "qual padrão de uso a peça precisa atender".
Perguntas frequentes
❓ Por que o Redshift é mais rápido que o Athena para o mesmo Parquet no S3?
❓ Redshift Spectrum é mais rápido que o Athena, já que os dois leem o mesmo S3?
❓ Como escolher a chave de distribuição com widgets de JOINs diferentes?
❓ Materialized view com autorrefresh substitui o CTAS agendado do L66?
❓ Vale a pena usar Redshift para todo painel de BI, já que ele é mais rápido que o Athena?
❓ Multi-AZ no Redshift Serverless protege contra perda de dado?
❓ Por que não só calibrar melhor o workgroup do Athena, em vez de trocar de motor?
Fixando
O painel mostra números que parecem corretos e o widget renderiza rápido — mas o time de compras percebe que os valores estão três dias desatualizados. O log não mostra nenhum erro. Qual é a causa mais provável, e como confirmar?
Uma equipe está decidindo entre manter o painel executivo no Athena (com o workgroup governado do L66) ou migrar para o Redshift deste laboratório. O uso real medido é: 2 acessos por semana, por 2 pessoas, sem exigência de tempo de resposta abaixo de alguns segundos. Qual é a decisão mais defensável?
Próximo módulo
Este laboratório resolveu latência de painel interativo trocando o motor de consulta — mas o dado que alimenta `mv_vendas_dia` ainda pode chegar errado sem que ninguém perceba: uma mudança de unidade, um campo nulo onde não deveria, um tipo trocado na origem. O `lab-contrato-dado-quarentena` (L70) trata exatamente disso — contrato de dado, validação e quarentena antes que o erro chegue ao painel executivo.
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…