Fix Thai Characters Showing as ? in MySQL — UTF-8 Charset Guide

You insert Thai text into MySQL, but when you query it back you see ??? or garbled characters like àž àž²àžŠàž²àžšàž±àžš. This is a charset mismatch between the database, table, PHP connection, and client. It is one of the most common issues on shared hosting environments with default configurations.

This guide explains the root cause and provides a step-by-step fix — from phpMyAdmin to PHP connection — without needing server-level MySQL config access.

Quick fix: Set the charset to utf8mb4 and collation to utf8mb4_unicode_ci at the database level, table level, and in your PHP connection.

1. Why Do Thai Characters Show as ?

MySQL stores data as binary bytes using a charset (character set) and collation (sorting rules). Problems occur when:

When MySQL cannot map the incoming bytes to characters supported by the table's charset, it replaces them with ? or stores corrupted data.

2. Latin1 vs UTF-8 vs utf8mb4

CharsetThai SupportEmojiRecommendation
latin1NoNoAvoid
utf8 (MySQL)YesNo (3 bytes max)Works but incomplete
utf8mb4YesYes (4 bytes)Recommended

Important: MySQL's utf8 is NOT standard UTF-8. It only supports up to 3 bytes per character, meaning emoji and some scripts are silently corrupted. Always use utf8mb4.

3. Fix via phpMyAdmin — ALTER TABLE

Open phpMyAdmin via DirectAdmin and follow these steps:

Step 1: Change Database Charset

Click your database, go to the Operations tab, find "Collation", select utf8mb4_unicode_ci, and click Go. Or run SQL directly:

ALTER DATABASE `your_database_name`
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

Step 2: Convert All Tables

Run this for each table (replace the table name accordingly):

ALTER TABLE `your_table_name`
  CONVERT TO CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

The CONVERT TO keyword changes the charset on every column in the table. This is different from DEFAULT CHARACTER SET which only changes the default for new columns.

⚠️ Always backup first: Export your database as a .sql file before running ALTER TABLE. If data is corrupted during conversion, you will need the backup to restore.

4. Fix the PHP Connection

Even if the database uses utf8mb4, MySQL won't know what charset your PHP script is sending unless you tell it explicitly.

Option 1: PDO (Recommended)

// PDO connection with charset
$dsn = "mysql:host=localhost;dbname=mydb;charset=utf8mb4";
$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
]);

Option 2: mysqli

// mysqli connection
$conn = new mysqli($host, $user, $pass, $dbname);
$conn->set_charset("utf8mb4");

5. Fix for WordPress

WordPress handles this automatically since version 4.2. Verify your wp-config.php contains:

define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATE', 'utf8mb4_unicode_ci');

Note: If you migrated an old WordPress site, the tables may still use utf8. Run ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4 on each WordPress table, or use the WP-CLI command wp db convert-charset.

6. Why Thai Text Becomes ??? — Charset/Collation Mismatch at Every Layer

The most important thing to understand about this problem is that "Thai text turning into ???" is never caused by a single point of failure. It is caused by a charset mismatch across the multiple layers the data travels through — from the HTML form the user types into, all the way down to the actual bytes written to disk. If any one layer interprets the bytes incorrectly, your Thai characters break immediately, and the symptom you see depends on which layer is wrong.

Picture your Thai data traveling through four layers in order. Every layer must agree to use utf8mb4:

LayerWhere it is setWhat breaks if it is wrong
1. HTML / form<meta charset="UTF-8"> and the Content-Type headerThe browser sends bytes in the wrong encoding from the start
2. PHP connectionSET NAMES utf8mb4 / charset=utf8mb4 in the DSNMySQL thinks the client speaks latin1 and translates bytes wrongly
3. Table / columnCHARACTER SET utf8mb4 at CREATE/ALTER timeThe storage cell only accepts latin1, so out-of-range bytes become ?
4. DatabaseALTER DATABASE ... CHARACTER SET utf8mb4New tables keep inheriting the wrong charset forever

Common symptoms and how to read which layer is at fault:

Golden rule: verify all four layers are utf8mb4. Fixing only the table but forgetting the connection (or vice versa) leaves the problem in place, because the data must pass through every layer without a single mismatch.

7. Set utf8mb4 Correctly at Every Layer (DB · Table · Connection · HTML)

This section collects every command you need to run across all layers, ordered the way you would actually diagnose the issue. Start by inspecting the current state to find which layer is still latin1:

Step 1: Inspect the Current Charset at Each Layer

Open phpMyAdmin in DirectAdmin, go to the SQL tab, and run these checks:

-- Database charset/collation
SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
FROM information_schema.SCHEMATA
WHERE SCHEMA_NAME = 'your_database_name';

-- Charset of every table in the database
SELECT TABLE_NAME, TABLE_COLLATION
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_database_name';

-- Charset of each column in a table
SHOW FULL COLUMNS FROM `your_table_name`;

-- Charset the connection is currently using
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';

If character_set_client, character_set_connection, or character_set_results shows latin1, the connection layer is wrong and must be fixed in your PHP code.

Step 2: Set the Charset at Every Layer with SQL

-- Database layer
ALTER DATABASE `your_database_name`
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

-- Table layer (CONVERT TO changes every column at once)
ALTER TABLE `your_table_name`
  CONVERT TO CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

-- Connection layer (run after every connect)
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;

For the HTML layer, make sure the pages that display and submit data declare the correct meta charset in the <head>:

<meta charset="UTF-8">
// and the PHP side sends a matching header
header('Content-Type: text/html; charset=utf-8');

⚠️ Order matters: set the connection to utf8mb4 before inserting new data. If you insert while the connection is still latin1, the stored data is corrupted from the start — even if the table is already utf8mb4.

