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