Índices compostos no banco do MU Online: como acabar com queries lentas
Diagnostique e resolva queries lentas no banco de dados do seu servidor de MU Online usando índices compostos bem projetados, do EXPLAIN ao monitoramento contínuo de performance.
Todo servidor de MU Online que cresce em jogadores online simultâneos eventualmente esbarra no mesmo gargalo: o banco de dados. Login lento, ranking que demora segundos para carregar, guild que trava ao abrir a lista de membros — na maioria das vezes a causa não é hardware fraco, é ausência de índic
Todo servidor de MU Online que cresce em jogadores online simultâneos eventualmente esbarra no mesmo gargalo: o banco de dados. Login lento, ranking que demora segundos para carregar, guild que trava ao abrir a lista de membros — na maioria das vezes a causa não é hardware fraco, é ausência de índices adequados nas tabelas certas. Um índice composto bem desenhado transforma uma varredura de milhões de linhas em uma busca direta de milissegundos. Este tutorial mostra como identificar queries lentas no MSSQL/MySQL usado pelo seu MuServer, como projetar índices compostos para os padrões de consulta mais comuns (login, ranking, inventário, guild) e como validar o ganho real com EXPLAIN/plano de execução, sem cair na armadilha de indexar tudo e piorar a escrita.
Por que o banco do MU Online sofre com queries lentas
A maioria dos emuladores (MuEmu, IGCN, OpenMU, GVault) usa um schema relacional clássico: tabelas de Character, Item, Guild, AccountCharacter, MEMB_STAT, entre outras, com dezenas de colunas e, com o tempo, milhões de linhas — principalmente Item (todo item de todo personagem) e logs de eventos. Sem índice adequado, uma consulta que filtra por AccountID e CharacterName faz um table scan completo, lendo linha por linha. Em 50 mil contas isso ainda passa despercebido; em 500 mil contas o login trava.
Diagnosticando: como encontrar as queries realmente lentas
Não adivinhe. Ative o registro de consultas lentas antes de criar qualquer índice:
-- SQL Server: habilitar Query Store no banco do MuServer
ALTER DATABASE MuOnline SET QUERY_STORE = ON;
ALTER DATABASE MuOnline SET QUERY_STORE (OPERATION_MODE = READ_WRITE);
; MySQL: my.cnf
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.5
Depois de algumas horas com jogadores online, revise o log ou o Query Store por queries com maior tempo médio × maior frequência — não pelo maior tempo isolado. Uma query de 50ms rodada 10 mil vezes por minuto pesa mais que uma de 2s rodada uma vez por hora.
Lendo o plano de execução (EXPLAIN)
EXPLAIN SELECT * FROM Character WHERE AccountID = 'player01' AND Ctl1Code = 0;
Procure por type: ALL (MySQL) ou Table Scan/Clustered Index Scan (SQL Server) — isso indica que o banco está lendo a tabela inteira. O objetivo é ver type: ref/range ou Index Seek, sinal de que um índice está sendo usado de forma seletiva.
O que é um índice composto e quando ele resolve o problema
Um índice composto ordena fisicamente os dados por mais de uma coluna, na ordem declarada. Ele resolve buscas que filtram por essas colunas juntas, e também buscas que filtram apenas pelo prefixo (a primeira coluna, ou as primeiras N colunas). A ordem das colunas importa: um índice (AccountID, CharacterName) acelera WHERE AccountID = X e WHERE AccountID = X AND CharacterName = Y, mas não acelera sozinho um WHERE CharacterName = Y isolado.
Mapeando as queries mais comuns do servidor de MU
| Cenário | Query típica | Colunas do índice composto |
|---|---|---|
| Login de conta | WHERE AccountID = ? AND Password = ? | (AccountID, Password) |
| Lista de personagens da conta | WHERE AccountID = ? ORDER BY Ctl1Code | (AccountID, Ctl1Code) |
| Ranking por Resets/Level | WHERE Ctl1Code = 0 ORDER BY Resets DESC, cLevel DESC | (Ctl1Code, Resets DESC, cLevel DESC) |
| Itens de um personagem | WHERE AccountID = ? AND Name = ? | (AccountID, Name) |
| Membros de uma guild | WHERE G_Name = ? ORDER BY G_Status | (G_Name, G_Status) |
| Log de trade/GM por período | WHERE Date BETWEEN ? AND ? AND AccountID = ? | (AccountID, Date) |
Passo a passo para criar um índice composto
-- SQL Server
CREATE NONCLUSTERED INDEX IX_Character_Account_Ctl1
ON Character (AccountID, Ctl1Code)
INCLUDE (Name, cLevel, Class);
-- MySQL
CREATE INDEX idx_character_account_ctl1
ON Character (AccountID, Ctl1Code);
O INCLUDE no SQL Server é importante: ele adiciona colunas ao índice sem torná-las parte da chave de busca, permitindo que a query seja respondida inteiramente pelo índice (covering index), sem voltar à tabela original.
Ordenando as colunas corretamente (seletividade primeiro)
Regra prática: coloque primeiro a coluna mais seletiva — a que mais reduz o conjunto de resultados. AccountID é altamente seletivo (poucas linhas por conta); Ctl1Code (que geralmente indica personagem ativo/excluído) tem só 2-3 valores possíveis, então deve vir depois, não antes. Um índice (Ctl1Code, AccountID) seria muito menos eficiente que (AccountID, Ctl1Code).
Índices para ORDER BY e ranking
Sites de ranking (como a própria página pública do ViciadosMU) fazem ORDER BY Resets DESC, cLevel DESC toda vez que alguém acessa. Sem índice, isso é uma ordenação completa em memória a cada requisição. Um índice composto que já contempla a ordem de classificação evita o sort no plano de execução:
CREATE NONCLUSTERED INDEX IX_Character_Ranking
ON Character (Ctl1Code, Resets DESC, cLevel DESC)
INCLUDE (Name, Class);
Isso é especialmente crítico se o ranking web consulta o banco diretamente (sem cache) a cada carregamento de página — considere também um cache de 1-5 minutos no lado da aplicação além do índice.
Cuidados com escrita e manutenção
Cada índice adicional custa CPU e I/O extra em todo INSERT, UPDATE e DELETE na tabela. Em Item, tabela que recebe escrita constante (drop, trade, refino), criar índices demais deixa o loop de save do GameServer mais lento. Diretriz prática:
- Crie no máximo 3-5 índices por tabela de alta escrita.
- Revise mensalmente quais índices nunca são usados (
sys.dm_db_index_usage_statsno SQL Server) e remova-os. - Rode
UPDATE STATISTICS(SQL Server) ouANALYZE TABLE(MySQL) periodicamente — estatísticas desatualizadas fazem o otimizador escolher planos ruins mesmo com índice presente.
Índices parciais/filtrados para reduzir tamanho
Se 90% das linhas de Character têm Ctl1Code = 0 (ativo) e você só consulta personagens ativos, um índice filtrado é menor e mais rápido:
CREATE NONCLUSTERED INDEX IX_Character_Active
ON Character (AccountID, Name)
WHERE Ctl1Code = 0;
Isso reduz o tamanho do índice e melhora o cache hit ratio, já que menos páginas precisam ficar em memória.
Validando o ganho antes e depois
Sempre compare o plano de execução antes e depois de criar o índice, e meça o tempo real:
SET STATISTICS TIME ON;
SELECT * FROM Character WHERE AccountID = 'player01' ORDER BY Resets DESC;
SET STATISTICS TIME OFF;
Um ganho típico ao sair de table scan para index seek em uma tabela de 2 milhões de linhas é de segundos para menos de 50ms. Se o ganho não aparecer, revise se a query realmente usa as colunas do índice na cláusula WHERE/ORDER BY na ordem certa.
Monitoramento contínuo
Configure um job (SQL Server Agent ou cron + script) que rode semanalmente e reporte: queries mais lentas da semana, índices não utilizados, fragmentação de índice acima de 30%. Fragmentação alta pede REORGANIZE (10-30%) ou REBUILD (acima de 30%). Sem essa rotina, o banco degrada de forma silenciosa conforme a base de jogadores cresce.
Erros comuns e soluções
| Sintoma | Causa provável | Solução |
|---|---|---|
| Login lento apenas em horário de pico | Table scan em Character/AccountCharacter sob concorrência | Criar índice composto (AccountID, Ctl1Code) |
| Ranking demora vários segundos | ORDER BY sem índice correspondente, sort em memória | Índice composto na mesma ordem do ORDER BY |
| Índice criado não é usado pelo otimizador | Ordem de colunas errada ou estatísticas desatualizadas | Reordenar por seletividade e rodar UPDATE STATISTICS/ANALYZE |
| Escrita ficou mais lenta após otimização | Índices demais em tabela de alta escrita (Item) | Remover índices não utilizados, manter só os essenciais |
| Índice existe mas plano ainda mostra scan | Função ou CAST aplicado na coluna do WHERE | Reescrever a query para não aplicar função sobre a coluna indexada |
| Índice funciona no teste mas não em produção | Estatísticas/parâmetros diferentes, plano em cache antigo | Forçar recompilação do plano e atualizar estatísticas |
Checklist de otimização de banco
- Log de queries lentas ou Query Store habilitado e monitorado.
- Top 10 queries mais frequentes/lentas identificadas via EXPLAIN/plano de execução.
- Índices compostos criados na ordem correta de seletividade para cada cenário (login, ranking, itens, guild).
- Covering indexes (INCLUDE) usados onde reduz leitura extra à tabela.
- Índices não utilizados identificados e removidos.
- Estatísticas atualizadas (UPDATE STATISTICS/ANALYZE) após grandes cargas de dados.
- Job de monitoramento de fragmentação e uso de índice agendado.
- Ganho de performance validado antes/depois com SET STATISTICS TIME ou benchmark real.
Com o banco respondendo rápido mesmo sob carga, o próximo passo é revisar a infraestrutura como um todo — desde o dimensionamento do servidor até a topologia de rede — para garantir que o gargalo não simplesmente migre do banco para outro componente. Veja o guia completo de criação de servidor de MU Online para revisar a arquitetura de ponta a ponta.
Perguntas frequentes
Índice composto é diferente de vários índices simples?
Sim. Um índice composto é uma única estrutura ordenada por múltiplas colunas, na ordem em que você as define. Ele acelera buscas que filtram por essas colunas juntas (ou pelo prefixo delas), enquanto vários índices simples raramente são combinados eficientemente pelo otimizador nessas consultas de servidor de MU.
Quantas colunas um índice composto pode ter?
Tecnicamente muitas, mas na prática o ideal é 2 a 4 colunas. Índices com muitas colunas ficam grandes, custam mais para manter em INSERT/UPDATE e o ganho de seletividade cai depois da terceira coluna na maioria das tabelas de personagem/item do MU.
Índice composto piora a performance de escrita?
Sim, um pouco — toda escrita na tabela precisa atualizar o índice também. Por isso o critério é: crie apenas para queries realmente frequentes e lentas (ranking, login, drop de item), não para todas as combinações possíveis de filtro.
Como sei qual índice falta sem adivinhar?
Ative o log de queries lentas do MSSQL/MySQL (slow query log ou Query Store) e rode EXPLAIN/Estimated Execution Plan nas consultas mais frequentes. O plano mostra Table Scan ou Index Scan onde deveria haver Index Seek — esse é o sinal de índice ausente ou mal ordenado.
Preciso recriar os índices depois de um restore de backup?
Geralmente não, os índices vêm dentro do backup completo. Mas se você fez restore só dos dados (BCP, import de CSV) ou migrou de engine, sim — rode o script de criação de índices novamente e atualize as estatísticas da tabela.