8. Convert latin1 Tables/Columns to utf8mb4 (ALTER, CONVERT)

When you have legacy tables created with latin1, conversion needs extra care because there are two distinct cases: (1) the column data is already correct Thai and only mislabeled, or (2) the data is already garbled from being stored under the wrong encoding.

Normal case: convert the whole table with CONVERT TO

If the existing data displays correctly (through a connection whose charset matched the one used at insert time), use CONVERT TO, which makes MySQL correctly re-encode the bytes of every column to utf8mb4:

ALTER TABLE `posts`
  CONVERT TO CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

Convert all tables at once (generate the statements)

If you have dozens of tables, run this query to generate the ALTER statement for every table, then copy the output and run it:

SELECT CONCAT(
  'ALTER TABLE `', TABLE_NAME,
  '` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'
) AS sql_command
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_database_name'
  AND TABLE_TYPE = 'BASE TABLE';

Special case: convert per column via BLOB (prevent data loss)

If CONVERT TO corrupts your data (because the existing data is already utf8mb4 bytes merely labeled as latin1), use the BLOB round-trip, which tells MySQL "don't touch the bytes, just relabel the charset":

// Step 1: change the column to BLOB (keep raw bytes)
ALTER TABLE `posts`
  MODIFY `content` BLOB;

// Step 2: change back to TEXT with the utf8mb4 label
ALTER TABLE `posts`
  MODIFY `content` TEXT
  CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

Which method to use: if the existing data displays correctly in phpMyAdmin (which defaults to utf8mb4), use plain CONVERT TO. But if the data only displays correctly when the connection is set to latin1, use the BLOB technique to preserve the original bytes.

9. Repair Already-Garbled Data (Double-Encoding) + WordPress wp-config

Double-encoding is when utf8mb4 data is interpreted as latin1 and then encoded as utf8 a second time, turning a single Thai character into a long string of garbled characters such as ภาษาไทย instead of "ภาษาไทย". In this case the data is not lost and can be recovered.

How to recover double-encoded data

The trick is to tell MySQL to interpret the current bytes as latin1 (to strip the outer encode layer), then read them back as utf8mb4:

// Preview the result first (does not modify real data)
SELECT CONVERT(CAST(CONVERT(`content` USING latin1) AS BINARY) USING utf8mb4)
FROM `posts` LIMIT 5;

// Once the preview looks right, run the real UPDATE (always back up first!)
UPDATE `posts`
SET `content` = CONVERT(CAST(CONVERT(`content` USING latin1) AS BINARY) USING utf8mb4);

⚠️ Never run the UPDATE twice: the double-encoding recovery command must be run exactly once. Running it again corrupts the data into another layer. Export a backup first, then test with SELECT until you are sure before running the UPDATE.

Set the charset in WordPress (wp-config.php)

WordPress sets the charset for you since version 4.2, but if you migrated an old site, verify wp-config.php contains:

<?php
// WordPress database charset
define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATE', 'utf8mb4_unicode_ci');

If the old WordPress tables are still utf8 or latin1, run ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4 on every WordPress table (such as wp_posts, wp_postmeta, wp_comments), or use WP-CLI if your hosting supports it:

# Convert the charset of every table via WP-CLI
wp db query "ALTER DATABASE \`$(wp config get DB_NAME)\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

WordPress on DirectAdmin: if you have no SSH/WP-CLI access, you can convert every table through phpMyAdmin using the generator query in section 8, then copy and run the output — the result is identical.

10. Fix Checklist

  1. Back up the database before making any changes.
  2. Set the database charset to utf8mb4 in phpMyAdmin.
  3. Run ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4 for every table.
  4. Add charset=utf8mb4 to the PDO DSN string.
  5. Issue SET NAMES utf8mb4 after every connection.
  6. Test by inserting Thai text and selecting it back.
  7. Inspect columns with SHOW FULL COLUMNS FROM table_name if issues persist.

Frequently Asked Questions (FAQ)

Why does Thai data look fine in phpMyAdmin but show as ??? on my website?

Because phpMyAdmin sets its connection to utf8mb4 by default, so it reads the data correctly. Your site's PHP code, however, may not issue SET NAMES utf8mb4 after connecting, so MySQL returns the data as latin1 and it breaks. The fix is to add charset=utf8mb4 to your PDO DSN or call $conn->set_charset("utf8mb4") with mysqli every time.

After converting to utf8mb4, will my old ??? data come back?

No. If the data is already stored as ?, the original bytes were dropped at insert time (a latin1 column could not accept utf8 bytes). That data is permanently lost and can only be restored from a backup. However, if the symptom is mojibake (ภาษา), the bytes are intact and can be recovered with the double-encoding technique.

Should I use utf8mb4_unicode_ci or utf8mb4_general_ci?

Use utf8mb4_unicode_ci. It sorts and compares characters from multiple languages more accurately per the Unicode standard, while utf8mb4_general_ci is slightly faster but less correct. For sites mixing Thai with other languages, unicode_ci is the better choice (MySQL 8 also offers the newer utf8mb4_0900_ai_ci).

Do I really need to back up before converting the charset?

Yes — especially for CONVERT TO and the double-encoding UPDATE, which modify real data. Export the database to a .sql file via phpMyAdmin in DirectAdmin first. If the result is not what you expected, you can restore.

Hosting with Full MySQL and phpMyAdmin Support

AsiaGB Hosting starts at 500 THB/year with DirectAdmin, phpMyAdmin, PHP 7.4/8.2/8.3, and SSD storage — everything you need to run a professional website.

View Hosting Plans