O maior portal de MU Online do Brasil — desde 2003
Tutorial Avançado Infra

Í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.

BR Bruno · Atualizado em 28 out 2025 · ⏱ 15 min de leitura
Resposta rápida

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árioQuery típicaColunas do índice composto
Login de contaWHERE AccountID = ? AND Password = ?(AccountID, Password)
Lista de personagens da contaWHERE AccountID = ? ORDER BY Ctl1Code(AccountID, Ctl1Code)
Ranking por Resets/LevelWHERE Ctl1Code = 0 ORDER BY Resets DESC, cLevel DESC(Ctl1Code, Resets DESC, cLevel DESC)
Itens de um personagemWHERE AccountID = ? AND Name = ?(AccountID, Name)
Membros de uma guildWHERE G_Name = ? ORDER BY G_Status(G_Name, G_Status)
Log de trade/GM por períodoWHERE 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_stats no SQL Server) e remova-os.
  • Rode UPDATE STATISTICS (SQL Server) ou ANALYZE 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

SintomaCausa provávelSolução
Login lento apenas em horário de picoTable scan em Character/AccountCharacter sob concorrênciaCriar índice composto (AccountID, Ctl1Code)
Ranking demora vários segundosORDER BY sem índice correspondente, sort em memóriaÍndice composto na mesma ordem do ORDER BY
Índice criado não é usado pelo otimizadorOrdem de colunas errada ou estatísticas desatualizadasReordenar 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 scanFunção ou CAST aplicado na coluna do WHEREReescrever a query para não aplicar função sobre a coluna indexada
Índice funciona no teste mas não em produçãoEstatísticas/parâmetros diferentes, plano em cache antigoForç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.

BR
Editor de eventos, mapas e itens

Bruno é especialista em eventos, mapas, bosses e economia de itens do MU Online. Documenta cada detalhe com base em jogo real.

Continue lendo

Artigos relacionados