MySQLCharacter Sets & Collations

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

Warning
MySQL's 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.

SQL
-- 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:

Bash
[mysqld]
character_set_server = utf8mb4
collation_server     = utf8mb4_unicode_ci

2. Database level:

SQL
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:

SQL
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):

SQL
-- 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:

SQL
-- 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;
Tip
Modern MySQL drivers (mysql2 for Node.js, PDO for PHP, Connector/J for Java) set the connection charset automatically when you specify charset: utf8mb4 in the connection configuration. This is safer than running SET NAMES manually.
Viewing Character Set Information

SQL
-- 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

SQL
-- 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 only
Fixing 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.

SQL
-- 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:

Bash
# 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.sql
Warning
Always back up your data before changing character sets. Converting character sets modifies the actual stored bytes and is not easily reversible without a backup. Test on a copy first.
Collation Effects on Query Results

SQL
-- 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:

SQL
-- 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

SQL
-- 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

Bash
// 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:

SQL
-- 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:

SQL
-- 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

SQL
-- 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:

Bash
[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

SQL
-- 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
  1. Set utf8mb4 and utf8mb4_unicode_ci (5.7) or utf8mb4_0900_ai_ci (8.0) at server, database, and table level.

  2. Configure your application driver with charset: utf8mb4 in the connection options — never rely on server defaults.

  3. Use utf8mb4_bin for columns that store tokens, API keys, password hashes, or other case-sensitive values.

  4. Run ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4 on existing tables to migrate from legacy latin1.

  5. Use SHOW CREATE TABLE to verify the actual charset and collation on any table you suspect has issues.

  6. Check connection charset with SHOW SESSION VARIABLES LIKE "character_set%" after connecting.

  7. When JOINing tables with mismatched collations, specify the collation explicitly in the ON clause to preserve index usage.

  8. Audit all tables for non-utf8mb4 charsets using the information_schema query above as part of your onboarding to any codebase.