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