El mayor portal de MU Online de Brasil — desde 2003
Tutorial Avanzado Infraestructura

Índices compuestos en la base de datos de MU Online: cómo acabar con las queries lentas

Diagnostica y resuelve las queries lentas en la base de datos de tu servidor de MU Online usando índices compuestos bien diseñados, desde el EXPLAIN hasta el monitoreo continuo de performance.

BR Bruno · Actualizado el 28 oct 2025 · ⏱ 15 min de lectura
Respuesta rápida

Todo servidor de MU Online que crece en jugadores online simultáneos eventualmente choca con el mismo cuello de botella: la base de datos. Login lento, ranking que tarda segundos en cargar, guild que se traba al abrir la lista de miembros: la mayoría de las veces la causa no es hardware débil, sino

Todo servidor de MU Online que crece en jugadores online simultáneos eventualmente choca con el mismo cuello de botella: la base de datos. Login lento, ranking que tarda segundos en cargar, guild que se traba al abrir la lista de miembros: la mayoría de las veces la causa no es hardware débil, sino la ausencia de índices adecuados en las tablas correctas. Un índice compuesto bien diseñado transforma un barrido de millones de filas en una búsqueda directa de milisegundos. Este tutorial muestra cómo identificar queries lentas en el MSSQL/MySQL usado por tu MuServer, cómo diseñar índices compuestos para los patrones de consulta más comunes (login, ranking, inventario, guild) y cómo validar la ganancia real con EXPLAIN/plan de ejecución, sin caer en la trampa de indexar todo y empeorar la escritura.

Por qué la base de datos de MU Online sufre con queries lentas

La mayoría de los emuladores (MuEmu, IGCN, OpenMU, GVault) usa un esquema relacional clásico: tablas de Character, Item, Guild, AccountCharacter, MEMB_STAT, entre otras, con decenas de columnas y, con el tiempo, millones de filas, principalmente Item (cada ítem de cada personaje) y logs de eventos. Sin un índice adecuado, una consulta que filtra por AccountID y CharacterName hace un table scan completo, leyendo fila por fila. Con 50 mil cuentas eso todavía pasa desapercibido; con 500 mil cuentas el login se traba.

Diagnosticando: cómo encontrar las queries realmente lentas

No adivines. Activa el registro de consultas lentas antes de crear cualquier índice:

-- SQL Server: habilitar Query Store en la base del 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

Después de algunas horas con jugadores online, revisa el log o el Query Store buscando las queries con mayor tiempo promedio × mayor frecuencia, no por el mayor tiempo aislado. Una query de 50ms ejecutada 10 mil veces por minuto pesa más que una de 2s ejecutada una vez por hora.

Leyendo el plan de ejecución (EXPLAIN)

EXPLAIN SELECT * FROM Character WHERE AccountID = 'player01' AND Ctl1Code = 0;

Busca type: ALL (MySQL) o Table Scan/Clustered Index Scan (SQL Server): eso indica que la base está leyendo la tabla entera. El objetivo es ver type: ref/range o Index Seek, señal de que se está usando un índice de forma selectiva.

Qué es un índice compuesto y cuándo resuelve el problema

Un índice compuesto ordena físicamente los datos por más de una columna, en el orden declarado. Resuelve búsquedas que filtran por esas columnas juntas, y también búsquedas que filtran solo por el prefijo (la primera columna, o las primeras N columnas). El orden de las columnas importa: un índice (AccountID, CharacterName) acelera WHERE AccountID = X y WHERE AccountID = X AND CharacterName = Y, pero no acelera por sí solo un WHERE CharacterName = Y aislado.

Mapeando las queries más comunes del servidor de MU

EscenarioQuery típicaColumnas del índice compuesto
Login de cuentaWHERE AccountID = ? AND Password = ?(AccountID, Password)
Lista de personajes de la cuentaWHERE AccountID = ? ORDER BY Ctl1Code(AccountID, Ctl1Code)
Ranking por Resets/LevelWHERE Ctl1Code = 0 ORDER BY Resets DESC, cLevel DESC(Ctl1Code, Resets DESC, cLevel DESC)
Ítems de un personajeWHERE AccountID = ? AND Name = ?(AccountID, Name)
Miembros de una 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)

