UTF-8 y Unicode ¿Por qué ese emoji rompió tu base de datos?
Tu aplicación insertó un emoji y MySQL lanzó Incorrect string value. Aquí está la razón — puntos de código vs bytes, el engaño de utf8 vs utf8mb4 en MySQL, los pares de representación de JavaScript y cómo solucionarlo realmente.
Tu aplicación funcionaba bien. Luego un usuario escribió un emoji en un campo de texto, y MySQL lanzó Incorrect string value: '\xF0\x9F\x98\x80' for column 'bio'. O tal vez el emoji desapareció silenciosamente. O el INSERT completo falló y perdiste datos. Todo por un carácter de cuatro bytes que tu columna de la base de datos no esperaba.
Esto no es un truco de MySQL ni un error en PHP. Es una consecuencia de cómo funciona la codificación Unicode → UTF-8, y una vez que lo entiendes, ya nunca volverás a sorprenderte por ello.
Puntos de código vs bytes: la diferencia real
Unicode asigna a cada carácter un punto de código — un número. La letra A es U+0041. El signo de euro es U+20AC. El emoji 😀 es U+1F600. Eso es la identidad abstracta del carácter.
UTF-8 es un codificación — una forma de almacenar puntos de código como bytes. La trampa es que UTF-8 es de ancho variable: utiliza de 1 a 4 bytes según el valor del punto de código. Así se mantiene compatible con ASCII (todos los caracteres ASCII ocupan 1 byte en UTF-8) mientras también codifica cada carácter existente.
Las reglas de codificación:
- U+0000 a U+007F (ASCII) → 1 byte
- U+0080 a U+07FF (latín extendido, árabe, hebreo, etc.) → 2 bytes
- U+0800 a U+FFFF (la mayoría de los caracteres CJK, puntuación, símbolos) → 3 bytes
- U+10000 a U+10FFFF (emojis, scripts raras, símbolos matemáticos) → 4 bytes
Es por eso que el emoji 😀 (U+1F600) ocupa 4 bytes: su punto de código está por encima de U+FFFF.
Tamaños de bytes en UTF-8: una tabla de referencia
Aquí está lo que cuestan en bytes los caracteres comunes:
| Carácter | Descripción | Punto de código Unicode | Bytes UTF-8 (hex) | Recuento de Bytes |
|---|---|---|---|---|
| A | Letra latina mayúscula A | U+0041 | 41 | 1 |
| é | Letra e con acento agudo | U+00E9 | C3 A9 | 2 |
| € | Signo de euro | U+20AC | E2 82 AC | 3 |
| 中 | Carácter chino "medio" | U+4E2D | E4 B8 AD | 3 |
| 😀 | Emoji de sonrisa | U+1F600 | F0 9F 98 80 | 4 |
| 🔥 | Emoji de fuego | U+1F525 | F0 9F 94 A5 | 4 |
| 𝕳 | H fraktur matemático | U+1D573 | F0 9D 95 B3 | 4 |
Para verificarlo por sí mismo, usa el Calculadora de longitud de cadena — muestra tanto el número de caracteres como el número de bytes para cualquier texto que pegues. Pega 😀 y verás 1 carácter pero 4 bytes.
El engaño de MySQL utf8
Aquí es donde los desarrolladores se quedan atrapados. MySQL tiene un conjunto de caracteres llamado utf8. Suena correcto. Está mal — el conjunto de caracteres utf8 de MySQL solo soporta secuencias de hasta 3 bytes. Los emojis (4 bytes) no están soportados. utf8 El conjunto de caracteres UTF-8 completo en MySQL es
(introducido en MySQL 5.5.3, lanzado en 2010). Si tu columna utiliza utf8mb4 y alguien inserta un emoji, MySQL lo trunca silenciosamente o lanza: utf8 También actualiza la configuración de conexión de tu aplicación a la base de datos. En MySQL PDO:
Incorrect string value: '\xF0\x9F\x98\x80' for column 'bio' at row 1
La solución:
-- Convert the table
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Or for a specific column
ALTER TABLE users MODIFY bio TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- And set your connection charset
SET NAMES utf8mb4;
La trampa de VARCHAR(255)
$pdo = new PDO($dsn, $user, $pass, [
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
]);
en MySQL significa 255
VARCHAR(255) caracteres , no 255 bytes — pero el límite de almacenamiento para una sola fila se calcula en bytes. Con, cada carácter puede ocupar hasta 4 bytes, por lo que una utf8mb4columna reserva hasta 1.020 bytes. Esto importa cuando usas el límite predeterminado de índice de InnoDB (767 bytes) para columnas VARCHAR: VARCHAR(255) El problema de los pares de surrogado en JavaScript
-- This fails on older MySQL with default innodb_large_prefix=OFF
CREATE INDEX idx_email ON users (email); -- email is VARCHAR(255) utf8mb4
-- Fix: use a prefix index
CREATE INDEX idx_email ON users (email(191)); -- 191 * 4 = 764 bytes, under 767
-- Or enable large prefixes (MySQL 5.7+, on by default in 8.0)
-- Set innodb_large_prefix = ON in my.cnf
JavaScript utiliza UTF-16 internamente, no UTF-8. Y UTF-16 tiene su propia codificación de múltiples unidades para puntos de código por encima de U+FFFF:
pares de surrogado — dos unidades de código de 16 bits que juntas representan un carácter. Esto significa que
en JavaScript cuenta unidades de código UTF-16, no caracteres: String.length Para operaciones de cadena que necesitan ser conscientes de caracteres, usa el operador de expansión o
'😀'.length // → 2 (two UTF-16 surrogate code units)
[...'😀'].length // → 1 (spread operator uses Unicode code points)
// Checking the character at index 0
'😀'[0] // → '\uD83D' (the high surrogate, not the emoji)
'😀'.codePointAt(0) // → 128512 (0x1F600, correct)
El ejemplo del emoji familiar vale la pena detenerse. Intl.Segmenter:
// Count actual grapheme clusters
const segmenter = new Intl.Segmenter();
const chars = [...segmenter.segment('👨👩👧👦')];
chars.length // → 1 (family emoji is one grapheme cluster)
'👨👩👧👦'.length // → 11 (UTF-16 code units)
es cuatro emojis unidos por separadores de ancho cero (U+200D). Un enfoque naïf 👨👩👧👦 te da 11. Clusters reales de grafemas: 1. Esto importa si estás implementando límites de caracteres — un límite basado en .length se comportará inesperadamente cuando los usuarios escriban secuencias de emojis. String.length Cómo verificar la codificación en la práctica
en PHP cuenta bytes, no caracteres. Esto atrapa constantemente a los desarrolladores de PHP al trabajar con cadenas multibyte — una cadena de 10 emojis reportará una longitud de 40. Usa
Pitón
s = '😀'
print(len(s)) # 1 (Python 3 counts code points)
print(len(s.encode('utf-8'))) # 4 bytes
print(s.encode('utf-8').hex()) # f09f9880
PHP
$s = '😀';
echo strlen($s); // 4 (bytes, not characters)
echo mb_strlen($s); // 1 (characters)
echo mb_strlen($s, '8bit'); // 4 (bytes, explicit)
strlen() cuando necesites contar caracteres. mb_strlen() Verificación rápida
MySQL
-- Check charset of a table
SHOW CREATE TABLE users\G
-- Check charset of a specific column
SELECT CHARACTER_SET_NAME, COLLATION_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'bio';
-- Character count vs byte count
SELECT CHAR_LENGTH('😀'), LENGTH('😀');
-- → 1, 4
Si quieres ver el conteo de bytes vs caracteres para cualquier texto sin escribir código, el
lo maneja instantáneamente — pega cualquier texto y muestra el conteo de caracteres, palabras y bytes al lado. Calculadora de longitud de cadena La lista de verificación del error de codificación
Conjunto de caracteres de MySQL:
- ¿Es? ¿Es?
utf8mb4, noutf8? Verifica conSHOW CREATE TABLE. - Conexión de MySQL: ¿Está tu aplicación enviando?
SET NAMES utf8mb4? Verifica tu DSN o configuración de conexión. - strlen vs mb_strlen en PHP: ¿Estás usando funciones que cuentan bytes donde necesitas contar caracteres?
- .length en JavaScript: ¿Estás contando unidades de código donde necesitas clusters de grafemas?
- Encabezados HTTP: ¿Está enviando tu respuesta?
Content-Type: text/html; charset=utf-8? - Codificación de archivos: ¿Están tus archivos originales y tus respaldos SQL guardados en UTF-8 sin BOM?
Instalar extensiones
Agregue herramientas IO a su navegador favorito para obtener acceso instantáneo y búsquedas más rápidas
恵 ¡El marcador ha llegado!
Marcador es una forma divertida de llevar un registro de tus juegos, todos los datos se almacenan en tu navegador. ¡Próximamente habrá más funciones!
Herramientas clave
Ver todo Los recién llegados
Ver todoActualizar: Nuestro última herramienta se agregó el 20 de junio de 2026
