UTF-8 и Unicode Почему тот эмодзи сломал вашу базу данных
Ваше приложение вставило эмодзи, и MySQL выдало ошибку неверного значения строки. Вот почему — кодовые точки против байтов, ложь MySQL utf8 по сравнению с utf8mb4, пары заменителей JavaScript и как на самом деле это исправить.
Ваша приложение работало нормально. Потом пользователь ввел эмодзи в текстовое поле, и MySQL выдало Incorrect string value: '\xF0\x9F\x98\x80' for column 'bio'. Или, возможно, эмодзи исчез без следа. Или полный ввод данных не удался и вы потеряли данные. Все из-за четырёхбайтового символа, который не ожидал ваша база данных.
Это не особенность MySQL или бага PHP. Это следствие того, как работает кодировка Unicode → UTF-8, и как только вы это понимаете, вы больше не будете удивляться этому.
Кодовые точки против байтов: реальное различие
Unicode присваивает каждому символу кодовую точку — число. Буква A — U+0041. Знак евро — U+20AC. Эмодзи 😀 — U+1F600. Это абстрактная идентичность символа.
UTF-8 — это кодирование — способ хранения кодовых точек в байтах. Техника заключается в том, что UTF-8 имеет переменную ширину: он использует от 1 до 4 байт в зависимости от значения кодовой точки. Это позволяет ему оставаться совместимым с ASCII (все символы ASCII занимают 1 байт в UTF-8) при одновременном кодировании всех существующих символов.
Правила кодировки:
- U+0000 до U+007F (ASCII) → 1 байт
- U+0080 до U+07FF (расширенный латинский, арабский, иврит и т.д.) → 2 байта
- U+0800 до U+FFFF (большая часть CJK, пунктуации, символов) → 3 байта
- U+10000 до U+10FFFF (эмодзи, редкие письменности, математические символы) → 4 байта
Поэтому эмодзи 😀 (U+1F600) занимает 4 байта: его кодовая точка превышает U+FFFF.
Размеры байтов UTF-8: справочная таблица
Вот сколько байт реально занимают распространённые символы:
| Символ | Описание | Кодовая точка Unicode | Байты UTF-8 (шестнадцатеричный) | Количество байт |
|---|---|---|---|---|
| А | Латинская заглавная A | U+0041 | 41 | 1 |
| é | Латинская e с острым знаком | U+00E9 | C3 A9 | 2 |
| € | Знак евро | U+20AC | E2 82 AC | 3 |
| 中 | Китайский символ «средний» | U+4E2D | E4 B8 AD | 3 |
| 😀 | Эмодзи с улыбкой | U+1F600 | F0 9F 98 80 | 4 |
| 🔥 | Эмодзи огня | U+1F525 | F0 9F 94 A5 | 4 |
| 𝕳 | Математическая фрактурская H | U+1D573 | F0 9D 95 B3 | 4 |
Чтобы проверить это самостоятельно, используйте Калькулятор длины строки — она показывает как количество символов, так и количество байтов для любого вставленного текста. Вставьте 😀 и вы увидите 1 символ, но 4 байта.
Ложь MySQL по поводу UTF-8
Здесь разработчики подвергаются удару. MySQL имеет кодировку под названием utf8. Звучит правильно. Это неверно — MySQL поддерживает только последовательности до 3 байт. Эмодзи (4 байта) не поддерживаются. utf8 Фактическая полная кодировка UTF-8 в MySQL — это
(введена в MySQL 5.5.3, выпущена в 2010 году). Если ваша колонка использует utf8mb4 и кто-то вводит эмодзи, MySQL либо тихо усекает данные, либо выдаёт: utf8 Также обновите настройки подключения к базе данных в вашем приложении. В MySQL PDO:
Incorrect string value: '\xF0\x9F\x98\x80' for column 'bio' at row 1
(байты). В Laravel, используйте
-- 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;
Ловушка VARCHAR(255)
$pdo = new PDO($dsn, $user, $pass, [
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
]);
в MySQL означает 255
VARCHAR(255) символов , а не 255 байт — но ограничение по хранению в одной строке рассчитывается в байтах. При использованиикаждый символ может занимать до 4 байт, поэтому колонка может занимать до 1020 байт. Это важно при использовании предельного значения индекса InnoDB (767 байт) для индексации колонок varchar: utf8mb4Проблема с суррогатными парами в JavaScript VARCHAR(255) JavaScript использует UTF-16 внутренне, а не UTF-8. И UTF-16 имеет собственную многобайтовую кодировку для кодовых точек выше U+FFFF:
-- 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
суррогатные пары
— два 16-битных кода, которые вместе представляют один символ. Это означает, что в JavaScript считает количество UTF-16 кодов, а не символов:
Для операций со строками, требующих учёта символов, используйте оператор расширения или String.length Пример эмодзи в семье стоит остановиться.
'😀'.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)
состоит из четырёх эмодзи, соединённых нулевыми соединителями (U+200D). Простой 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)
даёт 11. Фактические графемные кластеры: 1. Это важно, если вы реализуете ограничения на количество символов — ограничение, основанное на 👨👩👧👦 поведёт себя неожиданно, когда пользователи вводят последовательности эмодзи. .length Как проверить кодировку на практике String.length в PHP считает байты, а не символы. Это постоянно ловит разработчиков PHP при работе с многобайтовыми строками — строка из 10 эмодзи покажет длину 40. Используйте
при необходимости подсчёта количества символов.
Питон
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() Быстрый проверочный список mb_strlen() Если вы хотите увидеть количество байт и символов для произвольного текста без написания кода, сервис
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
обрабатывает это мгновенно — вставьте любой текст, и он показывает количество символов, слов и байт рядом.
Проверочный список ошибок кодировки Калькулятор длины строки Кодировка MySQL:
Это
- ? Проверьте с помощью Подключение к MySQL:
utf8mb4, а неutf8Используете ли вы приложениеSHOW CREATE TABLE. - ? Проверьте ваш DSN или конфигурацию подключения. PHP strlen против mb_strlen:
SET NAMES utf8mb4Используете ли вы функции подсчёта байтов там, где нужно подсчитывать символы? - JavaScript .length: Подсчитываете ли вы кодовые единицы там, где нужно подсчитывать графемные кластеры?
- HTTP-заголовки: Отправляете ли вы в ответе
- Файловой кодировке: Сохраняются ли ваши исходные файлы и SQL-дампы в формате UTF-8 без BOM?
Content-Type: text/html; charset=utf-8? - UTF-8 и Unicode: Почему эмодзи сломали вашу базу данных 2 UTF-8 и Unicode: Почему эмодзи сломали вашу базу данных 1
Установите наши расширения
Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска
恵 Табло результатов прибыло!
Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!
Подписаться на новости
все Новые поступления
всеОбновлять: Наш последний инструмент was added on Июн 22, 2026
