Tema
utf8mb4 y collation
¿Has visto un sitio que muestra "MarÃa" en vez de "María", "ñ" en vez de "ñ" o signos de interrogación donde debería haber un emoji? Es uno de los problemas más comunes (y vergonzosos frente a un cliente) en proyectos con MySQL. En esta lección entenderás qué son el charset y la collation, por qué utf8 no es UTF-8 de verdad, cómo configurarlo todo correctamente y cómo reparar datos dañados.
Concepto
- Charset (character set, juego de caracteres): cómo se convierten las letras en bytes. Es el "alfabeto" que entiende la columna.
- Collation (intercalación): cómo se comparan y ordenan esos caracteres. ¿"a" es igual a "A"? ¿"á" es igual a "a"? ¿"ñ" va después de "n"?
Analogía: el charset es el idioma en que está escrito un libro; la collation son las reglas del diccionario para ordenarlo alfabéticamente.
txt
Texto: "Ñandú 🐦"
│
│ charset utf8mb4
▼
Bytes: C3 91 61 6E 64 C3 BA 20 F0 9F 90 A6
└Ñ─┘ a n d └ú─┘ ␠ └───🐦────┘
4 bytesEl gran engaño: utf8 no es UTF-8
UTF-8 real usa de 1 a 4 bytes por carácter. Pero en MySQL:
| Charset | Bytes por carácter | ¿Emojis? | ¿Algunos caracteres chinos, símbolos matemáticos? |
|---|---|---|---|
latin1 | 1 | No | No (solo Europa occidental) |
utf8 = utf8mb3 | Hasta 3 | No | No |
utf8mb4 | Hasta 4 | Sí | Sí — es UTF-8 completo |
Cuando MySQL añadió utf8 (2003), lo limitó a 3 bytes. Los emojis (😀 🛒 ❤️🔥) necesitan 4. El resultado:
sql
CREATE TABLE prueba (texto VARCHAR(50)) CHARACTER SET utf8mb3;
INSERT INTO prueba VALUES ('Me encanta 😍');
-- ERROR 1366: Incorrect string value: '\xF0\x9F\x98\x8D' for column 'texto'
-- (o, sin modo estricto, guarda 'Me encanta ?')Regla de oro
Usa siempre utf8mb4. Nunca utf8 ni utf8mb3. MySQL 8 ya usa utf8mb4 por defecto, pero bases creadas en 5.7 (o importadas de un hosting viejo) suelen estar en latin1 o utf8.
Collations
El nombre de una collation dice mucho:
txt
utf8mb4 _ 0900 _ ai _ ci
│ │ │ └─ ci = case insensitive (A = a) | cs = distingue mayúsculas
│ │ └────── ai = accent insensitive (á = a) | as = distingue acentos
│ └───────────── versión del estándar Unicode (UCA 9.0.0)
└───────────────────── charset| Collation | Mayúsc. | Acentos | Notas |
|---|---|---|---|
utf8mb4_0900_ai_ci | No distingue | No distingue | Por defecto en MySQL 8. Recomendada |
utf8mb4_0900_as_cs | Distingue | Distingue | Comparaciones estrictas |
utf8mb4_es_0900_ai_ci | No | No | Reglas del español (orden de la ñ) |
utf8mb4_unicode_ci | No | No | Estándar antiguo (UCA 4.0). Compatible con MariaDB y MySQL 5.7 |
utf8mb4_general_ci | No | No | Más vieja y menos precisa; evítala |
utf8mb4_bin | Distingue | Distingue | Compara bytes. Para tokens, hashes, códigos |
MariaDB y MySQL 5.7
MariaDB y MySQL 5.7 no tienen las collations _0900_. Si vas a mover datos entre MySQL 8 y MariaDB, o tu hosting usa MariaDB, usa utf8mb4_unicode_ci (MariaDB 10.10+ tiene además utf8mb4_uca1400_ai_ci). Importar un respaldo con utf8mb4_0900_ai_ci en MariaDB da el error Unknown collation.
La collation cambia los resultados
sql
CREATE TABLE nombres (nombre VARCHAR(50)) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
INSERT INTO nombres VALUES ('José'), ('jose'), ('JOSE'), ('Jose');
SELECT COUNT(*) FROM nombres WHERE nombre = 'jose';
-- 4 (ai_ci: ignora mayúsculas y acentos)
SELECT COUNT(*) FROM nombres WHERE nombre = 'jose' COLLATE utf8mb4_0900_as_cs;
-- 1 (solo 'jose' exacto)
SELECT COUNT(*) FROM nombres WHERE nombre = 'jose' COLLATE utf8mb4_bin;
-- 1Consecuencias en el mundo real:
- Con
_ci, unUNIQUEenemailimpide registrarAna@correo.comsi ya existeana@correo.com. ¡Normalmente es lo que quieres! - Con
_ai_ci, unUNIQUEennombre_usuarioconsidera iguales "jose" y "josé". - Un buscador con
_ai_ciencuentra "canción" al buscar "cancion". ¡Excelente para usuarios! - Para tokens de API o contraseñas hasheadas usa
utf8mb4_bin: "abc" y "ABC" deben ser distintos.
Los 4 niveles de configuración
El charset se define en cascada; cada nivel hereda del anterior si no se especifica:
txt
Servidor ──► Base de datos ──► Tabla ──► Columna
+
Conexión (cliente ↔ servidor) ← ¡el que más se olvida!1. Servidor (my.cnf / my.ini)
ini
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_0900_ai_ci
[client]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb42. Base de datos
sql
CREATE DATABASE tienda CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- Cambiar una existente (solo afecta a las tablas NUEVAS)
ALTER DATABASE tienda CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;3. Tabla y 4. columna
sql
CREATE TABLE usuarios (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL, -- hereda de la tabla
email VARCHAR(150) NOT NULL UNIQUE,
api_token CHAR(64) CHARACTER SET ascii COLLATE ascii_bin, -- columna específica
bio TEXT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;5. La conexión
Aunque todo lo anterior esté perfecto, si la conexión de tu app usa latin1, los datos se guardan mal. Siempre indica el charset al conectar:
php
<?php
$pdo = new PDO(
'mysql:host=127.0.0.1;dbname=tienda;charset=utf8mb4',
$usuario,
$clave
);js
const pool = mysql.createPool({
host: '127.0.0.1',
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: 'tienda',
charset: 'utf8mb4'
});env
DB_CONNECTION=mysql
# config/database.php ya trae:
# 'charset' => 'utf8mb4',
# 'collation' => 'utf8mb4_unicode_ci',En SQL puro: SET NAMES utf8mb4; al iniciar la sesión.
Y no olvides el HTML: tus páginas deben declarar <meta charset="UTF-8"> y tus archivos .php guardarse en UTF-8 sin BOM.
Diagnóstico: ¿qué charset tengo?
sql
-- Variables de la conexión y del servidor
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';| Variable_name | Value |
|---|---|
| character_set_client | utf8mb4 |
| character_set_connection | utf8mb4 |
| character_set_database | utf8mb4 |
| character_set_results | utf8mb4 |
| character_set_server | utf8mb4 |
sql
-- Charset por base de datos
SELECT schema_name, default_character_set_name, default_collation_name
FROM information_schema.schemata;
-- Collation por tabla
SELECT table_name, table_collation
FROM information_schema.tables
WHERE table_schema = 'tienda';
-- Columnas que NO están en utf8mb4
SELECT table_name, column_name, character_set_name, collation_name
FROM information_schema.columns
WHERE table_schema = 'tienda'
AND character_set_name IS NOT NULL
AND character_set_name <> 'utf8mb4';Migrar una base a utf8mb4
sql
-- 1. Respaldo primero (siempre)
-- 2. La base de datos
ALTER DATABASE tienda CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- 3. Cada tabla (convierte también los datos de todas sus columnas de texto)
ALTER TABLE clientes CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- Generar los ALTER para todas las tablas:
SELECT CONCAT('ALTER TABLE `', table_name,
'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;')
FROM information_schema.tables
WHERE table_schema = 'tienda' AND table_type = 'BASE TABLE';Índices y el límite de 767 bytes
En MySQL 5.6 o tablas con formato COMPACT, un índice sobre VARCHAR(255) en utf8mb4 (255 × 4 = 1020 bytes) excede el límite de 767 bytes: Specified key was too long. Soluciones: usar MySQL 5.7.7+ / 8 con ROW_FORMAT=DYNAMIC (límite 3072 bytes), o reducir a VARCHAR(191). Por eso Laravel usa Schema::defaultStringLength(191) en servidores viejos.
Reparar texto dañado ("mojibake")
El clásico "ñ" aparece cuando texto UTF-8 se guardó a través de una conexión latin1: cada byte se interpretó como un carácter latin1 y se volvió a codificar.
txt
"ñ" en UTF-8 = C3 B1
leído como latin1 = "Ã" (C3) + "±" (B1) → se guarda "ñ" (ahora 4 bytes)Diagnóstico y reparación de una columna (¡prueba primero con SELECT!):
sql
-- Ver cómo quedaría reparado
SELECT nombre,
CONVERT(CAST(CONVERT(nombre USING latin1) AS BINARY) USING utf8mb4) AS reparado
FROM clientes
WHERE nombre LIKE '%Ã%';| nombre | reparado |
|---|---|
| MarÃa Pérez | María Pérez |
| Ana López | Ana López |
sql
-- Si se ve bien, aplicar (con respaldo previo)
UPDATE clientes
SET nombre = CONVERT(CAST(CONVERT(nombre USING latin1) AS BINARY) USING utf8mb4)
WHERE nombre LIKE '%Ã%';Y corrige la causa: añade charset=utf8mb4 a la conexión de la app.
Buena práctica
Usa utf8mb4 + utf8mb4_0900_ai_ci (o utf8mb4_unicode_ci si hay MariaDB de por medio) en todo: servidor, base de datos, tablas y conexión. Define el charset explícitamente al crear la BD y en la cadena de conexión, aunque sea el valor por defecto.
Ejercicios
- Revisa con
SHOW VARIABLES LIKE 'character_set%';el charset de tu conexión y servidor. - Crea una tabla
mensajesen utf8mb4 e inserta un texto con acentos, ñ y un emoji. Verifica que se lea igual. - Escribe la consulta que encuentra todas las columnas de tu servidor que usan
utf8mb3olatin1. - Tienes una columna
codigo_cupony quieres quePROMO10ypromo10sean cupones distintos. ¿Qué collation usas? - ¿Por qué un respaldo de MySQL 8 puede fallar al importarse en un hosting con MariaDB, y cómo lo solucionas?
Ver solución
sql
-- 2
CREATE TABLE mensajes (
id INT AUTO_INCREMENT PRIMARY KEY,
texto VARCHAR(200) NOT NULL
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO mensajes (texto) VALUES ('¡Año nuevo en Peñasco! 🎉');
SELECT texto, CHAR_LENGTH(texto) AS caracteres, LENGTH(texto) AS bytes FROM mensajes;
-- caracteres: 24 | bytes: 30 (¡, ñ, ñ = 2 bytes c/u; 🎉 = 4)
-- 3
SELECT table_schema, table_name, column_name, character_set_name
FROM information_schema.columns
WHERE character_set_name IN ('utf8mb3', 'utf8', 'latin1')
AND table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys');- Una collation que distinga mayúsculas:
utf8mb4_binoutf8mb4_0900_as_cs.
sql
ALTER TABLE cupones MODIFY codigo_cupon VARCHAR(30) NOT NULL COLLATE utf8mb4_bin;- Porque el respaldo contiene
COLLATE=utf8mb4_0900_ai_ci, que MariaDB no conoce. Solución: exportar con tablas enutf8mb4_unicode_ci, o reemplazar la collation en el archivo.sqlantes de importar (por ejemplo consed 's/utf8mb4_0900_ai_ci/utf8mb4_unicode_ci/g' respaldo.sql > respaldo_mariadb.sql).
Resumen
- Charset = cómo se guardan los caracteres en bytes; collation = cómo se comparan y ordenan.
utf8en MySQL es en realidadutf8mb3(3 bytes, sin emojis). Usa siempreutf8mb4.utf8mb4_0900_ai_cies la collation recomendada en MySQL 8;utf8mb4_unicode_cisi hay MariaDB.- Configura los 4 niveles y la conexión (
charset=utf8mb4en PDO, mysql2, Laravel). - El "ñ" se debe a conexiones latin1; se repara con
CONVERT(... USING latin1)y se previene corrigiendo la conexión.