Logo

Como Acelerar Relatórios em MySQL Usando Tabelas Sumarizadas: Do Bruto ao Enxuto

Transforme consultas lentas em respostas instantâneas através do pré-processamento inteligente de dados.

Publicado em 14 de novembro de 2025

Quando lidamos com tabelas transacionais gigantes — falamos de milhões ou até bilhões de linhas — a geração de relatórios analíticos se torna um pesadelo. Consultas que agregam dados por dia, mês, região, canal ou produto frequentemente varrem anos de histórico, resultando em queries que podem levar minutos ou até horas para responder.

O impacto vai além da lentidão: há consumo excessivo de CPU e I/O, travamento de janelas de relatório em horários de pico, e a frustração de descobrir que simplesmente adicionar mais índices ou hardware não resolve o problema de forma escalável.

O Problema Clássico: Consultas Diretas em Dados Transacionais

O erro mais comum em ambientes analíticos é tentar responder perguntas de alto nível — como "qual foi o faturamento por dia em cada canal?" ou "quais cidades geraram mais receita no último ano?" — lendo diretamente a tabela de nível mais detalhado (granular).

Imagine uma tabela de vendas com bilhões de registros, onde cada linha representa uma única transação. Toda vez que um usuário solicita um relatório mensal, o MySQL precisa:

  • Varrer milhões (ou bilhões) de linhas
  • Aplicar filtros de data
  • Agrupar por múltiplas dimensões
  • Calcular agregações (SUM, COUNT, AVG)

Mesmo com índices bem planejados, o custo computacional é enorme. E quando múltiplos usuários acessam relatórios simultaneamente, o servidor pode ficar completamente sobrecarregado.

A Solução: Pré-Processamento e Tabelas Sumarizadas

A abordagem mais eficiente para resolver esse problema é o pré-processamento de dados através de tabelas sumarizadas. A ideia central é simples e poderosa:

  • Pré-agregue os dados em janelas de tempo específicas (dia, semana, mês)
  • Armazene os resultados em tabelas de resumo otimizadas
  • Redirecione as consultas de relatório para ler essas tabelas resumidas, não a tabela original
  • Processe incrementalmente apenas os dados novos ou recentes

Em vez de recalcular tudo em tempo real, você troca processamento pesado online por um job periódico que pré-calcula os dados. O resultado? O usuário passa a consultar centenas ou milhares de linhas em vez de milhões.

Exemplo Prático: Tabela de Vendas

Vamos criar um cenário realista. Suponha que você tenha uma tabela de vendas com a seguinte estrutura:

CREATE TABLE vendas (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    data_venda DATETIME NOT NULL,
    cliente_id INT UNSIGNED NOT NULL,
    produto_id INT UNSIGNED NOT NULL,
    quantidade INT UNSIGNED NOT NULL,
    valor_unitario DECIMAL(10,2) NOT NULL,
    valor_total DECIMAL(12,2) NOT NULL,
    canal VARCHAR(50) NOT NULL,  -- online, loja, marketplace
    cidade VARCHAR(100) NOT NULL,
    estado CHAR(2) NOT NULL,
    INDEX idx_data_venda (data_venda),
    INDEX idx_canal (canal),
    INDEX idx_cidade (cidade)
) ENGINE=InnoDB;

Esta tabela cresce continuamente e já possui 500 milhões de linhas acumuladas ao longo de 3 anos.

A Query Ingênua (Problemática)

Agora imagine que o gerente de vendas solicita um relatório de faturamento por canal e cidade nos últimos 2 anos:

SELECT 
    canal,
    cidade,
    DATE_FORMAT(data_venda, '%Y-%m') AS mes,
    SUM(valor_total) AS faturamento,
    COUNT(*) AS total_vendas
FROM vendas
WHERE data_venda >= DATE_SUB(NOW(), INTERVAL 2 YEAR)
GROUP BY canal, cidade, mes
ORDER BY mes DESC, faturamento DESC;

