Skip to content

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 bytes

El gran engaño: utf8 no es UTF-8 ​

UTF-8 real usa de 1 a 4 bytes por carácter. Pero en MySQL:

CharsetBytes por carácter¿Emojis?¿Algunos caracteres chinos, símbolos matemáticos?
latin11NoNo (solo Europa occidental)
utf8 = utf8mb3Hasta 3NoNo
utf8mb4Hasta 4Sí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
CollationMayúsc.AcentosNotas
utf8mb4_0900_ai_ciNo distingueNo distinguePor defecto en MySQL 8. Recomendada
utf8mb4_0900_as_csDistingueDistingueComparaciones estrictas
utf8mb4_es_0900_ai_ciNoNoReglas del español (orden de la ñ)
utf8mb4_unicode_ciNoNoEstándar antiguo (UCA 4.0). Compatible con MariaDB y MySQL 5.7
utf8mb4_general_ciNoNoMás vieja y menos precisa; evítala
utf8mb4_binDistingueDistingueCompara 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;
-- 1

Consecuencias en el mundo real:

  • Con _ci, un UNIQUE en email impide registrar Ana@correo.com si ya existe ana@correo.com. ¡Normalmente es lo que quieres!
  • Con _ai_ci, un UNIQUE en nombre_usuario considera iguales "jose" y "josé".
  • Un buscador con _ai_ci encuentra "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 = utf8mb4

2. 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_nameValue
character_set_clientutf8mb4
character_set_connectionutf8mb4
character_set_databaseutf8mb4
character_set_resultsutf8mb4
character_set_serverutf8mb4
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 '%Ã%';
nombrereparado
María PérezMaría Pérez
Ana LópezAna 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 ​

  1. Revisa con SHOW VARIABLES LIKE 'character_set%'; el charset de tu conexión y servidor.
  2. Crea una tabla mensajes en utf8mb4 e inserta un texto con acentos, ñ y un emoji. Verifica que se lea igual.
  3. Escribe la consulta que encuentra todas las columnas de tu servidor que usan utf8mb3 o latin1.
  4. Tienes una columna codigo_cupon y quieres que PROMO10 y promo10 sean cupones distintos. ¿Qué collation usas?
  5. ¿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');
  1. Una collation que distinga mayúsculas: utf8mb4_bin o utf8mb4_0900_as_cs.
sql
ALTER TABLE cupones MODIFY codigo_cupon VARCHAR(30) NOT NULL COLLATE utf8mb4_bin;
  1. Porque el respaldo contiene COLLATE=utf8mb4_0900_ai_ci, que MariaDB no conoce. Solución: exportar con tablas en utf8mb4_unicode_ci, o reemplazar la collation en el archivo .sql antes de importar (por ejemplo con sed '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.
  • utf8 en MySQL es en realidad utf8mb3 (3 bytes, sin emojis). Usa siempre utf8mb4.
  • utf8mb4_0900_ai_ci es la collation recomendada en MySQL 8; utf8mb4_unicode_ci si hay MariaDB.
  • Configura los 4 niveles y la conexión (charset=utf8mb4 en PDO, mysql2, Laravel).
  • El "ñ" se debe a conexiones latin1; se repara con CONVERT(... USING latin1) y se previene corrigiendo la conexión.

Academia Web — aprende, copia y construye.