MySQL: Bases de Datos Relacionales de Alto Rendimiento
Guía técnica profunda sobre rendimiento MySQL a escala. Cubre diseño de esquemas, normalización, estrategias de indexado, optimización de queries con EXPLAIN, connection pooling, replicación, backup, particionamiento, stored procedures, transacciones, niveles de aislamiento, integración TypeORM y Prisma, columnas JSON, funciones de ventana y CTEs.
Índice
- 1. Diseño de Esquemas y Normalización
- 2. Estrategias de Indexado
- 3. Optimización de Queries
- 4. Connection Pooling
- 5. Replicación
- 6. Backup y Recuperación
- 7. Particionamiento
- 8. Transacciones y Niveles de Aislamiento
- 9. Stored Procedures
- 10. Columnas JSON, Funciones de Ventana y CTEs
- 11. Integración TypeORM con Node.js
- 12. Prisma ORM
- 13. Experiencia Real
- 14. Estrategia de Versiones MySQL y Características Modernas
1. Diseño de Esquemas y Normalización
Un buen diseño de esquema es la base del rendimiento de la base de datos. Un esquema mal diseñado no se puede arreglar agregando índices o ajustando queries. Empieza con normalización apropiada y desnormaliza solo cuando tengas evidencia medida de que se necesita.
- 1NF: Cada columna contiene valores atómicos. Sin grupos repetidos. Cada fila es única (tiene primary key)
- 2NF: Sin dependencias parciales. Cada columna no-clave depende de toda la primary key, no solo de parte
- 3NF: Sin dependencias transitivas. Las columnas no-clave dependen solo de la primary key, no de otras columnas no-clave
- Los tipos de datos importan: Usa el tipo más pequeño que sirva. INT (4 bytes) vs BIGINT (8 bytes). VARCHAR(255) vs VARCHAR(50). TIMESTAMP (4 bytes, convertido a UTC) vs DATETIME (5 bytes en 5.6+, sin conversión de timezone)
- UUIDs vs auto-increment: INTs auto-increment son más rápidos para indexar (inserciones secuenciales). UUIDs causan splits aleatorios de páginas B-tree. Si necesitas UUIDs, usa UUIDs ordenados (uuid_to_bin con flag swap)
- Soft deletes: Agrega
deleted_at DATETIME NULLen vez de hard deletes. Siempre incluíWHERE deleted_at IS NULLen queries. Considera índices parciales
-- Well-designed schema example
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
status ENUM('active','inactive','suspended') NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
UNIQUE KEY uk_email (email),
KEY idx_status_created (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE bookings (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
class_id INT UNSIGNED NOT NULL,
booked_at DATETIME NOT NULL,
status ENUM('confirmed','cancelled','completed') NOT NULL DEFAULT 'confirmed',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_user_status (user_id, status),
KEY idx_class_booked (class_id, booked_at),
CONSTRAINT fk_bookings_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_bookings_class FOREIGN KEY (class_id) REFERENCES classes(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
2. Estrategias de Indexado
Índices B-tree
B-tree es el tipo de índice por defecto de MySQL. Mantiene orden y soporta range queries, ordenamiento y agrupación. Entender cómo funcionan los índices B-tree es crítico para escribir queries rápidas.
- Regla del prefijo más a la izquierda: Un índice compuesto en (a, b, c) puede usarse para queries filtrando por (a), (a, b) o (a, b, c) -- pero NO (b), (c) o (b, c)
- El orden de columnas importa: Pon la columna más selectiva primero (mayor cardinalidad). Excepción: si una columna siempre se usa en igualdad y otra en rango, pon igualdad primero
- Índices cubrientes: Si un índice contiene todas las columnas que necesita una query, MySQL lee solo el índice (sin lookup a tabla). EXPLAIN muestra "Using index" en columna Extra
- Selectividad de índice: ratio de valores distintos sobre total de filas. Una columna boolean (selectividad ~0.5) es mal candidato para índice sola. Combina con otras columnas
- Overhead de índices: Cada índice ralentiza operaciones INSERT, UPDATE, DELETE. Tablas con muchas escrituras deberían tener índices mínimos. Tablas con muchas lecturas se benefician de más índices
-- Composite index for a common query pattern
-- Query: SELECT * FROM bookings WHERE user_id = ? AND status = 'confirmed' ORDER BY booked_at DESC
-- Optimal index:
CREATE INDEX idx_user_status_booked ON bookings (user_id, status, booked_at);
-- The index covers: WHERE (equality on user_id, status) + ORDER BY (booked_at)
-- MySQL can satisfy the entire query from this single index scan
-- Covering index example
-- Query: SELECT id, email, name FROM users WHERE status = 'active' ORDER BY created_at DESC LIMIT 20
CREATE INDEX idx_status_created_covering ON users (status, created_at, id, email, name);
-- All selected columns are in the index = no table lookup needed
Índices Hash
Los índices hash usan una función hash para búsquedas de igualdad O(1). En InnoDB, el adaptive hash index (AHI) se construye automáticamente sobre páginas B-tree accedidas frecuentemente. Las tablas MEMORY soportan índices hash explícitos.
- Solo igualdad: Los índices hash soportan
=eIN()pero no range queries (<,>,BETWEEN), ordenamiento ni coincidencias parciales - Adaptive Hash Index (AHI): InnoDB construye automáticamente índices hash en páginas hoja B-tree calientes. Monitora con
SHOW ENGINE INNODB STATUS. Desactivalo coninnodb_adaptive_hash_index=OFFsi hay contención alta - Motor MEMORY: Índices hash explícitos con
USING HASH. Útil para tablas de lookup que caben en memoria. Los datos se pierden al reiniciar
-- MEMORY table with hash index (useful for session/cache lookup)
CREATE TABLE session_cache (
session_id VARCHAR(64) NOT NULL,
user_id INT UNSIGNED NOT NULL,
data TEXT,
PRIMARY KEY (session_id) USING HASH
) ENGINE=MEMORY;
-- Check adaptive hash index status
SHOW ENGINE INNODB STATUS\G
-- Look for "Hash table size", "hash searches/s", "non-hash searches/s"
Índices Parciales y de Prefijo
Los índices parciales (índices funcionales en MySQL 8.0+) indexan solo un subconjunto de filas o una expresión computada. Los índices de prefijo indexan solo los primeros N caracteres de una columna string, reduciendo el tamaño del índice.
- Índices funcionales (8.0+): Indexa una expresión como
((CAST(metadata->>'$.type' AS CHAR(20)))). Útil para campos JSON o valores computados - Índices de prefijo:
CREATE INDEX idx_email ON users (email(20))indexa solo los primeros 20 caracteres. Índice más pequeño pero no puede usarse como índice cubriente y puede tener menor selectividad - Índices parciales simulados: MySQL no soporta nativamente cláusula WHERE en CREATE INDEX. Simula con columna generada:
ALTER TABLE orders ADD is_pending TINYINT GENERATED ALWAYS AS (IF(status='pending',1,NULL)) STORED, ADD INDEX idx_pending (is_pending)
-- Prefix index for long string columns
CREATE INDEX idx_url_prefix ON pages (url(50));
-- Functional index on JSON field (MySQL 8.0+)
ALTER TABLE events ADD INDEX idx_event_type ((CAST(metadata->>'$.event_type' AS CHAR(30))));
-- Simulated partial index for soft deletes
ALTER TABLE users
ADD active_flag TINYINT GENERATED ALWAYS AS (IF(deleted_at IS NULL, 1, NULL)) STORED,
ADD INDEX idx_active_users (active_flag, created_at);
Errores Comunes de Indexado
- Funciones en columnas indexadas:
WHERE YEAR(created_at) = 2026no puede usar un índice en created_at. Reescribí como:WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01' - Casting implícito de tipos:
WHERE phone = 12345cuando phone es VARCHAR fuerza un full table scan. Siempre coincidí los tipos - LIKE con comodín al inicio:
WHERE name LIKE '%smith'no puede usar un índice. SoloLIKE 'smith%'usa el índice - Condiciones OR:
WHERE a = 1 OR b = 2frecuentemente no puede usar un índice compuesto en (a, b). Usa UNION en su lugar, o crea índices separados - Demasiados índices: Cada índice es un B-tree separado que debe mantenerse en cada escritura. Más de 5-6 índices en una tabla es una señal de alerta
3. Optimización de Queries
EXPLAIN y Slow Query Log
EXPLAIN es la herramienta más importante para rendimiento MySQL. Muestra cómo MySQL ejecutará una query: qué índices usa, cuántas filas examina y la estrategia de join.
- Aurora RDS MySQL: Cluster Aurora único con endpoints writer + reader. Serverless v2 para escalado costo-eficiente durante horas de baja demanda. Retención de backup automatizado de 14 días con recuperación point-in-time
- Debugging de connection pool: Identifiqué y corregí agotamiento del pool causado por queryRunners de TypeORM no liberados en paths de error. Reducción del 95% de errores relacionados con conexiones agregando bloques finally apropiados y monitoreo del pool
- Optimización de queries: Usé Performance Insights y slow query log para identificar las 20 queries más lentas. Agregué índices compuestos y reescribí patrones N+1, reduciendo latencia P95 de API de 800ms a 120ms
- Migraciones TypeORM: Establecí disciplina de migraciones: migraciones generadas, code review de SQL, rollout escalonado (dev > staging > producción). Cero incidentes de pérdida de datos por cambios de esquema
- Separación lectura/escritura: Configuré replicación TypeORM con endpoints writer y reader. Moví todas las queries de reportes al endpoint reader, reduciendo CPU del primario en ~30%
-- EXPLAIN example
EXPLAIN SELECT b.id, b.booked_at, u.name, u.email
FROM bookings b
JOIN users u ON u.id = b.user_id
WHERE b.class_id = 42
AND b.status = 'confirmed'
AND b.booked_at >= '2026-03-01'
ORDER BY b.booked_at DESC
LIMIT 20;
-- Ideal EXPLAIN output:
-- +----+-------+------+--------------------------+------+----------+-------------+
-- | id | table | type | key | rows | filtered | Extra |
-- +----+-------+------+--------------------------+------+----------+-------------+
-- | 1 | b | ref | idx_class_status_booked | 85 | 100.00 | Using where |
-- | 1 | u | eq_ref | PRIMARY | 1 | 100.00 | NULL |
-- +----+-------+------+--------------------------+------+----------+-------------+
-- For this query, the optimal index is:
CREATE INDEX idx_class_status_booked ON bookings (class_id, status, booked_at);
Patrones de Optimización
- Paginación: Evita
OFFSET 10000(MySQL lee y descarta 10000 filas). Usa paginación por keyset:WHERE id > last_seen_id ORDER BY id LIMIT 20 - Optimización de COUNT:
SELECT COUNT(*) FROM large_table WHERE status = 'active'es lento sin índice cubriente. Agrega índice en (status) o mantén contadores en tabla resumen - Optimización de JOIN: Asegura que columnas de join estén indexadas. La tabla más pequeña debería ser la tabla conductora. Evita joinear más de 3-4 tablas en una query
- Subquery vs JOIN: En MySQL 8.0+, el optimizador maneja bien la mayoría de subqueries. Pero subqueries correlacionadas (que referencian query externa) pueden ser lentas -- reescribí como JOINs
- SELECT solo columnas necesarias:
SELECT *trae todas las columnas, previniendo uso de índice cubriente. Lista solo las columnas que necesitas - Operaciones batch: En vez de 1000 INSERTs individuales, usa INSERT multi-fila:
INSERT INTO t VALUES (1,'a'), (2,'b'), .... 10-50x más rápido
4. Connection Pooling
Cada conexión MySQL consume ~10MB de memoria en el servidor. Sin connection pooling, una arquitectura de microservicios con 20 servicios x 10 instancias = 200 conexiones puede agotar la base de datos. El agotamiento del pool de conexiones es uno de los incidentes más comunes en producción.
- Fórmula de tamaño del pool: Total conexiones = número_servicios x instancias_por_servicio x pool_size_por_instancia. Debe ser menor que
max_connectionsde MySQL (default: 151) - Ciclo de vida de conexiones: Las conexiones idle se mantienen vivas en el pool. Configura
waitForConnections: truepara que los requests hagan cola en vez de fallar cuando el pool está lleno - Timeout de conexión: Configura
connectTimeout(tiempo para establecer conexión) yacquireTimeout(tiempo para obtener conexión del pool). Ambos deberían ser cortos (5-10 segundos) - Timeout idle: Cierra conexiones idle más tiempo que
wait_timeout(default MySQL: 28800s / 8 horas). Reduce a 300-600s para recuperar recursos - Validación de conexiones: Habilita ping
SELECT 1antes de checkout para detectar conexiones obsoletas. Poco overhead pero previene errores "connection lost" - Separación lectura/escritura: Rutea lecturas al endpoint réplica, escrituras al endpoint primario. Duplica la capacidad efectiva de conexiones
// Node.js connection pool configuration (mysql2)
import mysql from 'mysql2/promise';
const writePool = mysql.createPool({
host: process.env.DB_WRITER_HOST, // Aurora cluster endpoint
port: 3306,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: 'myapp',
connectionLimit: 10, // Max connections in this pool
waitForConnections: true, // Queue when pool is full
queueLimit: 50, // Max queued requests (0 = unlimited)
connectTimeout: 5000, // 5s to establish connection
enableKeepAlive: true,
keepAliveInitialDelay: 30000 // TCP keepalive every 30s
});
const readPool = mysql.createPool({
host: process.env.DB_READER_HOST, // Aurora reader endpoint
port: 3306,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: 'myapp',
connectionLimit: 20, // More connections for reads
waitForConnections: true,
connectTimeout: 5000,
enableKeepAlive: true
});
// Monitor pool health
setInterval(() => {
const stats = writePool.pool;
console.log({
active: stats._allConnections.length,
idle: stats._freeConnections.length,
queued: stats._connectionQueue.length
});
}, 30000);
5. Replicación
La replicación copia datos desde un primario (master) a una o más réplicas (slaves). Habilita escalado de lecturas, alta disponibilidad y distribución geográfica.
- Replicación asíncrona: Modo por defecto de MySQL. El primario no espera que las réplicas confirmen. Riesgo: pérdida de datos si el primario falla antes de que la réplica alcance
- Replicación semi-síncrona: El primario espera que al menos una réplica confirme antes de commitear. Reduce riesgo de pérdida de datos. Agrega ~1ms de latencia por commit
- Replicación GTID (MySQL 5.6+): Los Global Transaction Identifiers simplifican el failover. Cada transacción tiene un ID único. Las réplicas pueden auto-posicionarse sin especificar archivo/posición de binlog
- Row-based vs statement-based: Row-based (ROW) replica cambios reales de datos -- más seguro y predecible. Statement-based (STATEMENT) replica sentencias SQL -- menos ancho de banda pero puede producir resultados diferentes en réplicas
- Lag de replicación: Monitora con
SHOW SLAVE STATUS\\G(Seconds_Behind_Master). Consistencia read-after-write requiere leer del primario si el lag es inaceptable - Replicación Aurora: La capa de almacenamiento compartido elimina problemas de lag de replicación. Todas las réplicas leen del mismo volumen de almacenamiento. Failover en <30 segundos sin pérdida de datos
- Group replication (MySQL 5.7.17+): Modo multi-primario o primario-único. Todos los miembros replican vía protocolo de comunicación grupal (basado en Paxos). Detección y resolución automática de conflictos. Base para MySQL InnoDB Cluster
- Aurora Global Database: Replicación cross-region con lag <1 segundo. Infraestructura dedicada para recuperación ante desastres. Failover gestionado promueve región secundaria a primaria en menos de 1 minuto
6. Backup y Recuperación
- Backups lógicos (mysqldump): Exporta sentencias SQL. Portable entre versiones. Lento para bases grandes (>10GB). Usa
--single-transactionpara InnoDB (snapshot consistente sin bloqueo) - Backups físicos (xtrabackup): Copia archivos de datos directamente. Rápido para bases grandes. Usa Percona XtraBackup para backups en caliente sin bloqueo. Soporta backups incrementales
- Backups Aurora: Backup continuo a S3. Automatizado con retención de 1-35 días. Recuperación point-in-time (PITR) a cualquier segundo dentro de la ventana de retención. Backtrack para rebobinado instantáneo sin restaurar
- Validación de backups: Restaura backups regularmente en un ambiente de prueba. Un backup no probado no es un backup. Automatiza esto con un cron semanal
- Retención de binary logs: Mantén binlogs por al menos 7 días para recuperación point-in-time. Configura
binlog_expire_logs_seconds=604800 - Backups cross-region: Para recuperación ante desastres, copia snapshots a otra región. Aurora soporta copia de snapshots cross-region y Global Database
# mysqldump with best practices
mysqldump \
--single-transaction \ # Consistent snapshot (InnoDB only)
--routines \ # Include stored procedures
--triggers \ # Include triggers
--events \ # Include scheduled events
--set-gtid-purged=OFF \ # Avoid GTID issues on import
--max-allowed-packet=256M \
--databases myapp \
| gzip > myapp_$(date +%Y%m%d_%H%M%S).sql.gz
# Percona XtraBackup (full + incremental)
xtrabackup --backup --target-dir=/backup/full --user=backup --password=$PW
xtrabackup --backup --target-dir=/backup/inc1 --incremental-basedir=/backup/full
7. Particionamiento
El particionamiento divide una tabla grande en piezas físicas más pequeñas mientras la presenta como una única tabla lógica. Mejora el rendimiento de queries en tablas muy grandes al habilitar partition pruning -- MySQL solo escanea las particiones relevantes.
- Particionamiento RANGE: Las filas se asignan a particiones basadas en el valor de una columna dentro de un rango. Ideal para datos de series temporales: particionar por mes o año. Eliminación instantánea de particiones viejas en vez de DELETE
- Particionamiento LIST: Las filas se asignan según valores de columna que coinciden con una lista. Bueno para datos categóricos como país o códigos de estado
- Particionamiento HASH: Distribuye filas uniformemente entre N particiones usando una función de módulo. Útil cuando no hay un límite natural de rango o lista
- Particionamiento KEY: Como HASH pero MySQL maneja la función hash internamente. Funciona con cualquier tipo de columna incluyendo strings
- Partition pruning: Queries que incluyen la clave de partición en el WHERE solo escanean las particiones coincidentes. Siempre incluí la clave de partición en queries para máximo beneficio
- Limitaciones: Las foreign keys no están soportadas en tablas particionadas. Todas las claves únicas (incluyendo primaria) deben incluir la clave de partición. Máximo 8192 particiones por tabla
-- RANGE partitioning by month (ideal for booking/event data)
CREATE TABLE booking_logs (
id BIGINT UNSIGNED AUTO_INCREMENT,
user_id INT UNSIGNED NOT NULL,
action VARCHAR(50) NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id, created_at),
KEY idx_user (user_id, created_at)
) PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
PARTITION p202601 VALUES LESS THAN (202602),
PARTITION p202602 VALUES LESS THAN (202603),
PARTITION p202603 VALUES LESS THAN (202604),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- Drop old partition (instant, no row-by-row DELETE)
ALTER TABLE booking_logs DROP PARTITION p202601;
-- Add new partition for next month
ALTER TABLE booking_logs REORGANIZE PARTITION p_future INTO (
PARTITION p202604 VALUES LESS THAN (202605),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- Verify partition pruning with EXPLAIN
EXPLAIN SELECT * FROM booking_logs
WHERE created_at >= '2026-03-01' AND created_at < '2026-04-01';
-- Should show "partitions: p202603" (only one partition scanned)
8. Transacciones y Niveles de Aislamiento
InnoDB soporta transacciones ACID completas. El nivel de aislamiento determina cómo las transacciones concurrentes ven los cambios de las demás, con impacto directo en rendimiento y consistencia de datos.
- READ UNCOMMITTED: Las transacciones pueden ver cambios no committed de otras transacciones (dirty reads). Casi nunca apropiado en producción. Máxima concurrencia, mínima consistencia
- READ COMMITTED: Cada SELECT ve solo datos committed al momento de ejecutar el SELECT. Sin dirty reads, pero non-repeatable reads son posibles. Usado por defecto en PostgreSQL y Oracle
- REPEATABLE READ (default MySQL): Todos los SELECTs dentro de una transacción ven el snapshot tomado en la primera lectura. Sin dirty reads, sin non-repeatable reads. Phantom reads se previenen con gap locks en InnoDB
- SERIALIZABLE: Ejecución completamente serializada. Todos los SELECTs son implícitamente
SELECT ... LOCK IN SHARE MODE. Máxima consistencia pero mínima concurrencia. Deadlocks frecuentes bajo carga - Gap locks (InnoDB): En REPEATABLE READ, InnoDB bloquea gaps entre registros de índice para prevenir phantom reads. Esto puede causar deadlocks inesperados en range queries. Cambia a READ COMMITTED si la contención de gap locks es alta
- Manejo de deadlocks: InnoDB detecta automáticamente deadlocks y hace rollback de la transacción más pequeña. Siempre maneja errores de deadlock (error 1213) en el código de aplicación con un loop de reintento (3 intentos con backoff)
-- Check current isolation level
SELECT @@transaction_isolation;
-- Set session isolation level
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Locking reads for inventory/booking systems
START TRANSACTION;
-- Lock the row to prevent concurrent overbooking
SELECT capacity, current_bookings
FROM classes WHERE id = 42 FOR UPDATE;
-- Application checks: if current_bookings < capacity, proceed
UPDATE classes SET current_bookings = current_bookings + 1 WHERE id = 42;
INSERT INTO bookings (user_id, class_id, status) VALUES (7, 42, 'confirmed');
COMMIT;
-- Deadlock retry pattern (application-side pseudocode)
-- for attempt in range(3):
-- try: execute_transaction(); break
-- catch DeadlockError: sleep(0.1 * 2^attempt); continue
9. Stored Procedures
Los stored procedures ejecutan lógica SQL en el servidor de base de datos, reduciendo round trips de red. Son útiles para operaciones complejas multi-paso, migraciones de datos y para imponer reglas de negocio a nivel de base de datos.
- Cuándo usar: Operaciones multi-paso que se benefician de menos round trips (procesamiento batch, limpieza de datos). Imponer invariantes que deben cumplirse sin importar qué aplicación escribe datos
- Cuándo evitar: Lógica de negocio que cambia frecuentemente (más difícil de versionar/desplegar que código de aplicación). Cálculos complejos más apropiados para procesamiento en la capa de aplicación
- Manejo de errores: Usa
DECLARE HANDLERpara manejo de excepciones. Siempre manejaSQLEXCEPTIONpara hacer rollback de trabajo parcial. UsaSIGNALpara lanzar errores custom - Seguridad: Contexto de seguridad
DEFINERvsINVOKER. DEFINER ejecuta con los privilegios del usuario que lo creó. Usa INVOKER cuando los llamadores deben usar sus propios privilegios - Rendimiento: Los stored procedures se compilan y cachean en el query cache. La primera ejecución puede ser más lenta; las llamadas subsiguientes reusan el plan cacheado
DELIMITER //
-- Stored procedure for batch archiving old bookings
CREATE PROCEDURE archive_old_bookings(IN cutoff_date DATE, OUT archived_count INT)
SQL SECURITY DEFINER
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
-- Move old completed bookings to archive table
INSERT INTO bookings_archive
SELECT * FROM bookings
WHERE status = 'completed' AND booked_at < cutoff_date;
SET archived_count = ROW_COUNT();
DELETE FROM bookings
WHERE status = 'completed' AND booked_at < cutoff_date;
COMMIT;
END //
DELIMITER ;
-- Call the procedure
CALL archive_old_bookings('2025-01-01', @count);
SELECT @count AS archived_rows;
10. Columnas JSON, Funciones de Ventana y CTEs
Columnas JSON
MySQL 5.7+ soporta columnas JSON nativas con validación, indexado y un amplio conjunto de funciones. Las columnas JSON son ideales para datos semi-estructurados que varían por fila -- metadata, preferencias, feature flags -- sin requerir cambios de esquema.
- JSON vs TEXT: Las columnas JSON se validan en INSERT/UPDATE (rechazan JSON malformado). Se almacenan en formato binario optimizado para acceso rápido. TEXT no tiene validación y requiere parsing en cada lectura
- Acceder valores:
->retorna JSON,->>retorna texto sin comillas. Ejemplo:metadata->>'$.plan'retorna el valor de plan como string - Indexar JSON: Crea una columna generada e indexála, o usa índices funcionales (8.0+):
ALTER TABLE t ADD INDEX ((CAST(data->>'$.type' AS CHAR(30)))) - Funciones útiles:
JSON_EXTRACT,JSON_SET,JSON_REMOVE,JSON_ARRAYAGG,JSON_OBJECTAGG,JSON_TABLE(8.0+ -- convierte array JSON a filas)
-- Table with JSON column for flexible metadata
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
metadata JSON DEFAULT NULL,
KEY idx_plan ((CAST(metadata->>'$.plan' AS CHAR(20))))
);
-- Insert with JSON
INSERT INTO users (email, metadata) VALUES
('[email protected]', '{"plan": "pro", "features": ["analytics", "exports"], "trial_ends": "2026-04-01"}');
-- Query JSON fields
SELECT email, metadata->>'$.plan' AS plan
FROM users WHERE metadata->>'$.plan' = 'pro';
-- Update nested JSON value
UPDATE users SET metadata = JSON_SET(metadata, '$.plan', 'enterprise')
WHERE id = 1;
-- JSON_TABLE: expand JSON array into rows (8.0+)
SELECT u.email, f.feature
FROM users u,
JSON_TABLE(u.metadata, '$.features[*]' COLUMNS (feature VARCHAR(50) PATH '$')) AS f;
Funciones de Ventana (MySQL 8.0+)
Las funciones de ventana realizan cálculos sobre un conjunto de filas relacionadas con la fila actual, sin colapsar en una sola fila de salida como GROUP BY. Esenciales para rankings, totales acumulados y analíticas comparativas.
- ROW_NUMBER(): Asigna un número secuencial único. Útil para paginación, deduplicación y queries top-N por grupo
- RANK() / DENSE_RANK(): Ranking con empates. RANK salta números después de empates; DENSE_RANK no
- LAG() / LEAD(): Acceder valores de filas anteriores o siguientes. Útil para comparaciones período-sobre-período
- SUM() OVER / AVG() OVER: Totales acumulados y promedios móviles sin self-joins
-- Top 3 most active users per gym (by booking count)
SELECT * FROM (
SELECT
u.name,
g.gym_name,
COUNT(b.id) AS booking_count,
ROW_NUMBER() OVER (PARTITION BY g.id ORDER BY COUNT(b.id) DESC) AS rn
FROM bookings b
JOIN users u ON u.id = b.user_id
JOIN classes c ON c.id = b.class_id
JOIN gyms g ON g.id = c.gym_id
WHERE b.booked_at >= '2026-01-01'
GROUP BY u.id, g.id
) ranked WHERE rn <= 3;
-- Month-over-month booking growth using LAG
SELECT
DATE_FORMAT(booked_at, '%Y-%m') AS month,
COUNT(*) AS bookings,
LAG(COUNT(*)) OVER (ORDER BY DATE_FORMAT(booked_at, '%Y-%m')) AS prev_month,
ROUND((COUNT(*) - LAG(COUNT(*)) OVER (ORDER BY DATE_FORMAT(booked_at, '%Y-%m')))
/ LAG(COUNT(*)) OVER (ORDER BY DATE_FORMAT(booked_at, '%Y-%m')) * 100, 1) AS growth_pct
FROM bookings
GROUP BY DATE_FORMAT(booked_at, '%Y-%m')
ORDER BY month;
Common Table Expressions (CTEs)
Los CTEs (cláusula WITH, MySQL 8.0+) definen conjuntos de resultados temporales con nombre que existen durante la duración de una sola query. Mejoran legibilidad, habilitan queries recursivas y pueden reemplazar subqueries complejas y tablas temporales.
- CTEs no recursivos: Subqueries con nombre que hacen queries complejas legibles. MySQL puede materializar o fusionarlos dependiendo del optimizador
- CTEs recursivos: CTEs auto-referenciados para datos jerárquicos (organigramas, árboles de categorías, recorrido de grafos). Usa
WITH RECURSIVE - Rendimiento: En MySQL 8.0, los CTEs referenciados múltiples veces pueden materializarse (computados una vez). Usa EXPLAIN para verificar. Para CTEs de uso único, el optimizador típicamente los fusiona en la query principal
-- CTE for readable multi-step reporting
WITH monthly_bookings AS (
SELECT
DATE_FORMAT(booked_at, '%Y-%m') AS month,
class_id,
COUNT(*) AS total
FROM bookings
WHERE status = 'completed'
GROUP BY month, class_id
),
class_rankings AS (
SELECT
month, class_id, total,
RANK() OVER (PARTITION BY month ORDER BY total DESC) AS rnk
FROM monthly_bookings
)
SELECT month, class_id, total
FROM class_rankings WHERE rnk <= 5;
-- Recursive CTE: category tree traversal
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, 0 AS depth
FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.depth + 1
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT CONCAT(REPEAT(' ', depth), name) AS tree_view
FROM category_tree ORDER BY depth, name;
11. Integración TypeORM con Node.js
Configuración y Diseño de Entidades
TypeORM es el ORM TypeScript más popular para Node.js. Soporta patrones Active Record y Data Mapper, migraciones y query builder. La configuración apropiada es esencial para rendimiento en producción.
// TypeORM data source configuration for Aurora MySQL
import { DataSource } from 'typeorm';
export const AppDataSource = new DataSource({
type: 'mysql',
replication: {
master: {
host: process.env.DB_WRITER_HOST,
port: 3306,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: 'myapp'
},
slaves: [{
host: process.env.DB_READER_HOST,
port: 3306,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: 'myapp'
}]
},
entities: ['dist/entities/**/*.js'],
migrations: ['dist/migrations/**/*.js'],
logging: ['error', 'warn', 'migration'], // Never log 'query' in production
maxQueryExecutionTime: 1000, // Log queries taking >1s
extra: {
connectionLimit: 10,
waitForConnections: true,
connectTimeout: 5000
}
});
// Entity example with proper decorators
@Entity('bookings')
@Index('idx_user_status', ['userId', 'status'])
export class Booking {
@PrimaryGeneratedColumn('increment')
id: number;
@Column({ type: 'int', unsigned: true })
userId: number;
@Column({ type: 'enum', enum: BookingStatus, default: BookingStatus.CONFIRMED })
status: BookingStatus;
@Column({ type: 'datetime' })
bookedAt: Date;
@CreateDateColumn()
createdAt: Date;
@ManyToOne(() => User, user => user.bookings)
@JoinColumn({ name: 'user_id' })
user: User;
}
Patrones de Producción
- Query builder sobre find: Usa QueryBuilder para queries complejas. La API
find()genera SQL ineficiente para joins y condiciones - Transacciones: Siempre usa
queryRunnerpara transacciones. Libera el runner en un bloque finally para prevenir fugas de conexiones - Migraciones: Nunca uses
synchronize: trueen producción. Genera migraciones contypeorm migration:generatey revisa el SQL antes de ejecutar - Queries N+1: Usa
leftJoinAndSelecto la opciónrelationspara eager-load. Nunca accedas a relaciones lazy en un loop - Queries raw: Para reportes complejos u operaciones bulk, baja a SQL raw vía método
query(). Las abstracciones de TypeORM no siempre son óptimas
// Transaction pattern with proper cleanup
async function createBooking(userId: number, classId: number): Promise<Booking> {
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Check class capacity (SELECT ... FOR UPDATE to lock the row)
const classEntity = await queryRunner.manager
.createQueryBuilder(GymClass, 'c')
.setLock('pessimistic_write')
.where('c.id = :classId', { classId })
.getOne();
if (!classEntity || classEntity.currentBookings >= classEntity.capacity) {
throw new Error('Class is full');
}
// Create booking
const booking = queryRunner.manager.create(Booking, {
userId, classId: classEntity.id, bookedAt: new Date(), status: BookingStatus.CONFIRMED
});
await queryRunner.manager.save(booking);
// Increment counter
await queryRunner.manager.increment(GymClass, { id: classId }, 'currentBookings', 1);
await queryRunner.commitTransaction();
return booking;
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release(); // ALWAYS release the connection
}
}
12. Prisma ORM
Prisma es un ORM TypeScript moderno con esquema declarativo, cliente type-safe auto-generado y sistema de migraciones integrado. Toma un enfoque schema-first que difiere fundamentalmente del modelo basado en decoradores de TypeORM.
- Esquema Prisma: Un único archivo
schema.prismadefine modelos, relaciones y datasource. El cliente generado provee type safety completo -- imposible hacer query a una columna que no existe - Prisma Migrate: Migraciones basadas en esquema. Cambia el archivo de esquema, ejecuta
prisma migrate devy Prisma genera la migración SQL. Revisa antes de aplicar en producción conprisma migrate deploy - Connection pooling: Prisma usa su propio pool de conexiones. Configura
connection_limiten la URL del datasource. Para ambientes serverless, usa Prisma Accelerate o pooling externo estilo PgBouncer - Transacciones: Transacciones interactivas con
prisma.$transaction(async (tx) => { ... }). Soporta configuración de timeout y nivel de aislamiento. Rollback automático en error - Queries raw:
prisma.$queryRawpara SQL raw tipado yprisma.$executeRawpara mutaciones. Usa tagged template literals para parametrización segura - TypeORM vs Prisma: TypeORM ofrece más flexibilidad (query builder, Active Record). Prisma ofrece mejor type safety y experiencia de desarrollo. Elige Prisma para proyectos nuevos; TypeORM cuando necesitas queries dinámicas complejas
// schema.prisma
datasource db {
provider = "mysql"
url = env("DATABASE_URL") // mysql://user:pass@host:3306/myapp?connection_limit=10
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String @unique @db.VarChar(255)
name String @db.VarChar(100)
status UserStatus @default(ACTIVE)
bookings Booking[]
createdAt DateTime @default(now()) @map("created_at")
deletedAt DateTime? @map("deleted_at")
@@index([status, createdAt])
@@map("users")
}
model Booking {
id Int @id @default(autoincrement())
userId Int @map("user_id")
classId Int @map("class_id")
bookedAt DateTime @map("booked_at")
status BookingStatus @default(CONFIRMED)
user User @relation(fields: [userId], references: [id])
@@index([userId, status])
@@index([classId, bookedAt])
@@map("bookings")
}
enum UserStatus { ACTIVE INACTIVE SUSPENDED }
enum BookingStatus { CONFIRMED CANCELLED COMPLETED }
// Prisma Client usage with type-safe queries
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Type-safe query -- TypeScript errors if column doesn't exist
const activeUsers = await prisma.user.findMany({
where: { status: 'ACTIVE', deletedAt: null },
include: { bookings: { where: { status: 'CONFIRMED' } } },
orderBy: { createdAt: 'desc' },
take: 20
});
// Interactive transaction with timeout
const booking = await prisma.$transaction(async (tx) => {
const gymClass = await tx.gymClass.findUniqueOrThrow({
where: { id: classId }
});
if (gymClass.currentBookings >= gymClass.capacity) {
throw new Error('Class is full');
}
const newBooking = await tx.booking.create({
data: { userId, classId, bookedAt: new Date(), status: 'CONFIRMED' }
});
await tx.gymClass.update({
where: { id: classId },
data: { currentBookings: { increment: 1 } }
});
return newBooking;
}, { timeout: 5000, isolationLevel: 'Serializable' });
13. Experiencia Real
En producción, gestioné Aurora RDS MySQL como base de datos principal para todos los servicios, manejando millones de reservas, pagos y registros de usuarios en múltiples países latinoamericanos.
- Aurora RDS MySQL: Cluster Aurora único con endpoints writer + reader. Serverless v2 para escalado costo-eficiente durante horas de baja demanda. Retención de backup automatizado de 14 días con recuperación point-in-time
- Debugging de connection pool: Identifiqué y corregí agotamiento del pool causado por queryRunners de TypeORM no liberados en paths de error. Reducción del 95% de errores relacionados con conexiones agregando bloques finally apropiados y monitoreo del pool
- Optimización de queries: Usé Performance Insights y slow query log para identificar las 20 queries más lentas. Agregué índices compuestos y reescribí patrones N+1, reduciendo latencia P95 de API de 800ms a 120ms
- Migraciones TypeORM: Establecí disciplina de migraciones: migraciones generadas, code review de SQL, rollout escalonado (dev > staging > producción). Cero incidentes de pérdida de datos por cambios de esquema
- Separación lectura/escritura: Configuré replicación TypeORM con endpoints writer y reader. Moví todas las queries de reportes al endpoint reader, reduciendo CPU del primario en ~30%
14. Estrategia de Versiones MySQL y Características Modernas
Estrategia de Versiones MySQL
MySQL ahora sigue un modelo de lanzamiento de doble vía. MySQL 8.4 es la versión de Soporte a Largo Plazo (LTS) con soporte extendido y garantías de estabilidad. MySQL 9.x es la vía Innovation con cadencia de lanzamiento de 3 meses, introduciendo nuevas características que eventualmente llegan al siguiente LTS. MySQL 8.0 llega a Fin de Vida en abril de 2026 -- planifica tu migración ahora.
- MySQL 8.4 LTS: Recomendado para cargas de trabajo de producción. Recibe correcciones de bugs y parches de seguridad durante todo el ciclo de vida de soporte. La ruta de actualización desde 8.0 es directa con
mysql_upgradeo actualización in-place - MySQL 9.x Innovation: Cadencia de lanzamiento de 3 meses. Nuevas características como el tipo VECTOR, stored programs en JavaScript y optimizador mejorado. Se requiere actualización a cada nueva versión Innovation. Ideal para desarrollo, testing y cargas de trabajo no críticas
- MySQL 8.0 EOL (Abril 2026): Sin más correcciones de bugs ni parches de seguridad después de abril 2026. Permanecer en 8.0 después del EOL expone tu base de datos a vulnerabilidades sin parchear. Migra a 8.4 LTS para estabilidad o 9.x para las últimas características
mysqlcheck --check-upgrade, revisa las características deprecadas de las que dependes y valida la compatibilidad de tu ORM. Aurora MySQL 8.0-compatible también requerirá migración a Aurora MySQL 8.4-compatible.Tipo de Dato VECTOR (MySQL 9.0+)
MySQL 9.0 introduce un tipo de dato VECTOR nativo para almacenar embeddings de AI/ML. Los vectores se almacenan como arrays de floats de 4 bytes, optimizados para operaciones de búsqueda por similitud. Esto es esencial para construir búsqueda semántica, motores de recomendación y pipelines de Retrieval-Augmented Generation (RAG) directamente en MySQL sin requerir una base de datos vectorial separada.
-- VECTOR data type (MySQL 9.0+)
CREATE TABLE documents (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(500) NOT NULL,
content TEXT NOT NULL,
embedding VECTOR(1536) NOT NULL, -- OpenAI ada-002 dimension
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_created (created_at)
);
-- Insert with vector literal
INSERT INTO documents (title, content, embedding)
VALUES ('Node.js Guide', 'Event loop internals...', TO_VECTOR('[0.0123, -0.0456, ...]'));
-- Cosine similarity search for RAG
SELECT id, title,
1 - DISTANCE(embedding, TO_VECTOR(?), 'COSINE') AS similarity
FROM documents
ORDER BY DISTANCE(embedding, TO_VECTOR(?), 'COSINE')
LIMIT 10;
Stored Programs en JavaScript (MySQL 9.0+, Enterprise)
MySQL 9.0 Enterprise introduce la capacidad de escribir stored procedures y funciones en JavaScript usando un runtime GraalVM/ES2023. Esto permite a los desarrolladores usar sintaxis JavaScript familiar, características modernas del lenguaje y patrones de módulos estilo npm para lógica de base de datos del lado servidor, manteniendo los beneficios de rendimiento de ejecutar código cerca de los datos.
-- JavaScript stored function (MySQL 9.0+ Enterprise)
CREATE FUNCTION calculate_bmi(weight_kg DOUBLE, height_m DOUBLE)
RETURNS DOUBLE
LANGUAGE JAVASCRIPT AS $$
if (height_m <= 0) throw new Error('Height must be positive');
const bmi = weight_kg / (height_m * height_m);
return Math.round(bmi * 10) / 10;
$$;
-- Use like any SQL function
SELECT name, calculate_bmi(weight_kg, height_m) AS bmi
FROM users
WHERE calculate_bmi(weight_kg, height_m) > 25;
MySQL 9.7.0 LTS y HeatWave AI (Abril 2026)
MySQL 9.7.0 LTS fue lanzado el 21 de abril de 2026, convirtiéndose en el nuevo release de Soporte a Largo Plazo en el track 9.x. Las adiciones clave incluyen rotación de audit log basada en tiempo (rotar logs por intervalo en vez de tamaño de archivo), la variable replica_allow_higher_version_source que permite a las réplicas aceptar replicación de fuentes de versión superior durante upgrades rolling, y optimizaciones expandidas de funciones de ventana. Ocho capacidades previamente limitadas a Enterprise Edition ahora están disponibles en Community Edition en cuatro áreas técnicas principales, y Dynamic Data Masking es nuevo en Enterprise Edition. MySQL 9.6.0 (enero de 2026) introdujo el componente modular de Audit Log que divide el log de auditoría monolítico en piezas más pequeñas y manejables. MySQL 8.0 ha llegado a Fin de Vida a partir de abril de 2026 (versión final 8.0.46) -- todos los usuarios de 8.0 deben migrar a 8.4 LTS o al track 9.x.
La integración de HeatWave AI en MySQL 9.4+ agrega Natural Language to Machine Learning (NL2ML), habilitando una interfaz intuitiva de lenguaje natural para flujos de trabajo de ML. La gestión mejorada de memoria de AutoML chequea automáticamente el uso y libera recursos según niveles de severidad. AutoML ahora también registra hiperparámetros de modelos entrenados, mejorando transparencia y reproducibilidad. Estas funcionalidades están disponibles en HeatWave en OCI, incluyendo el tier Always Free.