Mesmo com índices, essa query pode levar de 30 segundos a vários minutos, dependendo do volume de dados e da carga no servidor.

Implementando a Tabela Sumarizada

Criando a Estrutura

Vamos criar uma tabela que armazena agregações mensais por canal e cidade:

CREATE TABLE vendas_sumarizadas_mes (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    ano_mes DATE NOT NULL,  -- primeiro dia do mês
    canal VARCHAR(50) NOT NULL,
    cidade VARCHAR(100) NOT NULL,
    estado CHAR(2) NOT NULL,
    faturamento DECIMAL(15,2) NOT NULL DEFAULT 0,
    total_vendas INT UNSIGNED NOT NULL DEFAULT 0,
    total_quantidade INT UNSIGNED NOT NULL DEFAULT 0,
    UNIQUE KEY uk_mes_canal_cidade (ano_mes, canal, cidade),
    INDEX idx_ano_mes (ano_mes),
    INDEX idx_canal (canal)
) ENGINE=InnoDB;
Carga Inicial (Backfill)

Primeiro, populamos a tabela sumarizada com o histórico completo:

INSERT INTO vendas_sumarizadas_mes 
    (ano_mes, canal, cidade, estado, faturamento, total_vendas, total_quantidade)
SELECT 
    DATE_FORMAT(data_venda, '%Y-%m-01') AS ano_mes,
    canal,
    cidade,
    estado,
    SUM(valor_total) AS faturamento,
    COUNT(*) AS total_vendas,
    SUM(quantidade) AS total_quantidade
FROM vendas
GROUP BY ano_mes, canal, cidade, estado
ON DUPLICATE KEY UPDATE
    faturamento = VALUES(faturamento),
    total_vendas = VALUES(total_vendas),
    total_quantidade = VALUES(total_quantidade);

Essa operação é executada uma única vez ou em uma janela de manutenção. Pode demorar, mas é um processo pontual.

Carga Incremental (Atualização Periódica)

Depois da carga inicial, criamos um procedimento para atualizar apenas os dados novos. Este processo pode rodar diariamente, de madrugada:

-- Atualizar dados do mês corrente e do mês anterior
-- (para cobrir vendas atrasadas ou ajustes)

INSERT INTO vendas_sumarizadas_mes 
    (ano_mes, canal, cidade, estado, faturamento, total_vendas, total_quantidade)
SELECT 
    DATE_FORMAT(data_venda, '%Y-%m-01') AS ano_mes,
    canal,
    cidade,
    estado,
    SUM(valor_total) AS faturamento,
    COUNT(*) AS total_vendas,
    SUM(quantidade) AS total_quantidade
FROM vendas
WHERE data_venda >= DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 1 MONTH), '%Y-%m-01')
GROUP BY ano_mes, canal, cidade, estado
ON DUPLICATE KEY UPDATE
    faturamento = VALUES(faturamento),
    total_vendas = VALUES(total_vendas),
    total_quantidade = VALUES(total_quantidade);

Este processo leva apenas alguns segundos, pois processa apenas os últimos 30-60 dias de dados.

A Query Otimizada

Agora, a mesma consulta de relatório fica dramaticamente mais rápida:

SELECT 
    canal,
    cidade,
    DATE_FORMAT(ano_mes, '%Y-%m') AS mes,
    faturamento,
    total_vendas
FROM vendas_sumarizadas_mes
WHERE ano_mes >= DATE_SUB(NOW(), INTERVAL 2 YEAR)
ORDER BY ano_mes DESC, faturamento DESC;

Em vez de varrer 500 milhões de linhas, a query agora acessa apenas algumas centenas ou milhares de linhas na tabela sumarizada. O tempo de resposta cai de minutos para milissegundos.

Métrica Query Direta (Tabela Original) Query com Tabela Sumarizada
Linhas Escaneadas ~350 milhões ~2.400 (24 meses × 100 combinações)
Tempo de Resposta 45-180 segundos 0.05-0.2 segundos
Uso de CPU Alto (80-100%) Mínimo (<5%)
Impacto no Sistema Bloqueia outros processos Imperceptível