Paso a paso para crear un índice compuesto

-- 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);

El INCLUDE en SQL Server es importante: agrega columnas al índice sin hacerlas parte de la clave de búsqueda, permitiendo que la query sea respondida enteramente por el índice (covering index), sin volver a la tabla original.

Ordenando las columnas correctamente (selectividad primero)

Regla práctica: coloca primero la columna más selectiva, la que más reduce el conjunto de resultados. AccountID es altamente selectivo (pocas filas por cuenta); Ctl1Code (que generalmente indica personaje activo/eliminado) tiene solo 2-3 valores posibles, así que debe ir después, no antes. Un índice (Ctl1Code, AccountID) sería mucho menos eficiente que (AccountID, Ctl1Code).

Índices para ORDER BY y ranking

Los sitios de ranking (como la propia página pública de ViciadosMU) hacen ORDER BY Resets DESC, cLevel DESC cada vez que alguien accede. Sin índice, eso es una ordenación completa en memoria en cada solicitud. Un índice compuesto que ya contempla el orden de clasificación evita el sort en el plan de ejecución:

CREATE NONCLUSTERED INDEX IX_Character_Ranking
ON Character (Ctl1Code, Resets DESC, cLevel DESC)
INCLUDE (Name, Class);

Esto es especialmente crítico si el ranking web consulta la base de datos directamente (sin cache) en cada carga de página; considera también un cache de 1-5 minutos del lado de la aplicación además del índice.

Cuidados con la escritura y el mantenimiento

Cada índice adicional cuesta CPU e I/O extra en cada INSERT, UPDATE y DELETE en la tabla. En Item, tabla que recibe escritura constante (drop, trade, refinamiento), crear demasiados índices vuelve más lento el loop de guardado del GameServer. Directriz práctica:

  • Crea como máximo 3-5 índices por tabla de alta escritura.
  • Revisa mensualmente qué índices nunca se usan (sys.dm_db_index_usage_stats en SQL Server) y elimínalos.
  • Ejecuta UPDATE STATISTICS (SQL Server) o ANALYZE TABLE (MySQL) periódicamente; las estadísticas desactualizadas hacen que el optimizador elija planes malos incluso con el índice presente.

Índices parciales/filtrados para reducir el tamaño

Si el 90% de las filas de Character tienen Ctl1Code = 0 (activo) y solo consultas personajes activos, un índice filtrado es más pequeño y más rápido:

CREATE NONCLUSTERED INDEX IX_Character_Active
ON Character (AccountID, Name)
WHERE Ctl1Code = 0;

Esto reduce el tamaño del índice y mejora el cache hit ratio, ya que menos páginas necesitan permanecer en memoria.

Validando la ganancia antes y después

Compara siempre el plan de ejecución antes y después de crear el índice, y mide el tiempo real:

SET STATISTICS TIME ON;
SELECT * FROM Character WHERE AccountID = 'player01' ORDER BY Resets DESC;
SET STATISTICS TIME OFF;

Una ganancia típica al pasar de table scan a index seek en una tabla de 2 millones de filas es de segundos a menos de 50ms. Si la ganancia no aparece, revisa si la query realmente usa las columnas del índice en la cláusula WHERE/ORDER BY en el orden correcto.

Monitoreo continuo

Configura un job (SQL Server Agent o cron + script) que corra semanalmente y reporte: las queries más lentas de la semana, índices no utilizados, fragmentación de índice por encima del 30%. La fragmentación alta pide REORGANIZE (10-30%) o REBUILD (más del 30%). Sin esta rutina, la base de datos se degrada de forma silenciosa a medida que crece la base de jugadores.

Errores comunes y soluciones

