Tenho muitos dados, posso usar MySQL? Limites reais e como escalar
Bilhões de linhas em MySQL: onde estão os limites de verdade, por que o problema quase nunca é o tamanho da tabela, e as técnicas que uso — índices, particionamento, arquivamento e sharding.

"Ale, minha tabela vai passar de 500 milhões de linhas. MySQL aguenta?" Aguenta. O Facebook rodou (e roda) MySQL em escala que faria a sua tabela parecer uma planilha. A pergunta certa não é se aguenta — é o que você vai precisar fazer para que aguente bem.
Os limites formais (que você nunca vai alcançar)
| Limite | Valor |
|---|---|
| Tamanho de uma tabela InnoDB | 64 TB (com página de 16 KB) |
| Linhas por tabela | Sem limite lógico do MySQL |
| Colunas por tabela | 4096 (na prática ~1017 no InnoDB) |
| Índices por tabela | 64 |
Ou seja: o teto do produto não é o seu problema. O seu problema é memória, I/O e desenho de índice.
O conceito que muda tudo: working set
O MySQL não precisa ter a tabela inteira na RAM. Ele precisa ter na RAM as páginas que você realmente acessa. Uma tabela de 2 TB em que 95% das consultas tocam os últimos 30 dias tem um working set de talvez 40 GB. Isso cabe num servidor comum.
Como medir se o seu buffer pool está sofrendo:
SELECT
ROUND(@@innodb_buffer_pool_size/1024/1024/1024, 1) AS pool_gb,
ROUND(100 - (
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME='Innodb_buffer_pool_reads') /
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME='Innodb_buffer_pool_read_requests') * 100
), 3) AS hit_ratio_pct;
Abaixo de 99% de hit ratio em carga OLTP, comece a se preocupar. Abaixo de 95%, o seu disco virou o seu banco de dados.
Passo 1: índice antes de qualquer arquitetura
Eu perdi a conta de quantos projetos queriam sharding e precisavam de um índice composto. Antes de mexer em topologia, ache as consultas que varrem a tabela inteira:
SELECT query, exec_count, rows_examined_avg, rows_sent_avg,
ROUND(total_latency/1e12, 2) AS total_seg
FROM sys.statement_analysis
WHERE rows_examined_avg > 10000
ORDER BY total_latency DESC
LIMIT 10;
-- Índices que nunca foram usados (custam escrita e espaço)
SELECT * FROM sys.schema_unused_indexes;
Índice composto segue a regra da ordem: filtro de igualdade primeiro, faixa depois, ordenação por último. (cliente_id, criado_em) serve um WHERE cliente_id = ? ORDER BY criado_em DESC. O inverso não serve.
Passo 2: particionamento por data
Para tabelas históricas, partição por RANGE em data é o melhor custo-benefício que existe: o otimizador poda partições inteiras e você apaga meses antigos com DROP PARTITION — instantâneo, sem o inferno de um DELETE de 200 milhões de linhas.
CREATE TABLE eventos (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
usuario_id BIGINT UNSIGNED NOT NULL,
tipo VARCHAR(40) NOT NULL,
criado_em DATETIME NOT NULL,
PRIMARY KEY (id, criado_em),
KEY idx_usuario (usuario_id, criado_em)
) ENGINE=InnoDB
PARTITION BY RANGE (TO_DAYS(criado_em)) (
PARTITION p2026_01 VALUES LESS THAN (TO_DAYS('2026-02-01')),
PARTITION p2026_02 VALUES LESS THAN (TO_DAYS('2026-03-01')),
PARTITION p2026_03 VALUES LESS THAN (TO_DAYS('2026-04-01')),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
-- Expurgo do mês mais antigo: segundos, não horas
ALTER TABLE eventos DROP PARTITION p2026_01;
Detalhe que pega muita gente: no MySQL, toda chave única — inclusive a primária — precisa conter a coluna de particionamento. Por isso o PRIMARY KEY (id, criado_em) acima.
Passo 3: arquivamento e leitura em réplicas
Dado morno vai para tabela de histórico ou para o data lake. Relatório pesado vai para réplica de leitura, nunca para o primário. Essa separação sozinha já devolve fôlego para o OLTP — e é o assunto de MySQL em aplicações críticas e escaláveis.
Passo 4: sharding (só quando doer de verdade)
Sharding é dividir os dados em vários servidores por uma chave (cliente, região, hash do id). Resolve escrita que estourou um servidor só — e cobra caro: você perde JOIN entre shards, transação distribuída vira problema seu, e rebalancear é projeto. Vá para sharding quando índice, partição e réplica já estiverem esgotados, não antes.
Quando MySQL realmente não é a resposta
- Agregações analíticas sobre dezenas de TB o tempo todo: use engine colunar (ClickHouse, BigQuery) alimentada pelo MySQL.
- Séries temporais com ingestão massiva contínua: TimescaleDB ou InfluxDB entregam mais por menos.
- Busca textual sofisticada em escala: OpenSearch ao lado, com o MySQL como fonte da verdade.
Fora esses casos, "muitos dados" quase sempre significa "índice errado e buffer pool pequeno". Confira o panorama completo no guia definitivo de MySQL e, se a dúvida era de plataforma, veja MySQL ou PostgreSQL.
Sobre o autor: Alexandre Almeida atua com bancos de dados há mais de 25 anos, foi instrutor oficial de MySQL e Oracle e escreveu Aprendendo MySQL, onde há laboratórios guiados de índices, EXPLAIN e particionamento em máquina virtual pronta. Comandos testados em MySQL 8.4.
Fontes: MySQL 8.4 Reference Manual (InnoDB Limits, Partitioning, sys Schema) e experiência em ambientes de produção com tabelas acima de 1 bilhão de linhas.