Combinando Dados Sumarizados com Dados em Tempo Real

Um desafio comum surge quando a tabela transacional é extremamente dinâmica — recebe inserções a cada segundo — e você precisa que o relatório inclua os dados mais recentes, mesmo aqueles que ainda não foram processados pelo job de sumarização.

A solução é uma query híbrida que combina:

  • Dados históricos da tabela sumarizada
  • Dados recentes (últimas horas ou dia atual) da tabela original
Estratégia de Implementação

Vamos supor que o job de sumarização roda a cada noite e processa tudo até o dia anterior. Durante o dia, precisamos incluir as vendas do dia atual nos relatórios.

-- Parte 1: Dados históricos sumarizados (até ontem)
SELECT 
    canal,
    cidade,
    DATE_FORMAT(ano_mes, '%Y-%m') AS mes,
    SUM(faturamento) AS faturamento,
    SUM(total_vendas) AS total_vendas
FROM vendas_sumarizadas_mes
WHERE ano_mes >= DATE_SUB(NOW(), INTERVAL 2 YEAR)
  AND ano_mes < DATE_FORMAT(CURDATE(), '%Y-%m-01')
GROUP BY canal, cidade, mes

UNION ALL

-- Parte 2: Dados do mês corrente (ainda não sumarizados)
SELECT 
    canal,
    cidade,
    DATE_FORMAT(data_venda, '%Y-%m') AS mes,
    SUM(valor_total) AS faturamento,
    COUNT(*) AS total_vendas
FROM vendas
WHERE data_venda >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
GROUP BY canal, cidade, mes;

Esta query é eficiente porque:

  • A maior parte dos dados (99%+) vem da tabela sumarizada (rápida)
  • Apenas o mês corrente é processado em tempo real (volume pequeno)
  • O UNION ALL combina os resultados sem custo significativo

Boas Práticas e Considerações

Granularidade Adequada

Escolha o nível de agregação baseado nas necessidades do negócio:

  • Diária: para análises detalhadas e dashboards operacionais
  • Semanal: para relatórios de médio prazo
  • Mensal: para análises estratégicas e tendências

Você pode manter múltiplas tabelas sumarizadas com diferentes granularidades.

Retenção de Dados

Defina políticas claras de retenção sempre que for possível para a sua aplicação. Por exemplo:

  • Dados detalhados (tabela original): últimos 12 meses
  • Sumarizações diárias: últimos 24 meses
  • Sumarizações mensais: histórico completo
Monitoramento

Monitore a execução dos jobs de sumarização:

-- Criar tabela de log
CREATE TABLE log_sumarizacao (
    id INT AUTO_INCREMENT PRIMARY KEY,
    data_processamento DATETIME DEFAULT CURRENT_TIMESTAMP,
    periodo_inicio DATE,
    periodo_fim DATE,
    linhas_processadas INT,
    tempo_execucao_segundos INT,
    status ENUM('sucesso', 'erro') DEFAULT 'sucesso',
    mensagem TEXT
) ENGINE=InnoDB;

Conclusão

Tabelas sumarizadas são uma técnica poderosa e comprovada para acelerar relatórios analíticos em MySQL. Ao trocar processamento pesado em tempo real por pré-agregações periódicas, você obtém:

  • Redução drástica no tempo de resposta (de minutos para milissegundos)
  • Menor consumo de recursos do servidor
  • Melhor experiência do usuário
  • Escalabilidade sustentável mesmo com crescimento exponencial de dados

A estratégia híbrida — combinando dados históricos sumarizados com dados recentes em tempo real — oferece o melhor dos dois mundos: performance excepcional sem sacrificar a atualidade das informações. Com automação através de eventos MySQL e boas práticas de monitoramento, você cria um sistema robusto que cresce junto com seu negócio.

Referências