Cómo particionar tablas grandes de la base de datos en tu servidor de MU Online
Aprende a particionar tablas grandes de MS SQL Server (LogRecords, Character, AccountCharacter) en tu servidor de MU Online para mantener las consultas rápidas incluso con millones de líneas de logs e historial.
Los servidores de MU Online que corren durante meses acumulan millones de filas en tablas de log, historial de ítems y movimiento de cuenta — y es común ver el panel administrativo, el ranking o hasta el propio GameServer trabarse porque una consulta simple necesita recorrer una tabla de 40 millones
Los servidores de MU Online que corren durante meses acumulan millones de filas en tablas de log, historial de ítems y movimiento de cuenta — y es común ver el panel administrativo, el ranking o hasta el propio GameServer trabarse porque una consulta simple necesita recorrer una tabla de 40 millones de filas. El particionamiento de tablas es la técnica de base de datos que resuelve este problema: divide físicamente una tabla grande en pedazos más pequeños (particiones) sin cambiar la forma en que la aplicación ve la tabla. En este tutorial aprenderás qué particionar, cómo crear una partition function y un partition scheme en SQL Server, cómo migrar una tabla existente sin perder datos y cómo mantener las particiones a lo largo del tiempo con una rutina de mantenimiento (sliding window).
Por qué las tablas de MU Online crecen tan rápido
El MuServer registra prácticamente todo evento relevante: login/logout, intercambio de ítems, uso de la Chaos Machine, chat, PK, drop de boss, compra en la tienda de cash. En un servidor con 500 jugadores simultáneos, la tabla LogRecords (o equivalente de tu emulador) puede superar las 100 mil filas por día. Después de seis meses eso son 18 millones de filas en una sola tabla, sin ninguna segmentación física — cada SELECT con filtro de fecha hace un table scan completo, incluso con índice, porque el optimizador todavía necesita decidir qué páginas leer dentro de una estructura monolítica.
Diagnosticando el problema antes de particionar
Antes de salir a crear particiones, confirma que el cuello de botella realmente es volumen de datos. Corre el plan de ejecución de las consultas más lentas del panel admin y del ranking y observa si aparece un Table Scan o Clustered Index Scan en vez de un Seek. Verifica también el tamaño físico de la tabla con sp_spaceused 'LogRecords' y el tiempo de respuesta en producción con SET STATISTICS TIME ON. Si el cuello de botella es falta de índice, resuelve eso primero — particionar una tabla mal indexada solo cambia un problema por otro más grande y más difícil de revertir.
Eligiendo la clave de partición correcta
| Tabla candidata | Clave de partición sugerida | Criterio de corte |
|---|---|---|
| LogRecords / MuveLog | CreateDate (datetime) | Mensual o semanal |
| ChatLog | LogDate | Mensual |
| AccountCharacter / Character | CharacterID (range) | Rangos de ID por servidor |
| ItemHistory / TradeLog | TransactionDate | Semanal, con purge después de 90 días |
| GuildWarLog | EventDate | Por temporada de evento |
La regla práctica: la columna elegida necesita aparecer en el WHERE de la mayoría de las consultas que hoy son lentas. Particionar por una columna que nadie filtra no genera ninguna partition elimination — SQL Server sigue recorriendo todas las particiones.
Creando la partition function y el partition scheme
En SQL Server, el particionamiento nativo usa dos piezas: la partition function, que define los límites (boundaries), y el partition scheme, que mapea cada rango a un filegroup físico.
-- Función de partición por mes (RANGE RIGHT: el límite pertenece a la partición siguiente)
CREATE PARTITION FUNCTION PF_LogPorMes (datetime)
AS RANGE RIGHT FOR VALUES (
'2026-01-01', '2026-02-01', '2026-03-01',
'2026-04-01', '2026-05-01', '2026-06-01'
);
-- Esquema de partición asociando cada rango a un filegroup
CREATE PARTITION SCHEME PS_LogPorMes
AS PARTITION PF_LogPorMes
ALL TO ([PRIMARY]);
Usar ALL TO ([PRIMARY]) simplifica el inicio, colocando todas las particiones en el filegroup por defecto. En servidores más grandes, vale la pena crear un filegroup por partición para poder hacer backup/restore incremental de particiones antiguas de forma aislada.
Migrando una tabla existente al nuevo esquema
No es posible transformar una tabla común en particionada con un ALTER TABLE simple cuando ya tiene datos y claves. El camino seguro es:
-- 1. Crear la tabla nueva ya particionada, con la misma estructura
SELECT * INTO LogRecords_New FROM LogRecords WHERE 1 = 0;
-- 2. Recrear el índice clustered sobre el esquema de partición
CREATE CLUSTERED INDEX CIX_LogRecords_New
ON LogRecords_New (CreateDate)
ON PS_LogPorMes (CreateDate);
-- 3. Copiar los datos en lotes (evita bloquear la producción)
INSERT INTO LogRecords_New
SELECT TOP (100000) * FROM LogRecords
ORDER BY CreateDate;
-- repite en lotes hasta agotar
-- 4. Intercambiar los nombres dentro de una ventana de mantenimiento corta
EXEC sp_rename 'LogRecords', 'LogRecords_Old';
EXEC sp_rename 'LogRecords_New', 'LogRecords';
Haz la copia en lotes fuera del horario pico (madrugada, cuando el servidor tiene menos jugadores online) y solo ejecuta el intercambio de nombres cuando el desfase entre las dos tablas sea lo bastante pequeño como para copiarlo en segundos.
Sliding window: manteniendo solo el historial necesario
La mayor ventaja práctica del particionamiento en tablas de log es el sliding window: en vez de hacer DELETE de millones de filas antiguas (operación lenta y que genera mucho log de transacción), haces SWITCH de la partición entera hacia una tabla de staging y luego truncas esa tabla — operación casi instantánea.
-- Mueve la partición más antigua a una tabla vacía del mismo schema
ALTER TABLE LogRecords SWITCH PARTITION 1 TO LogRecords_Staging;
TRUNCATE TABLE LogRecords_Staging;
-- A continuación, crea el próximo boundary para el mes futuro
ALTER PARTITION FUNCTION PF_LogPorMes()
SPLIT RANGE ('2026-07-01');
Programa esta rutina como un job mensual en el SQL Server Agent, alineado con la política de retención que decidas (por ejemplo, mantener 6 meses de log detallado y archivar el resto).
Validando la ganancia con partition elimination
Después de particionar, confirma que las consultas realmente usan la partición correcta en vez de recorrer todo. Habilita el plan de ejecución y busca el atributo Actual Partition Count en el operador de scan — debe mostrar 1 (o pocas particiones), no el total. Si el número de particiones accedidas es igual al total, la consulta no está filtrando por la clave de partición y la ganancia es nula.
SET STATISTICS IO ON;
SELECT COUNT(*) FROM LogRecords
WHERE CreateDate >= '2026-06-01' AND CreateDate < '2026-07-01';
Compara los logical reads antes y después — la reducción suele ser de un orden de magnitud en tablas grandes.
Impacto en backup y mantenimiento de índices
Las tablas particionadas permiten reindexar solo las particiones "calientes" (las más recientes, con más fragmentación por inserciones constantes), en vez de reconstruir el índice entero cada noche. Esto reduce drásticamente la ventana de mantenimiento en servidores con tablas de decenas de gigabytes.
| Rutina | Antes (tabla única) | Después (particionada) |
|---|---|---|
| Reindex diario | Tabla entera, 40+ min | Solo últimas 2-3 particiones, 3-5 min |
| Backup completo | Crece cada mes | Backup por filegroup, incremental |
| Purge de datos antiguos | DELETE lento, bloquea escrituras | SWITCH + TRUNCATE, casi instantáneo |
| Consulta de ranking histórico | Table scan completo | Partition elimination, seek directo |
Cuidados con claves foráneas y replicación
Si la tabla particionada tiene foreign keys que referencian o son referenciadas por otras tablas del MuServer (por ejemplo Character referenciando AccountCharacter), confirma que la clave de partición no rompa la integridad referencial — en SQL Server, las claves foráneas funcionan normalmente con tablas particionadas, pero la planificación de la migración necesita considerar el orden de creación y la ventana de indisponibilidad de cada tabla dependiente. En entornos con replicación (por ejemplo, una base de solo lectura para el sitio/ranking), prueba la migración primero en el entorno de réplica.
Monitoreando el crecimiento de las particiones
Después de implementado, monitorea el tamaño de cada partición periódicamente para confirmar que la distribución está equilibrada — una partición desproporcionadamente grande (por ejemplo, un mes con evento especial que generó log en exceso) puede volver a causar lentitud aislada.
SELECT p.partition_number, p.rows, au.total_pages * 8 / 1024 AS SizeMB
FROM sys.partitions p
JOIN sys.allocation_units au ON au.container_id = p.hobt_id
WHERE p.object_id = OBJECT_ID('LogRecords')
ORDER BY p.partition_number;
Errores comunes y soluciones
| Síntoma | Causa probable | Solución |
|---|---|---|
| ALTER TABLE traba el GameServer | Migración hecha en horario pico | Haz la copia en lotes fuera del pico e intercambia nombres en ventana corta |
| La consulta sigue lenta después de particionar | Clave de partición no usada en el filtro | Ajusta la consulta o reevalúa la clave elegida |
| SPLIT/MERGE falla con error de datos | Boundary superpuesto a datos existentes | Siempre haz SPLIT antes de que la partición futura reciba datos |
| Los índices se fragmentan rápido | Reindex no segmentado por partición | Reconstruye solo las particiones recientes, diariamente |
| Backup muy grande y lento | Un único filegroup para todo | Separa particiones antiguas en filegroups propios y suma backups incrementales |
Lista de verificación de particionamiento
- Identificado el cuello de botella real vía plan de ejecución (no asumido de memoria).
- Elegida la clave de partición alineada con los filtros de las consultas más pesadas.
- Partition function y partition scheme creados y probados en entorno de homologación.
- Migración hecha vía tabla nueva + copia en lotes, con ventana corta de intercambio de nombres.
- Rutina de sliding window programada para purge/archivado automático.
- Partition elimination validado con STATISTICS IO antes/después.
- Monitoreo periódico del tamaño de cada partición configurado.
- Backup y reindexación ajustados para trabajar por partición.
Con las tablas de log bajo control, el siguiente paso natural es revisar toda la infraestructura del servidor a la luz de la carga esperada — desde el dimensionamiento de la base de datos hasta la topología de GameServer y ConnectServer descrita en el tutorial de creación de servidor de MU Online.
Preguntas frecuentes
¿Cuándo realmente necesito particionar una tabla?
Cuando supera algunos millones de filas y las consultas del ranking, del panel administrativo o de los logs de GM empiezan a tardar más de 1-2 segundos. Las tablas de log (LogRecords, MuveLog, ChatLog) suelen ser las primeras candidatas, porque crecen todos los días sin límite natural.
¿Particionar reemplaza la necesidad de índices?
No. Particionamiento e indexación resuelven problemas distintos y se complementan. El particionamiento reduce el volumen de datos que una consulta necesita recorrer (partition elimination); los índices aceleran la búsqueda dentro de cada partición. Uno sin el otro entrega una ganancia parcial.
¿Puedo particionar una tabla que ya está en producción sin downtime?
Es posible, pero exige cuidado. El enfoque más seguro es crear una tabla nueva particionada, copiar los datos en lotes fuera del horario pico, e intercambiar los nombres de las tablas dentro de una transacción corta. Particionar in-place con ALTER TABLE bloquea la tabla por más tiempo.
¿Qué columna uso como clave de partición?
Casi siempre una columna de fecha/hora (CreateDate, LogDate) para tablas de log, o el CharacterID para tablas de datos de personaje en servidores muy grandes. La clave necesita aparecer en la mayoría de los filtros de las consultas más pesadas, de lo contrario el particionamiento no ayuda.
¿El particionamiento funciona en cualquier edición de SQL Server?
El particionamiento nativo (partition function/scheme) es una función Enterprise en las versiones más antiguas, pero también está disponible en la edición Standard desde SQL Server 2016 SP1. Confirma la versión de tu servidor antes de planificar la arquitectura.