El usuario app tiene acceso total a todas las bases de datos y puede hacer DROP, CREATE USER, y acceder a mysql.user. Un error de inyección SQL en cualquier endpoint compromete toda la instancia.
total_sent FLOAT en campaigns y duration FLOAT en events acumulan error de punto flotante. Para cantidades que se suman o comparan, usar DECIMAL o enteros.
La tabla users usa VARCHAR(36) como PK. En MySQL/InnoDB la PK es el índice clustered — guardar UUIDs en texto ocupa 5-6x más espacio y degrada la localidad de caché. Dos opciones:
La tabla users usa CHARSET=utf8 (alias de utf8mb3 en MySQL 8), que solo soporta hasta U+FFFF. Cualquier emoji en nombres o emails causará error o truncado silencioso.
MySQL con OFFSET 500 descarta las primeras 500 filas después de leerlas. En una tabla de 4M filas con paginación profunda esto degrada a O(n).
El UPDATE ... WHERE status='pending' ORDER BY created_at LIMIT 1 adquiere locks en todas las filas que coinciden antes de limitarlas — múltiples workers colisionan. La solución es SELECT ... FOR UPDATE SKIP LOCKED en transacción.
Inmediato (sin migración) — Seguridad y pool
- Revocar ALL PRIVILEGES del usuario app, crear usuarios con least-privilege
- Cambiar pool_recycle=240 + activar pool_pre_ping=True
- Activar slow query log: SET GLOBAL slow_query_log='ON'; SET GLOBAL long_query_time=1;
- Habilitar REQUIRE SSL para usuarios app
Sprint 1 — Índices (ALGORITHM=INPLACE, sin lock)
- Crear idx_campaigns_user_created_id (user_id, created_at, id)
- Crear idx_jobs_status_created (status, created_at)
- Índice FK en events.campaign_id
- Validar con EXPLAIN antes y después
Sprint 2 — Tipos de datos (con pt-osc / gh-ost)
- Cambiar total_sent FLOAT → BIGINT UNSIGNED
- Cambiar duration FLOAT → duration_ms BIGINT UNSIGNED
- Cambiar CHARSET=utf8 → utf8mb4 en tabla users
- Dry-run con pt-online-schema-change --dry-run
Sprint 3 — PK de users (alto impacto, ventana de mantenimiento)
- Migrar id VARCHAR(36) → BIGINT UNSIGNED con gh-ost (shadow table)
- Añadir columna external_uuid BINARY(16) para compatibilidad de API
- Doble escritura durante rollout, validar conteo antes de flip
- Rollback: la shadow table antigua permanece 24h
Sprint 4 — Worker + paginación (cambio de código)
- Desplegar keyset pagination en API de campañas
- Refactorizar worker a SELECT FOR UPDATE SKIP LOCKED
- Feature flag para rollout gradual y comparación de latencia
| Anti-Pattern | Riesgo | Tabla Afectada | Patrón Correcto | Sprint |
|---|---|---|---|---|
| ALL PRIVILEGES en runtime | Crítico | app user | Least-privilege por DB | Inmediato |
| FLOAT para métricas | Crítico | campaigns, events | BIGINT o DECIMAL | S2 |
| pool_recycle > wait_timeout | Crítico | Pool config | Recycle < wait_timeout + pre_ping | Inmediato |
| VARCHAR(36) como PK hot | Alto | users | BIGINT UNSIGNED + uuid externo | S3 |
| Deep OFFSET pagination | Alto | campaigns | Keyset (created_at, id) | S4 |
| Worker UPDATE sin SKIP LOCKED | Medio | jobs | SELECT FOR UPDATE SKIP LOCKED | S4 |
| charset utf8 (utf8mb3) | Medio | users | utf8mb4_unicode_ci | S2 |
| ENUM para campo volátil | Bajo | users | VARCHAR + CHECK constraint | S2 |