Views materializadas: cache de consultas pesadas
1. O que são Views Materializadas?
Views materializadas são objetos de banco de dados que armazenam fisicamente o resultado de uma consulta, funcionando como um cache persistente no disco. Diferentemente das views comuns, que são apenas consultas virtuais executadas a cada acesso, as views materializadas mantêm uma cópia real dos dados em um snapshot específico.
A analogia com cache é perfeita: assim como um cache em memória acelera acessos repetidos, uma view materializada evita que consultas complexas sejam recalculadas a cada execução. Enquanto uma view comum é como uma janela que mostra o jardim em tempo real, uma view materializada é como uma fotografia impressa daquele jardim em um determinado momento.
Quando utilizar:
- Consultas agregadas pesadas que processam milhões de linhas
- Relatórios complexos com múltiplos JOINs e funções de agregação
- Dashboards que exigem respostas rápidas (subsegundos)
- Cenários onde a latência de dados de alguns minutos é aceitável
2. Sintaxe Básica e Criação
A sintaxe para criar uma view materializada segue o padrão SQL com a cláusula CREATE MATERIALIZED VIEW. Vamos a um exemplo prático:
CREATE MATERIALIZED VIEW vendas_mensais AS
SELECT
DATE_TRUNC('month', data_venda) AS mes,
categoria,
SUM(valor) AS total_vendas,
COUNT(*) AS quantidade_vendas,
AVG(valor) AS ticket_medio
FROM vendas
JOIN produtos ON vendas.produto_id = produtos.id
WHERE data_venda >= '2024-01-01'
GROUP BY DATE_TRUNC('month', data_venda), categoria
ORDER BY mes, categoria;
Esta view materializada pré-calcula agregações mensais de vendas por categoria. Sem ela, uma consulta que levasse 30 segundos em uma tabela de 10 milhões de registros passa a responder em milissegundos.
Opções de criação:
-- Cria com dados (padrão)
CREATE MATERIALIZED VIEW exemplo WITH DATA AS SELECT ...;
-- Cria vazia (apenas estrutura)
CREATE MATERIALIZED VIEW exemplo WITH NO DATA AS SELECT ...;
A opção WITH NO DATA é útil para criar a estrutura sem executar a consulta imediatamente, permitindo preparar índices antes de popular os dados.
3. Atualização dos Dados: Refresh
O comando REFRESH MATERIALIZED VIEW é responsável por atualizar os dados armazenados:
REFRESH MATERIALIZED VIEW vendas_mensais;
Este comando bloqueia a view completamente durante a execução, impedindo leituras simultâneas. Para ambientes de produção que exigem alta disponibilidade, existe a opção CONCURRENTLY:
-- Requer um índice único na view
CREATE UNIQUE INDEX idx_vendas_mensais_unique
ON vendas_mensais(mes, categoria);
REFRESH MATERIALIZED VIEW CONCURRENTLY vendas_mensais;
A opção CONCURRENTLY cria uma versão temporária da view, realiza o refresh em segundo plano e troca as versões atomicamente, permitindo consultas durante o processo. A contrapartida é que o refresh fica mais lento e consome mais recursos.
Estratégias de agendamento com pg_cron (PostgreSQL):
-- Agendar refresh a cada hora
SELECT cron.schedule('refresh-vendas', '0 * * * *',
'REFRESH MATERIALIZED VIEW CONCURRENTLY vendas_mensais');
4. Índices em Views Materializadas
Criar índices em views materializadas é fundamental para performance, especialmente quando a view é grande. A view se comporta como uma tabela física e se beneficia dos mesmos mecanismos de indexação.
-- Índice único (obrigatório para CONCURRENTLY)
CREATE UNIQUE INDEX idx_vendas_mes_categoria
ON vendas_mensais(mes, categoria);
-- Índice composto para consultas frequentes
CREATE INDEX idx_vendas_categoria_mes
ON vendas_mensais(categoria, mes);
-- Índice em coluna de agregação
CREATE INDEX idx_vendas_total
ON vendas_mensais(total_vendas DESC);
Os índices aceleram consultas que filtram ou ordenam pelos campos indexados, transformando scans sequenciais em buscas por índice.
5. Vantagens e Casos de Uso
Redução drástica de tempo: Uma consulta que processa 5 milhões de registros e leva 45 segundos pode ser respondida em 50ms através de uma view materializada.
Cenários típicos:
- BI e relatórios: Relatórios de final de mês que agregam dados de vendas, estoque e financeiro
- KPIs de painéis: Métricas de SLA, churn rate, receita recorrente mensal
- Isolamento de carga: Consultas pesadas não impactam tabelas OLTP transacionais
-- Exemplo: dashboard de vendas que consulta a view materializada
SELECT mes, total_vendas, ticket_medio
FROM vendas_mensais
WHERE categoria = 'Eletrônicos'
ORDER BY mes DESC;
6. Limitações e Cuidados
Dados não são tempo real: Entre refreshes, os dados ficam desatualizados. Para tabelas com alta taxa de atualização, a defasagem pode ser problemática.
Consumo de armazenamento: Uma view materializada ocupa espaço físico equivalente ao resultado da consulta. Para tabelas de 100GB, a view pode ocupar dezenas de GB.
Custo de manutenção: O refresh pode ser pesado. Em tabelas com 50 milhões de registros, um refresh completo pode levar minutos e consumir I/O intensivo.
-- Monitorar tamanho da view
SELECT pg_size_pretty(pg_total_relation_size('vendas_mensais'));
-- Verificar tempo do último refresh
SELECT * FROM pg_stat_user_tables
WHERE relname = 'vendas_mensais';
7. Comparação com Alternativas
| Característica | View Comum | View Materializada | Cache (Redis) |
|---|---|---|---|
| Dados em tempo real | Sim | Não (até refresh) | Depende |
| Performance | Baixa | Alta | Altíssima |
| Persistência | Nenhuma | Disco | Memória |
| Complexidade | Baixa | Média | Alta (integração) |
Quando usar cada alternativa:
- View comum: Consultas simples que precisam de dados em tempo real
- View materializada: Agregações pesadas com latência aceitável
- Tabela temporária: Processos batch que precisam de dados intermediários
- Cache em aplicação: Dados críticos que exigem latência sub-milissegundo
8. Boas Práticas e Monitoramento
Definir frequência de refresh:
- Minutal: Para dashboards operacionais
- Horária: Para relatórios de turno
- Diária: Para análises estratégicas
Monitoramento contínuo:
-- Tempo médio de refresh
SELECT relname,
n_tup_ins + n_tup_upd + n_tup_del AS total_modifications,
last_autoanalyze,
last_autovacuum
FROM pg_stat_user_tables
WHERE relname LIKE '%materialized%';
-- Tamanho das views materializadas
SELECT schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename))
FROM pg_tables
WHERE tablename LIKE '%materialized%';
Limpeza de views não utilizadas: Revise periodicamente quais views materializadas estão sendo acessadas. Views não utilizadas consomem espaço e recurso de refresh desnecessariamente.
Boas práticas adicionais:
- Sempre criar índices únicos para permitir CONCURRENTLY
- Documentar a frequência de refresh e a latência esperada
- Considerar partição da view original para refreshes incrementais
- Testar o impacto do refresh em horários de baixa carga
Views materializadas são ferramentas poderosas para otimizar consultas pesadas, transformando minutos de processamento em respostas instantâneas. O segredo está em equilibrar frescor dos dados com performance, escolhendo a frequência de refresh adequada para cada caso de uso.
Referências
- PostgreSQL Documentation: CREATE MATERIALIZED VIEW — Documentação oficial do PostgreSQL sobre criação e gerenciamento de views materializadas
- PostgreSQL Documentation: REFRESH MATERIALIZED VIEW — Documentação oficial sobre o comando REFRESH e opção CONCURRENTLY
- Use the Index, Luke: Materialized Views — Guia prático sobre otimização de views materializadas com índices
- Percona Blog: Materialized Views in PostgreSQL — Artigo técnico sobre performance e estratégias de refresh em produção
- PostgreSQL Tutorial: Materialized Views — Tutorial passo a passo com exemplos práticos de criação e manutenção
- Citus Data: Materialized Views and Performance — Análise de performance e casos de uso reais com views materializadas em bancos distribuídos