SíntomaCausa probableSolución
Login lento solo en horario picoTable scan en Character/AccountCharacter bajo concurrenciaCrear índice compuesto (AccountID, Ctl1Code)
El ranking tarda varios segundosORDER BY sin índice correspondiente, sort en memoriaÍndice compuesto en el mismo orden del ORDER BY
El índice creado no es usado por el optimizadorOrden de columnas incorrecto o estadísticas desactualizadasReordenar por selectividad y ejecutar UPDATE STATISTICS/ANALYZE
La escritura se volvió más lenta después de la optimizaciónDemasiados índices en tabla de alta escritura (Item)Eliminar índices no utilizados, mantener solo los esenciales
El índice existe pero el plan aún muestra scanFunción o CAST aplicado en la columna del WHEREReescribir la query para no aplicar función sobre la columna indexada
El índice funciona en pruebas pero no en producciónEstadísticas/parámetros diferentes, plan en cache antiguoForzar recompilación del plan y actualizar estadísticas

Lista de verificación de optimización de base de datos

  • Log de queries lentas o Query Store habilitado y monitoreado.
  • Top 10 queries más frecuentes/lentas identificadas vía EXPLAIN/plan de ejecución.
  • Índices compuestos creados en el orden correcto de selectividad para cada escenario (login, ranking, ítems, guild).
  • Covering indexes (INCLUDE) usados donde reduce lectura extra a la tabla.
  • Índices no utilizados identificados y eliminados.
  • Estadísticas actualizadas (UPDATE STATISTICS/ANALYZE) después de grandes cargas de datos.
  • Job de monitoreo de fragmentación y uso de índice programado.
  • Ganancia de performance validada antes/después con SET STATISTICS TIME o benchmark real.

Con la base de datos respondiendo rápido incluso bajo carga, el siguiente paso es revisar la infraestructura en su conjunto, desde el dimensionamiento del servidor hasta la topología de red, para garantizar que el cuello de botella no simplemente migre de la base de datos a otro componente. Consulta la guía completa de creación de servidor de MU Online para revisar la arquitectura de punta a punta.

Preguntas frecuentes

¿Un índice compuesto es diferente de varios índices simples?

Sí. Un índice compuesto es una única estructura ordenada por múltiples columnas, en el orden en que las defines. Acelera búsquedas que filtran por esas columnas juntas (o por el prefijo de ellas), mientras que varios índices simples rara vez son combinados de forma eficiente por el optimizador en estas consultas de servidor de MU.

¿Cuántas columnas puede tener un índice compuesto?

Técnicamente muchas, pero en la práctica lo ideal es de 2 a 4 columnas. Los índices con demasiadas columnas se vuelven grandes, cuestan más de mantener en INSERT/UPDATE y la ganancia de selectividad cae después de la tercera columna en la mayoría de las tablas de personaje/ítem de MU.

¿Un índice compuesto empeora la performance de escritura?

Sí, un poco: toda escritura en la tabla necesita actualizar el índice también. Por eso el criterio es: crear índices solo para queries realmente frecuentes y lentas (ranking, login, drop de ítem), no para todas las combinaciones posibles de filtro.

¿Cómo sé qué índice falta sin adivinar?

Activa el log de queries lentas de MSSQL/MySQL (slow query log o Query Store) y ejecuta EXPLAIN/Estimated Execution Plan en las consultas más frecuentes. El plan muestra Table Scan o Index Scan donde debería haber Index Seek: esa es la señal de un índice ausente o mal ordenado.

¿Necesito recrear los índices después de un restore de backup?

Generalmente no, los índices vienen dentro del backup completo. Pero si hiciste restore solo de los datos (BCP, import de CSV) o migraste de motor, sí: ejecuta de nuevo el script de creación de índices y actualiza las estadísticas de la tabla.

BR
Editor de eventos, mapas e ítems

Bruno es especialista en eventos, mapas, bosses y economía de ítems de MU Online. Documenta cada detalle basándose en el juego real.

Sigue leyendo

Artículos relacionados