MySQL Character Sets and Collations
Introduction¶
Character set bugs in MySQL are legendary, mostly because of one historical
mistake: utf8 in MySQL is not UTF-8. It's a 3-byte-max subset that can't
represent characters outside the Basic Multilingual Plane — no emoji, some CJK
characters missing. The real thing is utf8mb4.
Always use utf8mb4¶
-- server default (my.cnf)
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_0900_ai_ci -- MySQL 8; MariaDB: utf8mb4_unicode_ci
-- new database
CREATE DATABASE app CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- new table
CREATE TABLE posts (...) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
utf8mb4 stores all of Unicode in 1–4 bytes. There is no downside for
Latin-heavy text (ASCII is still 1 byte).
Collations — how strings compare and sort¶
A collation is a set of rules for comparison/ordering, tied to a charset:
| Collation | Behaviour |
|---|---|
utf8mb4_0900_ai_ci |
accent-insensitive, case-insensitive (MySQL 8 default) — é = e, A = a |
utf8mb4_0900_as_ci |
accent-sensitive, case-insensitive |
utf8mb4_0900_as_cs |
accent- and case-sensitive |
utf8mb4_bin |
byte-for-byte; A ≠ a, fastest, no locale rules |
utf8mb4_unicode_ci |
older UCA 5.2 rules (MariaDB default-ish) |
utf8mb4_<lang>_... |
language-specific tailoring (e.g. _es_, _tr_ for Turkish dotted i) |
Consequences:
WHERE name = 'jose'matchesJoséunder_ciaccent-insensitive — often what you want for search, sometimes a surprise for exact-match logic.- A
UNIQUEindex under_citreats[email protected]and[email protected]as duplicates. Useutf8mb4_bin(or add a generated lowercase column) if you need case-sensitive uniqueness. - Sorting:
_cisortsapple, Äpfel, bananalinguistically;_binsorts by code point (Äafterz).
The four levels¶
Charset/collation is set at server → database → table → column, each inheriting from the level above unless overridden. Plus the connection:
SHOW VARIABLES LIKE 'character_set%';
-- character_set_client, _connection, _results <- the connection triplet
SET NAMES utf8mb4; -- sets all three for this session
Most drivers have a charset option (charset=utf8mb4 in the DSN, ?charset=utf8mb4
in a URL). If the connection charset is latin1 but the data is UTF-8, you get
double-encoding or mojibake even though the table is utf8mb4.
The 767-byte index limit (legacy)¶
Old MySQL (pre-5.7 / without innodb_large_prefix) capped an index key at 767
bytes. utf8mb4 is 4 bytes/char → VARCHAR(191) was the max fully-indexable
length (191 × 4 = 764). Modern MySQL 8 with DYNAMIC row format allows 3072
bytes, so VARCHAR(255) indexes fine. If you see
Specified key was too long, you're on old defaults — set ROW_FORMAT=DYNAMIC
and innodb_large_prefix=ON (default in 8).
Fixing mojibake¶
Symptoms: é instead of é (UTF-8 bytes read as latin1), or ? (data lost at
insert time — unrecoverable).
If the bytes are correct but tagged wrong (common: data inserted via a latin1 connection into a latin1 column but the bytes are actually UTF-8):
-- The "double convert" fix, per column. TEST ON A COPY.
ALTER TABLE t MODIFY col BLOB; -- strip charset interpretation
ALTER TABLE t MODIFY col VARCHAR(255) CHARACTER SET utf8mb4; -- reinterpret bytes as utf8mb4
Convert a whole table (when the current charset is genuinely what's stored):
ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
Always dump a backup first; charset conversions are easy to get wrong and hard to undo.
Verification and troubleshooting¶
SELECT table_name, ccsa.character_set_name, t.table_collation
FROM information_schema.tables t
JOIN information_schema.collation_character_set_applicability ccsa
ON ccsa.collation_name = t.table_collation
WHERE t.table_schema = 'app';
SELECT column_name, character_set_name, collation_name
FROM information_schema.columns
WHERE table_schema='app' AND table_name='posts' AND character_set_name IS NOT NULL;
SELECT HEX(col), col FROM t WHERE id = 123; -- inspect the actual bytes
- Emoji / some characters become
????— column or connection isutf8(3-byte) notutf8mb4, or the client didn'tSET NAMES utf8mb4. The?means the character was lost at write time — data is gone, fix forward. éeverywhere — connection charset mismatch at insert time. Use the BLOB round-trip fix above.Illegal mix of collationserror — aJOIN/comparison between columns with different collations.CONVERT(x USING utf8mb4)in the query, orALTERthe columns to match.- Case-insensitive unique index rejecting distinct-looking values —
_cicollation. Switch that column/index to_binor a_cscollation. ORDER BYgives an unexpected order — collation rules;ORDER BY name COLLATE utf8mb4_binfor code-point order, or pick a language collation.SHOW CREATE TABLEshows the right charset but data is wrong — the table charset only affects new writes and how bytes are interpreted; existing bad bytes need conversion.
Related tools and reading¶
- On-site: Text diff, URL encoder / decoder.
- Related posts: Reading a MySQL EXPLAIN plan, Safe online schema changes in MySQL.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Databases & Caching
PostgreSQL Autovacuum Tuning
Autovacuum reclaims dead rows and prevents transaction-ID wraparound. When it falls behind, tables bloat and queries slow — here's the formula and the fixes.
October 24, 2026Database Deadlocks Explained
A deadlock is two transactions each holding a lock the other needs. The engine kills one. Here's how to read the log and remove the cause — usually inconsistent lock order.
October 24, 2026MySQL Replication Basics
How MySQL replication actually works, GTID vs binlog coordinates, and the handful of commands for setup, lag monitoring, and un-breaking a stuck replica.
October 24, 2026