Почему utf8 в MySQL не поддерживает emoji
MySQL реализовал кодировку utf8 как 3-байтовую, что покрывает большинство символов, но не emoji (они занимают 4 байта в UTF-8). При попытке сохранить emoji в utf8-колонке MySQL молча обрезает строку до первого проблемного символа или возвращает ошибку, в зависимости от режима sql_mode. utf8mb4 — настоящая 4-байтовая UTF-8, поддерживающая полный Unicode, включая emoji и редкие иероглифы.
Настройка my.cnf для utf8mb4 по умолчанию
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
character-set-filesystem = utf8mb4
[mysql]
default-character-set = utf8mb4
[client]
default-character-set = utf8mb4
sudo systemctl restart mysql
Проверка текущей кодировки
mysql -u root -p -e "
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';"
Изменение кодировки существующей базы данных
-- Изменить charset базы данных
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Изменить charset таблицы
ALTER TABLE mytable CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Изменить колонку
ALTER TABLE mytable MODIFY content TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Конвертация всех таблиц в базе данных
-- Генерация команд для всех таблиц
SELECT CONCAT(
'ALTER TABLE `', TABLE_NAME, '` ',
'CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'
) AS migration_sql
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'mydb' AND TABLE_TYPE = 'BASE TABLE';
Разбор типичных ошибок при конвертации
- Incorrect string value: '\xF0\x9F\x98\x80' for column — колонка всё ещё в
utf8илиlatin1, а приложение уже пытается писать emoji; конвертация из раздела выше должна пройти на этой таблице до повторной вставки. - Specified key was too long; max key length is 767 bytes — индекс на
VARCHAR(255)приutf8mb4занимает до 1020 байт (255×4), что превышает лимит старого формата таблиц; решение — сократить длину индексируемого поля, перейти на движок InnoDB с большими префиксами или включитьinnodb_large_prefix. - Illegal mix of collations for operation 'UNION' — при выборочной конвертации часть таблиц осталась на
utf8_general_ci, а часть уже наutf8mb4_unicode_ci; запросы, объединяющие такие таблицы, падают, пока collation не приведены к одному значению. - Data too long for column после конвертации — 4-байтовые символы занимают больше места, чем закладывалось при расчёте длины
VARCHAR; иногда после перехода нужно увеличить длину колонки, а не только сменить charset.
Collation: unicode_ci vs general_ci vs bin
| Collation | Описание | Когда использовать |
|---|---|---|
utf8mb4_unicode_ci | Точное Unicode-сравнение (CI = case insensitive) | Большинство сайтов (рекомендуется) |
utf8mb4_general_ci | Упрощённое сравнение, чуть быстрее | Высоконагруженные проекты |
utf8mb4_bin | Побайтовое сравнение (CS = case sensitive) | Пароли, хэши, чувствительные к регистру данные |
utf8mb4_0900_ai_ci | MySQL 8.0 стандарт Unicode 9.0 | MySQL 8.0+ |
Настройка в PHP (PDO)
<?php
$pdo = new PDO(
'mysql:host=localhost;dbname=mydb;charset=utf8mb4',
'user', 'password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci'
]
);
При обновлении с utf8 на utf8mb4 проверьте, что индексы VARCHAR-колонок не превышают 767 байт. При utf8mb4 один символ может занять до 4 байт, поэтому VARCHAR(191) — практический максимум для индексируемых строк при стандартном InnoDB (innodb_large_prefix=OFF).
Сравнение utf8 и utf8mb4
| Критерий | utf8 (устаревший) | utf8mb4 |
|---|---|---|
| Байт на символ | До 3 | До 4 |
| Поддержка emoji | Нет | Да |
| Макс. длина VARCHAR с индексом (старый InnoDB) | 255 | 191 |
| Занимаемое место на диске | Меньше | Немного больше |
| Рекомендация для новых проектов | Не использовать | Использовать всегда |
Когда конвертация не так проста
- В базе уже есть данные с эмодзи, сохранённые как «обрезанные» строки в
utf8, — восстановить исходный текст из повреждённых байт невозможно, только заново получить данные из источника. - Таблица использует полнотекстовый индекс (
FULLTEXT) — при смене charset его нужно пересоздать заново, конвертация «на лету» не переносит статистику индекса автоматически. - Приложение подключается к MySQL через несколько разных драйверов (PHP, Java, cron-скрипты на Python) — charset нужно синхронно поменять во всех местах подключения, иначе часть сервисов продолжит писать битые данные даже после конвертации таблиц.
Как проверить, что конвертация прошла успешно
# Убедиться, что все таблицы базы в utf8mb4
mysql -u root -p -e "
SELECT TABLE_NAME, TABLE_COLLATION
FROM information_schema.TABLES
WHERE TABLE_SCHEMA='mydb' AND TABLE_COLLATION NOT LIKE 'utf8mb4%';"
# Пустой результат означает, что конвертированы все таблицы
# Тестовая вставка emoji
mysql -u root -p mydb -e "
CREATE TEMPORARY TABLE emoji_test (t TEXT) CHARACTER SET utf8mb4;
INSERT INTO emoji_test VALUES ('Тест emoji: ');
SELECT * FROM emoji_test;"
Если запрос к information_schema не находит таблиц вне utf8mb4, а тестовая вставка проходит без ошибки Incorrect string value — конвертация завершена корректно на уровне базы данных.
Частые вопросы
Рискует ли конвертация базы в utf8mb4 потерей данных в существующих строках, или это всегда безопасная операция?
Сама команда конвертации обычно безопасна для строк, которые и так хранили корректный текст, потому что расширяет набор символов, а не сужает его. Риск появляется там, где в колонке уже лежат испорченные байты от неудачной попытки сохранить эмодзи — такие обрезанные значения конвертация не восстановит, их исходный вид уже потерян и можно только заново получить его из источника.
Работает ли utf8mb4 на практике медленнее utf8 из-за того, что символ может занимать больше байт?
Разница в большинстве обычных нагрузок почти неощутима: дополнительный байт на символ увеличивает объём данных на диске и в памяти незначительно, а само сравнение строк по выбранному collation работает так же. Заметнее влияние проявляется только там, где индексы на длинных строковых полях упираются в ограничение длины ключа и требуют укорачивания или смены настроек движка.
Если в приложении уже есть испорченные из-за старой кодировки данные с эмодзи, можно ли восстановить исходный текст?
Нет, если символ был обрезан на уровне кодировки до первого проблемного байта, часть исходной информации уничтожена безвозвратно и никакая смена charset её не вернёт. Единственный надёжный путь — заново получить такие данные из первоисточника, будь то исходный экспорт, лог приложения или внешний сервис, откуда они когда-то попали в базу.
Нужно ли менять кодировку и в коде приложения, или достаточно поменять её только в базе данных?
Менять настройки нужно согласованно в обоих местах: если база уже на utf8mb4, а соединение из приложения по-прежнему открывается со старой кодировкой, сервер субд будет молча преобразовывать символы под старый набор прямо на лету, и те же проблемы с обрезанием эмодзи вернутся. Параметр подключения и настройки базы должны указывать на один и тот же charset одновременно.