El charset utf8 de MySQL existe desde la versión 3.23 y tiene un defecto de diseño que lleva décadas causando problemas: solo soporta hasta 3 bytes por carácter. UTF-8 real puede necesitar hasta 4 bytes para representar ciertos caracteres, y eso deja fuera a todos los emojis y a bastantes símbolos matemáticos, monedas y escrituras asiáticas poco comunes.
El resultado práctico es que intentas guardar un ???? en una columna utf8 y MySQL te lo convierte en ???? sin avisar, o directamente lanza un error si tienes el modo estricto activado. No es un bug que vayas a ver en los logs hasta que alguien se queja de que su comentario llegó mutilado.
MySQL creó utf8mb4 en la versión 5.5.3 para solucionar esto. El nombre significa "UTF-8 multibyte de 4 bytes" y soporta el rango completo de Unicode, incluyendo todos los emojis actuales y futuros. Si tu aplicación maneja texto generado por usuarios, deberías estar en utf8mb4.
Detectar si tienes el problema
Antes de tocar nada, comprueba el estado actual de tu servidor y tus tablas.
Ver el charset del servidor
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';
Lo que te interesa es character_set_server y character_set_database. Si ves utf8, tienes trabajo por delante. Si ves utf8mb4, el servidor ya está bien configurado, pero puede que las tablas antiguas todavía no lo estén.
Ver el charset de una tabla concreta
SHOW CREATE TABLE mi_tablaG
Al final del resultado verás algo como DEFAULT CHARSET=utf8 o DEFAULT CHARSET=utf8mb4. Comprueba también las columnas de texto: una tabla puede tener charset utf8mb4 pero columnas individuales que se crearon con utf8 y no se tocaron.
La prueba rápida del emoji
INSERT INTO mi_tabla (columna_texto) VALUES ('????');
SELECT columna_texto FROM mi_tabla ORDER BY id DESC LIMIT 1;
Si el resultado es ???? o da error, el charset es utf8. Si aparece el emoji tal cual, ya estás en utf8mb4.
Plan de migración paso a paso
Paso 0: el backup
Sin backup no hay paso siguiente. Haz un mysqldump completo antes de empezar. Con bases de datos grandes puedes usar --single-transaction para no bloquear lecturas en InnoDB:
mysqldump --single-transaction --routines --triggers -u usuario -p nombre_bd > backup_antes_utf8mb4.sql
Paso 1: cambiar my.cnf
Añade o modifica estas líneas en /etc/mysql/my.cnf (o my.ini en Windows):
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
[client]
default-character-set = utf8mb4
Esto afecta a las conexiones nuevas y a las tablas que se creen a partir de ahora. Las tablas existentes no cambian solas.
Paso 2: reiniciar MySQL
Necesitas reiniciar el servicio para que apliquen los cambios de my.cnf. Si tienes réplicas de lectura, migra una réplica primero y verifica que funciona antes de tocar la primaria:
systemctl restart mysql
Paso 3: convertir la base de datos
ALTER DATABASE mi_bd CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Esto cambia el charset por defecto para las tablas nuevas. Las tablas existentes siguen sin tocar.
Paso 4: convertir cada tabla
ALTER TABLE mi_tabla CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Repite esto para cada tabla. Si tienes muchas, puedes generar las sentencias automáticamente:
SELECT CONCAT('ALTER TABLE ', TABLE_NAME, ' CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;')
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'mi_bd'
AND TABLE_TYPE = 'BASE TABLE';
Copia el resultado y ejecútalo. Con tablas grandes, este ALTER TABLE bloquea escrituras mientras se procesa. Si no puedes permitirte eso, ve al apartado de migración sin tiempo de actividad.
Paso 5: verificar los índices
Después del CONVERT TO, comprueba que no hay errores de longitud de índice. En MySQL 5.6 con InnoDB hay un límite de 767 bytes por columna indexada, y con utf8mb4 ese límite se alcanza antes.
El problema de los índices
Con utf8 cada carácter ocupa como máximo 3 bytes, así que una columna VARCHAR(255) puede llegar a 765 bytes en un índice, justo por debajo del límite de 767 bytes de InnoDB. Con utf8mb4 cada carácter puede ocupar 4 bytes, y esa misma columna llegaría a 1020 bytes, superando el límite.
Hay dos formas de resolverlo:
- Activar innodb_large_prefix: disponible desde MySQL 5.6 y activado por defecto en MySQL 5.7+. Requiere también
innodb_file_format=BarracudayROW_FORMAT=DYNAMICen las tablas. Con esto el límite sube a 3072 bytes y el problema desaparece. - Reducir la longitud de las columnas indexadas: con
utf8mb4, el máximo seguro sininnodb_large_prefixesVARCHAR(191)(191 × 4 = 764 bytes, por debajo del límite). Si tienes columnasVARCHAR(255)con índice único o clave primaria en MySQL 5.6, toca reducirlas.
En MySQL 5.7 y 8.0 esto ya no es un problema porque innodb_large_prefix viene activado por defecto. Si estás en 5.6, comprueba la versión exacta y actívalo en my.cnf si no está.
Ajustar la conexión PHP
Aunque hayas migrado todas las tablas a utf8mb4, si PHP negocia la conexión en utf8, los datos de 4 bytes seguirán fallando o corrompiendo. El charset de la conexión determina cómo MySQL interpreta lo que PHP envía.
Con PDO
$dsn = 'mysql:host=localhost;dbname=mi_bd;charset=utf8mb4';
$pdo = new PDO($dsn, $usuario, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
El charset=utf8mb4 en el DSN es todo lo que necesitas. PDO lanza el SET NAMES utf8mb4 automáticamente al conectar.
Con mysqli
$mysqli = new mysqli($host, $usuario, $password, $bd);
$mysqli->set_charset('utf8mb4');
Usa set_charset, no query("SET NAMES utf8mb4"). La diferencia es que set_charset también actualiza el estado interno de la extensión mysqli, lo que afecta al escapado de cadenas.
Con Laravel
En config/database.php, dentro de la conexión MySQL:
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
Laravel lleva esto configurado por defecto desde la versión 5.4 para nuevos proyectos. Si tienes un proyecto antiguo, comprueba que el valor no siga siendo utf8. Para más detalles sobre migraciones de base de datos en Laravel y cómo gestionar el esquema de forma ordenada, tienes una guía completa en programacion.net.
Migración sin tiempo de actividad
Para tablas grandes, el ALTER TABLE ... CONVERT TO CHARACTER SET puede tardar minutos o incluso horas y bloquea escrituras durante todo ese tiempo. Si no puedes permitirte esa ventana, tienes dos opciones.
pt-online-schema-change (Percona Toolkit)
Esta herramienta crea una tabla nueva con el charset correcto, copia los datos en segundo plano usando triggers para capturar los cambios que van llegando, y al terminar hace un RENAME atómico. Las escrituras no se bloquean en ningún momento:
pt-online-schema-change
--alter "CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
--execute
D=mi_bd,t=mi_tabla
Con tablas de varios millones de filas, esto puede tardar igualmente mucho tiempo, pero sin bloquear la aplicación.
Estrategia con réplicas
Si usas réplicas de lectura, el flujo habitual es: migrar una réplica primero, hacer que la aplicación apunte a esa réplica para lecturas, verificar que todo funciona, promover la réplica a primaria y migrar la antigua primaria como réplica. Así el tiempo de actividad es mínimo, lo que afecta es solo el failover.
Verificación post-migración
Tras la migración, haz estas comprobaciones antes de dar el proceso por terminado.
Desde la consola de MySQL, verifica que un emoji se guarda con 4 bytes:
SELECT HEX('????');
El resultado debe ser F09F9880. Si ves 3F3F3F3F (cuatro signos de interrogación en hex), la conexión sigue en utf8.
Desde PHP, haz una inserción real y lee de vuelta:
$mysqli->set_charset('utf8mb4');
$stmt = $mysqli->prepare("INSERT INTO prueba (texto) VALUES (?)");
$emoji = 'Funciona: ???? ????';
$stmt->bind_param('s', $emoji);
$stmt->execute();
$resultado = $mysqli->query("SELECT texto FROM prueba ORDER BY id DESC LIMIT 1");
$fila = $resultado->fetch_assoc();
echo $fila['texto'];
Si el emoji aparece correctamente, la migración está bien. Revisa también los logs de errores de MySQL durante las primeras 24 horas después de la migración: si hay tablas o columnas que se te pasaron por alto, aparecerán ahí.
La migración a utf8mb4 es uno de esos cambios que parece grande pero que con un backup decente y el proceso ordenado no da problemas. Si tienes dudas sobre cómo aplicar estas mismas buenas prácticas al resto de tu código, en programacion.net encontrarás más sobre PHP moderno: buenas prácticas en 2025-2026.
Imagen: Pexels / Digital Buggu
