name: mysql
description: Experiencia profunda y específica en bases de datos MySQL/MariaDB: tipos de datos correctos y charset/collation (utf8mb4, DECIMAL para dinero, VARCHAR/INT dimensionados correctamente, compensaciones de ENUM), InnoDB vs MyISAM, diseño de índices y optimización de consultas mediante EXPLAIN (orden de índices compuestos, índices de cobertura, predicados sargables, paginación con keyset), aislamiento de transacciones y bloqueos (REPEATABLE READ, gap locks, deadlocks), y trampas de corrección específicas de MySQL. Úsalo cuando package.json liste mysql2 o mysql, el dialecto/proveedor de un ORM esté configurado para mysql (Prisma provider = "mysql", Sequelize dialect: 'mysql', TypeORM type: 'mysql'/'mariadb'), un servicio docker-compose ejecute una imagen mysql/mariadb, o haya una cadena de conexión mysql://, incluso cuando se te pida directamente (en tailandés o inglés) escribir o revisar un schema/query de MySQL que sea correcto y eficiente. No reemplaza el flujo de trabajo de migración independiente del motor de database-schema-dev ni el código de capa de acceso de backend-dev; es la experiencia de MySQL que ambos utilizan cuando el motor es MySQL/MariaDB.
MySQL
Capa de experiencia específica de MySQL de database-schema-dev y backend-dev:
esos agentes son independientes del motor y coinciden con la herramienta de migración/ORM que ya tiene un proyecto — este skill es el conocimiento real de MySQL que ellos utilizan cuando el motor resulta ser MySQL, o su fork compatible MariaDB.
Cuando MariaDB se comporta de manera diferente, se señala — no asumas paridad completa en características avanzadas.
Paso 1 — Confirmar que el motor es realmente MySQL/MariaDB
Revisa package.json para mysql2/mysql, un dialecto/proveedor de ORM (provider = "mysql" en Prisma, dialect: 'mysql' en Sequelize, type: 'mysql'/'mariadb' en TypeORM), un servicio en docker-compose.yml que ejecute una imagen mysql/mariadb, o una cadena de conexión mysql://. Si nada de esto se cumple, detente — usa los valores predeterminados independientes del motor de database-schema-dev/backend-dev, o el skill específico del motor correspondiente.
Paso 2 — Charset, collation, motor de almacenamiento
- El charset predeterminado es
utf8mb4, noutf8— elutf8de MySQL es un alias de máximo 3 bytes que no puede contener la mayoría de emojis/caracteres astrales — con una collation explícita (utf8mb4_0900_ai_cien MySQL 8+; MariaDB no tiene collations0900, usautf8mb4_unicode_cipara cualquier cosa dirigida a ambos). InnoDBa menos que haya una razón específica paraMyISAM(sin transacciones, sin bloqueos de filas, sin claves foráneas) — es el predeterminado desde MySQL 5.5/MariaDB 10.x, así que esto generalmente es solo confirmar que nada lo sobrescribe.- La collation
_cisignifica que'[email protected]'coincide con'[email protected]'— usa_bin/_cs(o una columna con hash) para cualquier cosa que deba compararse distinguiendo mayúsculas/minúsculas (tokens, secretos).
Paso 3 — Tipos de datos correctos
- Dinero/decimales exactos:
DECIMAL(p,s), nuncaFLOAT/DOUBLE— el flotante binario no puede representar la mayoría de fracciones en base 10 exactamente. - Enteros dimensionados correctamente (
TINYINT/SMALLINT/INT/BIGINT) según el rango real, noBIGINTen todos lados por costumbre — tipos más pequeños significan índices más pequeños.UNSIGNEDes espacio libre adicional en una columna de id, que nunca es negativa. DATETIMEpara un valor de reloj sin conversión de zona horaria prevista;TIMESTAMP(normalizado a UTC, se convierte automáticamente según la zona horaria de la sesión, límite de rango 2038) solo para un instante real en el tiempo — no lo uses por defecto.VARCHAR(n)dimensionado según la restricción real, noVARCHAR(255)por reflejo;TEXT/BLOBpara contenido genuinamente ilimitado — ninguno acepta un valor predeterminado ni un índice completo (no de prefijo).ENUMsolo para un conjunto fijo que rara vez cambia — los valores se ordenan según el orden de definición, no alfabéticamente (una sorpresa enORDER BY), y agregar un valor es solo metadatos en MySQL 8 pero puede reescribir la tabla en MariaDB/MySQL más antiguo. Una tabla de referencia escala mejor cuando el conjunto crece o necesita metadatos.JSONnativo (MySQL 5.7.8+/MariaDB 10.2+ — elJSONde MariaDB es en realidadLONGTEXTmás unCHECK, no un tipo binario, por lo que ninguna de las ventajas de almacenamiento binario/actualización parcial de MySQL se aplica) solo para datos genuinamente sin esquema — cualquier cosa que se filtre o se una regularmente debe estar en una columna real.
Paso 4 — Diseño de índices
- Índices compuestos: columna(s) filtradas por igualdad primero, la columna de rango/orden al final —
(status, created_at)sirve paraWHERE status = ? ORDER BY created_at; el orden inverso no. - Regla del prefijo más a la izquierda: un índice en
(a, b, c)también sirve para búsquedas en(a)y(a, b)— no agregues un índice redundante(a)junto a él. - Un índice de cobertura (cada columna seleccionada incluida) permite que MySQL responda solo con el índice (
Using indexenEXPLAIN) — vale la pena para una consulta caliente y estrecha, no por defecto en cada consulta. - Índice de prefijo (
col(20)) paraVARCHAR/TEXTlargos — verificaCOUNT(DISTINCT LEFT(col, n))contraCOUNT(DISTINCT col)para dimensionarn. - Paginación profunda:
LIMIT 20 OFFSET 100000todavía escanea y descarta 100,000 filas cada página. La paginación con keyset (WHERE (created_at, id) < (:last_created, :last_id) ORDER BY created_at DESC, id DESC LIMIT 20) reutiliza el mismo índice compuesto y busca directamente el cursor — úsala para desplazamiento infinito o paginación profunda de API en lugar deOFFSET. - Cada índice cuesta tiempo de escritura y espacio en disco — agrega índices para patrones de consulta reales (Paso 1 de
database-schema-dev), no especulativamente por columna.
Paso 5 — Optimización de consultas mediante EXPLAIN
- Ejecuta
EXPLAIN(oEXPLAIN ANALYZE, MySQL 8.0.18+/MariaDB 10.4+) antes de confiar en que una consulta es rápida — no juzgues solo por la forma de la consulta. type: ALLes un escaneo completo de tabla;ref/range/constsignifica que realmente se usa un índice — una consulta que vale la pena optimizar no debería mostrarALLmás allá de un tamaño de tabla trivial.Extra: Using filesort/Using temporarysignifica que MySQL ordenó/materializó fuera de un índice — generalmente se soluciona con un índice que coincida con elORDER BY/GROUP BY.- Una función envolvente o conversión implícita en una columna indexada (
WHERE YEAR(created_at) = ..., comparar una columna de texto con un número) anula el índice — reescribe como un rango de columna simple en su lugar (created_at >= '2024-01-01' AND created_at < '2025-01-01'). SELECT *en una tabla ancha anula los índices de cobertura y trae columnasTEXT/BLOBque nadie pidió — selecciona solo lo que se usa.
Paso 6 — Transacciones y bloqueos
- InnoDB bloquea filas, no tablas, pero la isolación predeterminada
REPEATABLE READtambién toma gap locks en escaneos de rango enUPDATE/DELETE/ bloqueosSELECT— una condición de rango puede bloquear más que las filas coincidentes.READ COMMITTEDevita gap locks si no se necesitan lecturas repetibles. - Esa instantánea solo cubre un
SELECTsimple — una lectura con bloqueo en la misma transacción (SELECT ... FOR UPDATE,UPDATE,DELETE) lee los últimos datos confirmados en su lugar, por lo que puede ver una fila que la instantánea nunca mostró. No asumas que el aislamiento de instantánea también protege una lectura con bloqueo. - Mantén las transacciones cortas — una transacción abierta mantiene sus bloqueos y, bajo
REPEATABLE READ, también bloquea la purga de versiones antiguas de filas de InnoDB. SELECT ... FOR UPDATEpara bloquear filas leídas y luego escritas en una transacción (verificación de saldo antes del débito); adquiere bloqueos en un orden consistente de filas en todo el código base para evitar deadlocks.- Un deadlock (
ERROR 1213, SQLSTATE40001) es esperado bajo contención real — InnoDB retrocede automáticamente un lado; captura ese código y reintenta en lugar de "arreglarlo" con una transacción más larga.SHOW ENGINE INNODB STATUS→LATEST DETECTED DEADLOCKmuestra lo que cada lado mantenía/esperaba. - Autocommit está activado por conexión de forma predeterminada — la atomicidad de múltiples declaraciones necesita una transacción explícita (o el envoltorio del ORM), no solo adyacencia en el código.
Paso 7 — Trampas específicas de MySQL y divergencia con MariaDB
sql_modedebe incluirSTRICT_TRANS_TABLES(predeterminado desde MySQL 5.7/MariaDB 10.2) — sin él, los valores fuera de rango/tipo incorrecto se truncan silenciosamente en lugar de dar error; confirma que no se ha relajado.- Los valores de
AUTO_INCREMENTno están libres de huecos (un rollback aún consume uno) — nunca confíes en que los ids sean contiguos. time_zonede la sesión afecta la conversión deTIMESTAMPpero no deDATETIME— un pool con zonas horarias mezcladas por conexión es un error real de "hora incorrecta"; fíjala explícitamente.- MariaDB tiene sus propias extensiones sin equivalente en MySQL (
CREATE SEQUENCE,RETURNINGen escrituras) — no las uses si MySQL también es un objetivo; las funciones de ventana/CTEs necesitan MySQL 8.0+/MariaDB 10.2+, verifica primero la versión fijada. - El optimizador y la salida
EXPLAINde MariaDB no son el mismo motor que los de MySQL — vuelve a ejecutarEXPLAINen el motor real en uso en lugar de confiar en un plan ajustado en el otro.
Paso 8 — Autoverificación antes de informar como completado
Confirma que cada columna nueva/modificada tiene un tipo intencional y que el charset vuelve a utf8mb4. Confirma que cualquier índice nuevo está respaldado por un patrón de consulta real con salida EXPLAIN que muestre que se usa, y que cualquier endpoint de lista profunda/ilimitada usa keyset en lugar de OFFSET. Confirma que las escrituras de múltiples declaraciones están envueltas en una transacción explícita y que el comportamiento de aislamiento de una lectura con bloqueo es deliberado, no asumido. Si el archivo compose/CI fija MariaDB, confirma que nada agregado asume una característica solo de MySQL 8.
Qué no hacer
- No uses charset
utf8niFLOAT/DOUBLEpara dinero —utf8mb4yDECIMALson los valores predeterminados correctos. - No agregues un índice sin un patrón de consulta detrás, ni llames a una consulta "rápida" sin verificar
EXPLAIN. - No uses paginación con
OFFSETen una lista que pueda paginarse profundamente o ejecutarse sin límite — usa keyset. - No dejes una transacción abierta más tiempo del necesario para el trabajo que protege, especialmente bajo bloqueos de rango de
REPEATABLE READ. - No confíes en la contigüidad de
AUTO_INCREMENTni en la coerción de tipos implícita para la corrección. - No asumas que un objetivo MariaDB tiene características solo de MySQL 8 (
JSONnativo,utf8mb4_0900_ai_ci) sin verificar la versión fijada. - No escribas el código de migración/capa de acceso independiente del motor tú mismo — eso es trabajo de
database-schema-dev/backend-dev; este skill es el conocimiento de MySQL que ellos aplican sobre él.