Boas práticas de design de banco de dados para escalabilidade
1. Fundamentos do design para escalabilidade
1.1. Diferença entre escalabilidade vertical e horizontal
A escalabilidade vertical (scale-up) consiste em adicionar mais recursos a um único servidor — mais CPU, RAM ou armazenamento mais rápido. Embora simples de implementar, possui limites físicos e financeiros. A escalabilidade horizontal (scale-out) distribui a carga entre múltiplos servidores, permitindo crescimento quase ilimitado, mas exige design cuidadoso para lidar com consistência distribuída.
Exemplo de estratégia híbrida:
- Escalabilidade vertical: Aumentar RAM de 32GB para 128GB em um nó master
- Escalabilidade horizontal: Adicionar 3 réplicas de leitura distribuídas geograficamente
1.2. Princípios de normalização vs. desnormalização estratégica
A normalização (3FN) reduz redundância, mas pode gerar joins custosos. A desnormalização estratégica introduz redundância controlada para evitar joins em consultas críticas.
Cenário de e-commerce:
Tabela normalizada:
pedidos (id, cliente_id, data)
itens_pedido (pedido_id, produto_id, quantidade, preco_unitario)
Tabela desnormalizada para consulta de carrinho:
pedidos_com_itens (pedido_id, cliente_id, produto_id, quantidade, preco_unitario, data)
-- Consulta mais rápida, mas requer sincronização cuidadosa
1.3. Identificação de gargalos comuns em bancos de dados relacionais
Os gargalos mais frequentes incluem: consultas sem índices adequados, locks excessivos em tabelas de alta concorrência, I/O de disco insuficiente e queries que varrem tabelas inteiras.
Métrica crítica para monitoramento:
- Slow query log: tempo médio > 100ms
- Lock wait ratio: > 5% do tempo total de transação
- Cache hit ratio: < 95% para dados frequentemente acessados
2. Modelagem de dados eficiente
2.1. Uso de índices compostos e seletivos para consultas frequentes
Índices compostos devem seguir a ordem das colunas no WHERE, priorizando colunas com alta seletividade.
CREATE INDEX idx_pedidos_cliente_data
ON pedidos (cliente_id, data_criacao DESC);
-- Consulta beneficiada:
SELECT * FROM pedidos
WHERE cliente_id = 12345
ORDER BY data_criacao DESC
LIMIT 10;
2.2. Particionamento de tabelas (range, list, hash) semântico
O particionamento horizontal divide tabelas grandes em partes menores, melhorando manutenção e performance.
-- Particionamento por range (data):
CREATE TABLE logs (
id BIGINT,
data TIMESTAMP,
mensagem TEXT
) PARTITION BY RANGE (YEAR(data)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p_futuro VALUES LESS THAN MAXVALUE
);
-- Particionamento por hash (distribuição uniforme):
CREATE TABLE usuarios (
id BIGINT,
nome VARCHAR(100)
) PARTITION BY HASH (id) PARTITIONS 4;
2.3. Estratégias de chaves primárias e surrogadas para alto throughput
Chaves surrogadas (auto-incremento ou UUID) evitam problemas de chaves naturais que mudam. UUIDs são melhores para sharding, mas consomem mais espaço.
-- Chave surrogada com UUID para distribuição:
CREATE TABLE transacoes (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
usuario_id BIGINT,
valor DECIMAL(10,2),
criado_em TIMESTAMP DEFAULT NOW()
);
-- Índice para consultas por usuário:
CREATE INDEX idx_transacoes_usuario
ON transacoes (usuario_id, criado_em DESC);
3. Otimização de consultas e transações
3.1. Evitar locks longos e deadlocks com transações curtas
Transações longas aumentam a probabilidade de deadlocks. Mantenha transações atômicas e rápidas.
-- Ruim: transação longa
BEGIN;
SELECT * FROM contas WHERE id = 1 FOR UPDATE;
-- ... processamento lento (2 segundos)
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
COMMIT;
-- Bom: transação curta
BEGIN;
UPDATE contas SET saldo = saldo - 100
WHERE id = 1 AND saldo >= 100;
COMMIT;
3.2. Uso de consultas preparadas e planos de execução previsíveis
Consultas preparadas evitam recompilação do plano de execução e previnem injeção SQL.
-- Preparação da consulta:
PREPARE busca_usuario (INT) AS
SELECT nome, email FROM usuarios WHERE id = $1;
-- Execução repetida:
EXECUTE busca_usuario(101);
EXECUTE busca_usuario(202);
3.3. Técnicas de leitura consistente vs. leitura eventual para cargas altas
Para cargas altas, use leitura eventual em réplicas para dados não críticos e leitura consistente no master para transações financeiras.
-- Leitura consistente (master):
SELECT saldo FROM contas WHERE id = 1;
-- Leitura eventual (réplica, para relatórios):
-- Permite dados ligeiramente desatualizados
SELECT SUM(valor) FROM transacoes WHERE data > '2024-01-01';
4. Estratégias de caching integradas ao design
4.1. Cache de consulta no banco de dados vs. cache em aplicação
O cache no banco (query cache) é automático, mas pode causar contenção. Cache em aplicação (Redis) oferece mais controle.
-- Configuração de query cache no MySQL:
SET GLOBAL query_cache_size = 67108864; -- 64MB
SET GLOBAL query_cache_type = 1;
-- Exemplo de cache em aplicação (pseudo-código):
function getUsuario(id) {
cacheKey = "usuario:" + id;
if (redis.exists(cacheKey)) return redis.get(cacheKey);
usuario = db.query("SELECT * FROM usuarios WHERE id = ?", [id]);
redis.setex(cacheKey, 3600, usuario); // TTL de 1 hora
return usuario;
}
4.2. Invalidação de cache baseada em eventos de escrita
Invalide o cache imediatamente após escritas para evitar dados obsoletos.
function atualizarUsuario(id, novosDados) {
db.query("UPDATE usuarios SET nome = ? WHERE id = ?",
[novosDados.nome, id]);
redis.del("usuario:" + id); // Invalidação imediata
}
4.3. Cache distribuído (Redis/Memcached) como camada de aceleração
Use cache distribuído para reduzir carga no banco principal, especialmente para dados de acesso frequente.
-- Configuração de cluster Redis:
redis-cli --cluster create 192.168.1.1:6379 192.168.1.2:6379 \
192.168.1.3:6379 --cluster-replicas 1
-- Cache de sessão de usuário:
redis.setex("sessao:" + token, 1800, JSON.stringify(dadosUsuario));
5. Replicação e distribuição de dados
5.1. Replicação mestre-escravo para leitura escalável
Distribua consultas de leitura entre réplicas, mantendo escritas no master.
-- Configuração no MySQL:
CHANGE MASTER TO
MASTER_HOST='192.168.1.10',
MASTER_USER='replicador',
MASTER_PASSWORD='senha_segura',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS= 107;
START SLAVE;
-- Roteamento de consultas:
function executeQuery(sql, isWrite) {
if (isWrite) return master.query(sql);
return replicaPool.query(sql); // Round-robin entre réplicas
}
5.2. Sharding horizontal por chave de distribuição (ex: ID de usuário)
O sharding divide dados entre múltiplos bancos, cada um responsável por um subconjunto.
-- Função de roteamento baseada em hash do user_id:
function getShard(userId) {
return "shard_" + (userId % 4); // 4 shards
}
-- Consulta com roteamento:
SELECT * FROM pedidos WHERE user_id = 12345;
-- Roteado para: shard_1 (12345 % 4 = 1)
5.3. Roteamento de consultas em ambientes multi-nó
Use um proxy de banco de dados (ProxySQL, HAProxy) para rotear consultas automaticamente.
-- Configuração do ProxySQL:
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES
(0, '192.168.1.10', 3306), -- Master (hostgroup 0)
(1, '192.168.1.11', 3306), -- Réplica 1 (hostgroup 1)
(1, '192.168.1.12', 3306); -- Réplica 2 (hostgroup 1)
-- Regra de roteamento:
INSERT INTO mysql_query_rules
(rule_id, active, match_pattern, destination_hostgroup) VALUES
(1, 1, '^SELECT', 1), -- SELECTs vão para réplicas
(2, 1, '^.*', 0); -- Demais comandos vão para master
6. Monitoramento e manutenção preventiva
6.1. Métricas essenciais: latência, taxa de cache hit, locks e deadlocks
Monitore continuamente para detectar degradação antes que afete usuários.
-- Consultas de monitoramento:
-- Latência média de consultas:
SELECT AVG(query_time) FROM information_schema.processlist;
-- Taxa de cache hit (InnoDB):
SELECT
(1 - (innodb_buffer_pool_reads /
(innodb_buffer_pool_reads + innodb_buffer_pool_read_requests))
) * 100 AS buffer_hit_ratio
FROM performance_schema.global_status;
-- Deadlocks recentes:
SHOW ENGINE INNODB STATUS\G
6.2. Estratégias de backup e recuperação para grandes volumes
Para grandes volumes, use backups incrementais e replicação como estratégia de recuperação.
-- Backup físico com XtraBackup (MySQL):
xtrabackup --backup --target-dir=/backups/full/$(date +%Y%m%d)
-- Backup incremental:
xtrabackup --backup --target-dir=/backups/incr/$(date +%Y%m%d) \
--incremental-basedir=/backups/full/$(date +%Y%m%d -d yesterday)
-- PITR (Point-in-Time Recovery):
mysqlbinlog --start-datetime="2024-01-15 10:00:00" \
--stop-datetime="2024-01-15 10:30:00" \
/var/log/mysql/mysql-bin.000001 | mysql -u root
6.3. Análise proativa de crescimento e rebalanceamento de nós
Antecipe necessidades de capacidade com análise de tendências.
-- Análise de crescimento por mês:
SELECT
DATE_TRUNC('month', data_criacao) AS mes,
COUNT(*) AS total_pedidos,
COUNT(*) * AVG(tamanho_linha) / 1024 / 1024 AS crescimento_mb
FROM pedidos
WHERE data_criacao > NOW() - INTERVAL '6 months'
GROUP BY mes
ORDER BY mes;
-- Script de rebalanceamento de shard:
function rebalanceShard(fromShard, toShard, percentage) {
// Migrar % dos dados de fromShard para toShard
// Durante migração, manter ambos shards ativos
// Atualizar função de roteamento após conclusão
}
Referências
- PostgreSQL Documentation: Performance Tuning — Guia oficial de ajuste de performance para PostgreSQL, incluindo índices, particionamento e configuração de memória.
- MySQL 8.0 Reference Manual: Optimization — Documentação oficial do MySQL sobre otimização de consultas, índices e estratégias de replicação.
- Redis Documentation: Scaling — Guia completo sobre clustering e sharding no Redis para cache distribuído escalável.
- MongoDB University: Data Modeling for Scalability — Curso gratuito sobre modelagem de dados NoSQL para escalabilidade horizontal.
- AWS Database Blog: Best Practices for Sharding — Artigo técnico sobre implementação de sharding em bancos relacionais na nuvem.
- Use the Index, Luke! — Guia prático e visual sobre índices de banco de dados, com exemplos em múltiplos SGBDs.
- High Scalability: Database Scaling Patterns — Artigo clássico sobre padrões de escalabilidade de banco de dados, incluindo replicação e sharding.