MySQL Character Sets and Collations
Every string stored in MySQL has two attributes: a character set (charset) that defines which characters can be stored and how they are encoded in bytes, and a collation that defines how those characters are compared and sorted. Getting these right is essential for correct internationalization, emoji support, and case-insensitive searches.
Character Sets vs Collations
Concept | Definition | Example |
|---|---|---|
Character set | The encoding: which Unicode code points are valid and how many bytes each character occupies | utf8mb4 encodes each character in 1–4 bytes |
Collation | The comparison and sort rules applied when comparing two strings in that character set | utf8mb4_unicode_ci compares 'A' and 'a' as equal (case-insensitive) |
A collation always belongs to exactly one character set. You cannot mix a charset with a collation from a different charset (e.g., utf8mb4 column with a latin1 collation is invalid).
Why utf8 in MySQL Is Broken
This is the most critical charset decision in MySQL. MySQL's utf8 charset is not real UTF-8:
Charset | Max bytes per character | Emoji support | Recommendation |
|---|---|---|---|
utf8 (= utf8mb3) | 3 bytes max | No — 4-byte emoji cause silent truncation or errors | Avoid for all new tables |
utf8mb4 | 4 bytes max | Yes — full Unicode including all emoji and supplementary chars | Use this for everything |
latin1 | 1 byte | No — Latin-1 / ISO 8859-1 only | Legacy databases only |
ascii | 1 byte | No — 7-bit ASCII only | UUIDs, hashes, base64 data where you know all chars are ASCII |
utf8 charset is NOT full UTF-8. It only supports up to 3-byte characters and silently drops 4-byte emoji (like most modern emoji). Always use utf8mb4 for all new tables and databases.-- Demonstrate the problem with utf8 (3-byte only)
CREATE TABLE test_utf8 (msg VARCHAR(100) CHARACTER SET utf8);
INSERT INTO test_utf8 VALUES ('Hello ????'); -- emoji is silently dropped or errors
-- Result: 'Hello ' (emoji missing) or ERROR 1366
-- utf8mb4 handles it correctly
CREATE TABLE test_utf8mb4 (msg VARCHAR(100) CHARACTER SET utf8mb4);
INSERT INTO test_utf8mb4 VALUES ('Hello ????'); -- works correctly
SELECT msg FROM test_utf8mb4;
-- Result: 'Hello ????'Collation Naming Convention
Collation names follow the pattern: charset_language_flags. The suffix flags are:
Suffix | Meaning | Example comparison |
|---|---|---|
_ci | Case-insensitive: uppercase = lowercase | 'A' = 'a' is TRUE |
_cs | Case-sensitive: uppercase != lowercase | 'A' = 'a' is FALSE |
_bin | Binary: compare raw byte values, fully case-sensitive and accent-sensitive | 'A' != 'a', 'é' != 'e' |
_ai | Accent-insensitive: accented chars equal base chars | 'é' = 'e' is TRUE |
_as | Accent-sensitive: accented chars differ from base chars | 'é' = 'e' is FALSE |
_ks | Kana-sensitive (Japanese) | Hiragana != Katakana |
_0900 | Unicode 9.0 algorithm (MySQL 8.0+) | Faster and more standards-compliant |
Common Collations for utf8mb4
Collation | Use case | Notes |
|---|---|---|
utf8mb4_unicode_ci | General use in MySQL 5.7 | Unicode-aware, case-insensitive. Best default for MySQL 5.7. |
utf8mb4_0900_ai_ci | General use in MySQL 8.0 | Unicode 9.0, accent-insensitive, case-insensitive. Faster than unicode_ci. MySQL 8.0 default. |
utf8mb4_general_ci | Legacy | Older, less accurate Unicode sorting. Avoid for new tables. |
utf8mb4_bin | Tokens, passwords, hashes | Binary byte comparison. Fully case-sensitive and accent-sensitive. Use for exact-match fields. |
utf8mb4_0900_cs_ai | Case-sensitive text search | Unicode 9.0, case-sensitive, accent-insensitive. |
utf8mb4_unicode_cs | Case-sensitive in MySQL 5.7 | Unicode-aware and case-sensitive. |
Charset at Server / Database / Table / Column Level
1. Server level (my.cnf) — sets the default for everything:
[mysqld] character_set_server = utf8mb4 collation_server = utf8mb4_unicode_ci
2. Database level:
CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- Change an existing database (new tables inherit this; existing tables unchanged) ALTER DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
3. Table level:
CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL, bio TEXT, PRIMARY KEY (id) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- Convert an existing table (also converts all string column data) ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
4. Column level (overrides table default):
-- Case-sensitive column for API tokens (must match exactly)
ALTER TABLE users
ADD COLUMN api_token VARCHAR(64)
CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
-- Check charset and collation of all columns in a table
SELECT COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users';SET NAMES in Application Code
The connection charset controls what the client sends and what MySQL returns. Always ensure it matches your application encoding:
-- Set the client, connection, and results charset in one statement SET NAMES 'utf8mb4'; -- Equivalent to all three of these: SET character_set_client = utf8mb4; SET character_set_connection = utf8mb4; SET character_set_results = utf8mb4;
charset: utf8mb4 in the connection configuration. This is safer than running SET NAMES manually.Viewing Character Set Information
-- List all available character sets SHOW CHARACTER SET; -- List collations for utf8mb4 SHOW COLLATION WHERE Charset = 'utf8mb4'; -- Check current server defaults SHOW VARIABLES LIKE 'character_set%'; SHOW VARIABLES LIKE 'collation%';
+----------------------------+---------+ | Variable_name | Value | +----------------------------+---------+ | character_set_client | utf8mb4 | | character_set_connection | utf8mb4 | | character_set_database | utf8mb4 | | character_set_results | utf8mb4 | | character_set_server | utf8mb4 | +----------------------------+---------+
CONVERT Between Charsets
-- Convert a string expression to a different charset
SELECT CONVERT('Hello World' USING utf8mb4);
-- Cast with explicit charset
SELECT CAST('test' AS CHAR CHARACTER SET utf8mb4);
-- Check the charset and collation of any string expression
SELECT CHARSET('Hello'), COLLATION('Hello');
-- Force a specific collation for a single query
SELECT * FROM users
WHERE username = 'ALICE' COLLATE utf8mb4_bin; -- case-sensitive for this query onlyFixing Mojibake (Garbled Text)
Mojibake occurs when data encoded in one charset is read or stored as a different charset. The classic scenario: data was actually stored as latin1 but the column is declared as utf8, so non-ASCII characters appear garbled.
-- Diagnose: check the actual hex bytes stored SELECT username, HEX(username) FROM users WHERE id = 1; -- If you see 'é' where 'é' is expected, that's mojibake -- Fix approach: tell MySQL the real encoding, then convert -- Step 1: reinterpret the column as the actual encoding (e.g., latin1) ALTER TABLE users MODIFY username VARCHAR(100) CHARACTER SET latin1; -- Step 2: convert to utf8mb4 (MySQL re-encodes properly) ALTER TABLE users MODIFY username VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
For a full database migration from latin1 to utf8mb4:
# Dump with the original charset declaration
mysqldump --default-character-set=latin1 myapp > dump_latin1.sql
# Replace charset references in the dump
sed -i 's/CHARSET=latin1/CHARSET=utf8mb4/g;
s/utf8_general_ci/utf8mb4_unicode_ci/g;
s/DEFAULT CHARSET=utf8/DEFAULT CHARSET=utf8mb4/g' dump_latin1.sql
# Import into the new (or same) database
mysql --default-character-set=utf8mb4 myapp_utf8 < dump_latin1.sqlCollation Effects on Query Results
-- With _ci collation: returns the row for 'alice' when searching 'ALICE' SELECT * FROM users WHERE username = 'ALICE'; -- returns alice -- With _bin collation: returns no rows (case-sensitive) SELECT * FROM users WHERE username = 'ALICE'; -- returns nothing if stored as 'alice' -- JOIN on collation mismatch: can cause performance issues or no rows SELECT * FROM users u JOIN sessions s ON u.username = s.username; -- If u.username is utf8mb4_unicode_ci and s.username is utf8mb4_bin, -- MySQL cannot use an index on the join and may do a full scan -- Fix collation mismatch in a JOIN for one query SELECT * FROM users u JOIN sessions s ON u.username = s.username COLLATE utf8mb4_unicode_ci;
Index Size Considerations with utf8mb4
utf8mb4 uses up to 4 bytes per character. The default InnoDB index key length limit is 3072 bytes. A VARCHAR(768) column in utf8mb4 would hit exactly 3072 bytes (768 × 4). For longer columns, use a prefix index:
-- Error: "Specified key was too long; max key length is 3072 bytes" CREATE INDEX idx_long ON articles (body(800)); -- 800 * 4 = 3200 > 3072 -- Fix 1: use a shorter prefix CREATE INDEX idx_long ON articles (body(767)); -- 767 * 4 = 3068 bytes -- Fix 2: use a generated column + hash for full-column uniqueness ALTER TABLE users ADD COLUMN email_hash VARCHAR(64) AS (SHA2(email, 256)) STORED; CREATE UNIQUE INDEX idx_email_hash ON users (email_hash);
Collation Comparison Deep Dive
-- See exactly how two collations differ on the same strings SELECT 'cafe' = 'café' COLLATE utf8mb4_unicode_ci AS unicode_ci_equal, -- 1 (accent-insensitive) 'cafe' = 'café' COLLATE utf8mb4_0900_as_ci AS as_ci_equal, -- 0 (accent-sensitive) 'cafe' = 'café' COLLATE utf8mb4_bin AS bin_equal; -- 0 (binary) -- Sort order differences between collations SELECT name FROM countries ORDER BY name COLLATE utf8mb4_unicode_ci; -- Swedish chars (Å, Ä, Ö) sort after Z in unicode_ci (correct for Swedish) SELECT name FROM countries ORDER BY name COLLATE utf8mb4_general_ci; -- general_ci sorts some chars differently — less accurate for non-English
Character Set Configuration in Application Drivers
// Node.js (mysql2)
const connection = mysql.createConnection({
host: 'localhost',
user: 'app',
password: 'secret',
database: 'myapp',
charset: 'utf8mb4' // sets SET NAMES utf8mb4 automatically
});
// PHP PDO
$pdo = new PDO(
'mysql:host=localhost;dbname=myapp;charset=utf8mb4',
'app', 'secret'
);
// Java JDBC (Connector/J)
// jdbc:mysql://localhost/myapp?characterEncoding=utf8mb4
// Python (mysql-connector-python)
cnx = mysql.connector.connect(
host='localhost', user='app', password='secret',
database='myapp', charset='utf8mb4'
)String Functions and Character Sets
String functions operate in the character set and collation of their arguments. Mixing charsets in a function call can cause implicit conversion:
-- LENGTH vs CHAR_LENGTH
SELECT LENGTH('café'); -- 5 (bytes in utf8mb4: c=1, a=1, f=1, é=2)
SELECT CHAR_LENGTH('café'); -- 4 (characters: c, a, f, é)
-- LENGTH counts bytes; CHAR_LENGTH counts characters
-- For utf8mb4 strings, always use CHAR_LENGTH for display purposes
SELECT username, CHAR_LENGTH(username) AS char_count FROM users;
-- UPPER and LOWER respect collation
SELECT UPPER('café'); -- 'CAFÉ'
SELECT LOWER('CAFÉ'); -- 'café'
-- Comparing with explicit collation
SELECT 'résumé' = 'RÉSUMÉ' COLLATE utf8mb4_0900_ai_ci; -- 1 (accent+case insensitive)
SELECT 'résumé' = 'RÉSUMÉ' COLLATE utf8mb4_bin; -- 0 (binary comparison)Unicode Categories in MySQL Collations
The utf8mb4_0900_ai_ci collation (MySQL 8.0 default) implements Unicode 9.0 full case folding. This means more characters compare as equal than in the older utf8mb4_unicode_ci:
-- Ligature comparison (unicode_ci vs 0900)
SELECT 'Ⅷ' = 'VIII' COLLATE utf8mb4_unicode_ci; -- 0 (not equal)
SELECT 'Ⅷ' = 'VIII' COLLATE utf8mb4_0900_ai_ci; -- 1 (equal in 0900)
-- Emoji search — works with utf8mb4 but not utf8
SELECT * FROM users WHERE bio LIKE '%????%';
-- Check the number of bytes for different characters
SELECT CHAR_LENGTH('A'), LENGTH('A'); -- 1 char, 1 byte
SELECT CHAR_LENGTH('é'), LENGTH('é'); -- 1 char, 2 bytes (utf8mb4)
SELECT CHAR_LENGTH('????'), LENGTH('????'); -- 1 char, 4 bytes (4-byte emoji)Detecting Charset Problems
-- Check what charset a specific table is using SHOW CREATE TABLE usersG -- Look for: DEFAULT CHARSET=utf8mb4 -- Check all tables in a database that are NOT utf8mb4 SELECT TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_COLLATION NOT LIKE 'utf8mb4%' ORDER BY TABLE_NAME; -- Check all columns that are NOT utf8mb4 SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND CHARACTER_SET_NAME IS NOT NULL AND CHARACTER_SET_NAME != 'utf8mb4' ORDER BY TABLE_NAME, COLUMN_NAME;
Connection Charset Configuration in my.cnf
Set the default connection charset in my.cnf to avoid relying on application code to set it correctly:
[mysqld] character_set_server = utf8mb4 collation_server = utf8mb4_unicode_ci # These ensure the server -> client path is also utf8mb4 character_set_results = utf8mb4 character_set_connection = utf8mb4 [client] # Default connection charset for the mysql CLI client default-character-set = utf8mb4
-- Verify client-server charset agreement after connecting SHOW SESSION VARIABLES LIKE 'character_set%'; -- All of these should be utf8mb4 for a correctly configured setup: -- character_set_client utf8mb4 -- character_set_connection utf8mb4 -- character_set_database utf8mb4 -- character_set_results utf8mb4 -- character_set_server utf8mb4
Best Practices Summary
Set utf8mb4 and utf8mb4_unicode_ci (5.7) or utf8mb4_0900_ai_ci (8.0) at server, database, and table level.
Configure your application driver with charset: utf8mb4 in the connection options — never rely on server defaults.
Use utf8mb4_bin for columns that store tokens, API keys, password hashes, or other case-sensitive values.
Run ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4 on existing tables to migrate from legacy latin1.
Use SHOW CREATE TABLE to verify the actual charset and collation on any table you suspect has issues.
Check connection charset with SHOW SESSION VARIABLES LIKE "character_set%" after connecting.
When JOINing tables with mismatched collations, specify the collation explicitly in the ON clause to preserve index usage.
Audit all tables for non-utf8mb4 charsets using the information_schema query above as part of your onboarding to any codebase.