Audiencia: Propietarios, programadores y mantenedores de servidores FiveM
Meta: Reemplazar mysql-async con oxmysql de forma segura, acelere las consultas y modernice su uso de SQL.
Lea también:
- Optimización de servidores FiveM: La guía definitiva para 2025 — https://fivemx.com/how-to-optimize-fivem-server-performance/
- Patrones de adaptador: Exportaciones ESX↔QBCore↔QBOX, eventos y modelos de reproductor — https://fivemx.com/adapter-patterns/
Resumen
- Usar
oxmysql:declaraciones preparadas, API de promesa/espera, mejores diagnósticos, sólido desempeño. - Cambios mínimos en el código: intercambio
@param→?(posicional) o:nameparámetros (con nombre); reemplazarMySQL.Async.*llamadas conMySQL.*/exports.oxmysql:*. - Ejecute los scripts SQL “UP” A continuación (correcciones de conjunto de caracteres/índice) y mantener el revertir práctico.
- Verificar con el arnés de micro-benchmark al final para confirmar las victorias en tu hardware.
1) Lista de verificación de seguridad previa al vuelo
- Copia de seguridad completa:
mysqldump --single-transaction yourdb > backup.sql. - Entorno de ensayo esquema de producción reflejado + subconjunto de datos.
- Artefacto y dependencias:Compilación actual de FXServer, última
oxmysql. - Ventana de tiempo de inactividad para cambio de producto (generalmente < 5 minutos).
- Sondas de salud listo:
/players, flujo de inicio de sesión, operaciones económicas, operaciones de garaje, operaciones de inventario, controles de prohibición.
2) Instalar y cablear oxmysql
2.1 servidor.cfg
# Stop using mysql-async default_prio 500 # ensure mysql-async # ← comment out or remove # Start oxmysql default_prio 50 ensure oxmysql # Connection string consumed by oxmysql set mysql_connection_string "mysql://user:pass@127.0.0.1:3306/yourdb?charset=utf8mb4" # Optional diagnostics set mysql_slow_query_warning 200 # log queries slower than 200ms set mysql_debug false # true for verbose logging during staging
Mantener
mysql-asyncdeshabilitado pero disponible en su carpeta de recursos durante la fase de preparación (para una rápida reversión).
3) Mapeo de API: mysql‑async → oxmysql
mysql-async (legado):
- Asíncrono:
MySQL.Async.fetchAll,MySQL.Async.fetchScalar,MySQL.Async.execute - Sincronizar:
MySQL.Sync.fetchAll,MySQL.Sync.fetchScalar,MySQL.Sync.execute - Parámetros:
@parammesas de estilo como{ ['@identifier']=identifier }
oxmysql (moderno):
- Estilo de devolución de llamada a través de exportar:
exports.oxmysql:query|scalar|single|insert|update(sql, params, cb) - Promesa/espera vía global:
MySQL.query|scalar|single|insert|update.await(sql, params)y devoluciones de llamadas sin espera sin.await - Parámetros: posicional
?a través de una matriz, o nombrado:namevía objeto
3.1 Reemplazos comunes
SELECCIONE muchos
-- mysql-async
MySQL.Async.fetchAll(
'SELECT * FROM users WHERE identifier = @id',
{ ['@id'] = identifier },
function(rows) ... end
)
-- oxmysql (callback via export)
exports.oxmysql:query(
'SELECT * FROM users WHERE identifier = ?',
{ identifier },
function(rows) ... end
)
-- oxmysql (await)
local rows = MySQL.query.await(
'SELECT * FROM users WHERE identifier = ?',
{ identifier }
)
SELECCIONAR una sola fila
-- mysql-async (fetchAll + rows[1])
-- oxmysql
local row = MySQL.single.await(
'SELECT * FROM users WHERE identifier = ?',
{ identifier }
)
SELECCIONAR escalar (por ejemplo, contar, identificar)
-- mysql-async
-- oxmysql
local count = MySQL.scalar.await(
'SELECT COUNT(*) FROM owned_vehicles WHERE owner = ?',
{ owner }
)
INSERTAR (obtener insertId)
-- mysql-async (execute)
-- oxmysql
local insertId = MySQL.insert.await(
'INSERT INTO notes (owner, text) VALUES (?, ?)',
{ cid, text }
)
ACTUALIZAR/ELIMINAR (filas afectadas)
-- mysql-async (execute)
-- oxmysql
local changed = MySQL.update.await(
'UPDATE users SET job = ?, job_grade = ? WHERE identifier = ?',
{ job, grade, identifier }
)
Actas (manual)
-- oxmysql manual transaction
MySQL.query.await('START TRANSACTION')
local ok = true
local r1 = MySQL.update.await('UPDATE users SET bank = bank - ? WHERE identifier = ? AND bank >= ?', { amount, fromId, amount })
local r2 = MySQL.update.await('UPDATE users SET bank = bank + ? WHERE identifier = ?', { amount, toId })
if r1 == 1 and r2 == 1 then
MySQL.query.await('COMMIT')
else
MySQL.query.await('ROLLBACK')
end
Algunos marcos exponen envoltorios (por ejemplo,
ox_lib) que añadenMySQL.ready,.transaction, etc. Las llamadas anteriores son seguras sin envoltorios adicionales.
4) Hoja de referencia para declaraciones preparadas
Estilos de parámetros
- mysql‑async (heredado):
@namemarcadores de posición con una tabla:{ ['@name']=value } - oxmysql (posicional):
?marcadores de posición con un formación:{ value1, value2 } - oxmysql (nombrado):
:namemarcadores de posición con un objeto:{ name = value }
Ejemplos
-- Named params (recommended for readability)
local row = MySQL.single.await(
'SELECT * FROM users WHERE identifier = :id',
{ id = identifier }
)
-- IN (...) list
-- Build placeholders dynamically and pass a flat array
local ids = { 'cid1','cid2','cid3' }
local qs = ('?,' ):rep(#ids):sub(1,-2) -- "?, ?, ?"
local rows = MySQL.query.await('SELECT * FROM players WHERE citizenid IN ('..qs..')', ids)
-- JSON fields (MySQL 5.7+/MariaDB 10.2+)
local name = MySQL.scalar.await('SELECT JSON_UNQUOTE(JSON_EXTRACT(data, "$.name")) FROM players WHERE citizenid = ?', { cid })
Hacer
- Usar declaraciones preparadas en todas partes (nunca concatene cadenas de entrada del usuario).
- Preferir parámetros nombrados para mayor claridad en afirmaciones complejas.
- Agregar LÍMITE 1 al leer una sola entidad.
Evitar
- Comodín
SELECT *en caminos calientes (el proyecto necesitaba columnas). - Consultas N+1 por fila; lote con
IN (...).
5) Scripts de migración de base de datos “UP” (listos para ejecutar)
Seleccione los bloques compatibles con su framework (ESX/QBCore) y servidor (MySQL 8+ o MariaDB 10.4+). Ejecútelos primero en la configuración.
5.1 Normalizar el conjunto de caracteres y la intercalación (UTF‑8 en todas partes)
(A) MySQL 8+ - reemplazar yourdb una vez
-- Force database default to utf8mb4 (emoji‑safe) ALTER DATABASE `yourdb` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- Convert common tables (extend list as needed) ALTER TABLE `users` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE `owned_vehicles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE `players` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE `player_vehicles` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
(B) MariaDB 10.4+ —Las mismas afirmaciones son válidas.
Agregue otras tablas activas (inventario, facturación, teléfono, sociedad, trabajos) tal como están presentes en su servidor.
5.2 Índices ESX (el rendimiento seguro es lo mejor)
MySQL 8+
ALTER TABLE `users` ADD INDEX IF NOT EXISTS `idx_users_identifier` (`identifier`), ADD INDEX IF NOT EXISTS `idx_users_job` (`job`), ADD INDEX IF NOT EXISTS `idx_users_name` (`name`); ALTER TABLE `owned_vehicles` ADD UNIQUE INDEX IF NOT EXISTS `ux_owned_vehicles_plate` (`plate`), ADD INDEX IF NOT EXISTS `idx_owned_vehicles_owner` (`owner`);
MariaDB 10.4+
-- Drop first to be idempotent where IF NOT EXISTS is unavailable DROP INDEX IF EXISTS `idx_users_identifier` ON `users`; DROP INDEX IF EXISTS `idx_users_job` ON `users`; DROP INDEX IF EXISTS `idx_users_name` ON `users`; CREATE INDEX `idx_users_identifier` ON `users` (`identifier`); CREATE INDEX `idx_users_job` ON `users` (`job`); CREATE INDEX `idx_users_name` ON `users` (`name`); DROP INDEX IF EXISTS `ux_owned_vehicles_plate` ON `owned_vehicles`; DROP INDEX IF EXISTS `idx_owned_vehicles_owner` ON `owned_vehicles`; CREATE UNIQUE INDEX `ux_owned_vehicles_plate` ON `owned_vehicles` (`plate`); CREATE INDEX `idx_owned_vehicles_owner` ON `owned_vehicles` (`owner`);
5.3 Índices QBCore/QBOX
MySQL 8+
ALTER TABLE `players` ADD UNIQUE INDEX IF NOT EXISTS `ux_players_citizenid` (`citizenid`), ADD INDEX IF NOT EXISTS `idx_players_license` (`license`), ADD INDEX IF NOT EXISTS `idx_players_steam` (`steam`), ADD INDEX IF NOT EXISTS `idx_players_last_name` (`lastname`); ALTER TABLE `player_vehicles` ADD UNIQUE INDEX IF NOT EXISTS `ux_player_vehicles_plate` (`plate`), ADD INDEX IF NOT EXISTS `idx_player_vehicles_citizenid` (`citizenid`);
MariaDB 10.4+
DROP INDEX IF EXISTS `ux_players_citizenid` ON `players`; DROP INDEX IF EXISTS `idx_players_license` ON `players`; DROP INDEX IF EXISTS `idx_players_steam` ON `players`; DROP INDEX IF EXISTS `idx_players_last_name` ON `players`; CREATE UNIQUE INDEX `ux_players_citizenid` ON `players` (`citizenid`); CREATE INDEX `idx_players_license` ON `players` (`license`); CREATE INDEX `idx_players_steam` ON `players` (`steam`); CREATE INDEX `idx_players_last_name` ON `players` (`lastname`); DROP INDEX IF EXISTS `ux_player_vehicles_plate` ON `player_vehicles`; DROP INDEX IF EXISTS `idx_player_vehicles_citizenid` ON `player_vehicles`; CREATE UNIQUE INDEX `ux_player_vehicles_plate` ON `player_vehicles` (`plate`); CREATE INDEX `idx_player_vehicles_citizenid` ON `player_vehicles` (`citizenid`);
5.4 Opcional: ox_inventory (si está instalado)
ALTER TABLE `ox_inventory` ADD INDEX IF NOT EXISTS `idx_inv_owner` (`owner`), ADD INDEX IF NOT EXISTS `idx_inv_type` (`type`); ALTER TABLE `ox_inventory_items` ADD INDEX IF NOT EXISTS `idx_items_inv_owner_name` (`inventory`, `owner`, `name`);
Ajuste los nombres de las tablas si su esquema es diferente (algunas configuraciones utilizan
inventories/items).
6) Plan de reversión (sin pánico)
6.1 Reversión de código
- Revierte los cambios en tus recursos (mantén un
legacy-mysql-asyncrama). - En
server.cfgintercambio:# ensure oxmysql ensure mysql-async - Reinicie FXServer o los recursos afectados en orden de dependencia.
6.2 Reversión de SQL
- Si solo tu índices añadidos:déjalos caer (ver el MariaDB bloques arriba — uso
DROP INDEX IF EXISTS). - Si usted cambio de conjunto de caracteres/intercalación y debe deshacer, revertir la base de datos y las tablas:
ALTER DATABASE `yourdb` CHARACTER SET = utf8 COLLATE = utf8_general_ci; ALTER TABLE `users` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; ALTER TABLE `owned_vehicles` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; ALTER TABLE `players` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; ALTER TABLE `player_vehicles` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;
Prefiero restaurar desde copia de seguridad.sql en lugar de inversiones masivas de caracteres cuando sea posible.
7) Procedimiento de migración de extremo a extremo (programable)
- Congelación de despliegues, realizar una copia de seguridad de la base de datos.
- Aplicar Sección 5 SQL “UP” en puesta en escena → verificar → prod.
- Refactorización del código de confirmación: reemplazar llamadas (Sección 3) + estilos de parámetros (Sección 4).
- Desplegar,
ensure oxmysql, reiniciar el servidor. - Correr pruebas de humo (inicio de sesión, cheques de pago, inventario, aparición/desaparición de vehículos, prohibiciones, dinero de la sociedad, alternancia de deberes laborales).
- Observe los registros durante 15 a 30 minutos (
mysql_slow_query_warningayuda); solucione cualquier parámetro faltante o desajuste del esquema.
8) Micro-Benchmarks (Traiga sus propios números)
Un pequeño recurso que puedes usar para comparar consultas de rutas activas en su hardware y conjunto de datos.
fxmanifest.lua
fx_version 'cerulean' game 'gta5' server_script 'bench.lua'
banco.lua
local COUNT = 2000 -- adjust for your server
local function bench(name, fn)
local t0 = os.clock()
local ok, err = pcall(fn)
local dt = (os.clock() - t0) * 1000.0
print(('[bench] %s: %.2f ms %s'):format(name, dt, ok and '' or ('ERR: '..tostring(err))))
end
-- Hot path 1: ownership lookup
bench('SELECT single', function()
for i=1,COUNT do
local row = MySQL.single.await('SELECT owner FROM owned_vehicles WHERE plate = :p LIMIT 1', { p = ('TEST%04d'):format(i % 500) })
end
end)
-- Hot path 2: batched fetch
bench('SELECT batch IN', function()
local ids = {}
for i=1,100 do ids[#ids+1] = ('cid%04d'):format(i) end
local qs = ('?,' ):rep(#ids):sub(1,-2)
local rows = MySQL.query.await('SELECT citizenid, firstname, lastname FROM players WHERE citizenid IN ('..qs..')', ids)
end)
-- Hot path 3: update with guard
bench('UPDATE guarded', function()
for i=1,COUNT do
local changed = MySQL.update.await('UPDATE users SET bank = bank + :d WHERE identifier = :id AND bank >= 0', { d = 1, id = ('lic:%04d'):format(i % 500) })
end
end)
Cómo correr
- Coloque el recurso en una carpeta (por ejemplo,
ox-bench/), agregarensure ox-benchaserver.cfg. - Consola del servidor Tail; los resultados se imprimen como líneas como:
[bench] SELECT single: 134.21 ms. - Para una antes/después comparación, ejecutar una vez con
mysql-async(ajustar las llamadas si es necesario), luego conoxmysql.
Qué buscar
- Menor cantidad total de ms por sección después de la migración.
- Menor latencia P95/P99 en acciones de juego vinculadas a consultas.
- Menos advertencias de consultas lentas durante una hora de juego en vivo.
9) Solución de problemas
P: Recibo el mensaje “no existe dicha exportación: consulta/única/…”.
A: oxmysql no se inicia con la suficiente antelación. Asegúrese ensure oxmysql está por encima de los recursos que lo utilizan.
Q: Errores de parámetros o resultados vacíos.
A: Probablemente lo mantuviste @param marcadores de posición. Reemplazar con ? o :name y pasar una matriz/objeto en consecuencia.
P: Bloqueos o escrituras parciales.
A: Envuelva saldos/transferencias de varios pasos en una transacción (consulte la Sección 3) y agregue índices de la Sección 5.
P: La ruta JSON devuelve NULL.
A: Confirme que su motor admita funciones JSON (MySQL ≥5.7/MariaDB ≥10.2) y que el tipo de columna sea JSON (no LONGTEXT).
P: Lento después de la migración.
A: Verifique los índices faltantes, EXPLAIN su consulta, el proyecto solo necesita columnas y revise el Manual de optimización del servidor.
10) Lista de verificación de revisión de código (copiar y pegar)
- No hay SQL concatenado de cadenas; todas las consultas están parametrizadas.
- Usar
.single/.scalarconLIMIT 1cuando solo se requiere una fila/valor. - Lote
IN (...)lee para colecciones. - Transacciones en torno a operaciones de dinero/inventario de múltiples pasos.
- Índice presente para cada hot
WHERE/JOINcolumna. - Evitar
SELECT *en caminos calientes. - Registrar consultas lentas; realizar un seguimiento semanal de los principales infractores.
Enlaces internos
- Optimización de servidores FiveM: La guía definitiva para 2025 — https://fivemx.com/how-to-optimize-fivem-server-performance/
- Patrones de adaptador: Exportaciones ESX↔QBCore↔QBOX, eventos y modelos de reproductor — https://fivemx.com/adapter-patterns/
Créditos
Mantenido por fivemx.com. Se agradecen las contribuciones (envíe comparaciones de índices seguros adicionales o ayudantes de wrapper).
