MySQLの「Incorrect string value」の直し方 — utf8 に変えると、また同じエラーが出る

先に結論

Incorrect string value: '\xE3\x83\x9E...' for column 'name' at row 1 のようなエラーが出たとき、やってはいけないのは「とりあえず utf8 にする」です。

MySQL の utf8 は UTF-8 ではありません。 3バイトまでしか入らない別物(utf8mb3)の別名です。 これに変えると日本語は通るようになりますが、絵文字や一部の漢字で、同じエラーがまた出ます。

そして、原因はエラーに出ているバイト列を見れば分かります。

表1:エラーの最初のバイトで原因が決まる
エラーのバイト入れようとした文字今の文字セット直す先
\xE3 \xE6 \xE9 \xE2 など\xEで始まる3バイト文字(ふつうの日本語)latin1 などutf8mb4
\xF0 で始まる4バイト文字(絵文字・一部の漢字)utf8(=utf8mb3)utf8mb4

どちらの場合も、行き先は utf8mb4 の一択です。 MySQL公式も「推奨される文字セットは utf8mb4 であり、すべての新しいアプリケーションは utf8mb4 を使うべき」と書いています。

直す場所は1か所ではありません。全部揃っていないと、どこかで落ちます。

表2:直す場所のチェックリスト
場所確認の仕方直し方
サーバーの既定値character_set_serverRDSならパラメータグループ(詳しく)
データベースcharacter_set_databaseALTER DATABASE。既存テーブルは変わらない
テーブル・カラムSHOW CREATE TABLEALTER TABLE ... CONVERT TO(詳しく)
接続character_set_client ほか接続文字列・ドライバ設定(詳しく)

一番多い失敗は、サーバーやデータベースだけ直して終わりにすることです。 エラーはカラムで出ています。 ALTER DATABASE が変えるのは「これから作るテーブルの既定値」であって、 すでにあるテーブルには何も起きません。

エラーの最初のバイトを読む

3バイトか4バイトかで原因が違う

エラーに出ている \xE3\x83\x9E のような文字列は、 入れようとした文字をUTF-8でエンコードした生のバイトです。 UTF-8は文字によってバイト数が変わるので、ここを見れば何バイトの文字で落ちたかが分かります。

実際に日本語でよく使う文字をUTF-8にエンコードすると、こうなります。

表3:文字とUTF-8のバイト列
文字UTF-8のバイト列バイト数utf8mb3に入るか
あ\xE3\x81\x823入る
マ\xE3\x83\x9E3入る
漢\xE6\xBC\xA23入る
①\xE2\x91\xA03入る
髙(はしごだか)\xE9\xAB\x993入る
𠮷(つちよし)\xF0\xA0\xAE\xB74入らない
𩸽(ほっけ)\xF0\xA9\xB8\xBD4入らない
😀\xF0\x9F\x98\x804入らない
🍣\xF0\x9F\x8D\xA34入らない

「髙」が3バイトである点は押さえておいてください。 旧字体や異体字はすべて4バイトだと思われがちですが、違います。 「髙」は utf8mb3 でも入ります。入らないのは「𠮷」のほうです。 名前の登録で落ちたとき、どちらの字かで原因が変わります。

バイトから原因を引く

UTF-8は、先頭バイトを見れば何バイトの文字か分かるように作られています。 ここが診断に使えます。

表4:先頭バイトと、そこから分かること
先頭バイト文字のバイト数落ちた相手の文字セット意味
\xC2〜\xDF2latin1 など記号・欧文の拡張文字で落ちている
\xE0〜\xEF3latin1 などふつうの日本語で落ちている。utf8mb3なら通る
\xF0〜\xF44utf8mb3(=utf8)絵文字・サロゲートペアの漢字で落ちている

\xF0 が見えたら、すでに utf8 になっている可能性が高いということです。 「utf8にしたのにエラーが出る」という状況は、これです。 設定が効いていないのではなく、設定した先が足りていません。

utf8 は UTF-8 ではない

公式が何と書いているか

MySQL公式マニュアルの記述は、はっきりしています。

utf8mb3 について: 「utf8mb3 文字セットは非推奨です。 MySQL 8.0.x および MySQL 8.4.x LTS リリースの間はサポートされます。 将来のメジャーリリースで削除される見込みです。」
「MySQL で推奨される文字セットは utf8mb4 です。新しいアプリケーションはすべて utf8mb4 を使用すべきです。」
— MySQL 8.4 Reference Manual 12.9.2(要約)

表5:3つの名前の関係
名前正体最大バイト数扱い
utf8mb4本物のUTF-84公式の推奨。これを使う
utf8mb33バイトまでのUTF-83非推奨。将来削除予定
utf8utf8mb3 の別名3非推奨の別名。使わない

utf8 の意味は、将来変わる

ここが一番大事なところです。公式マニュアルは utf8 についてこう書いています。

「utf8mb3 の非推奨の別名です。代わりに utf8mb4 を使ってください。 将来のリリースで utf8mb4 の別名になることが見込まれます。」
— MySQL 8.4 Reference Manual 12.10.1(要約)

つまり、スキーマに utf8 と書いてあると、それが指すものが将来変わります。 いまは3バイト、将来は4バイト。同じSQLが、MySQLのバージョンによって違う意味になります。

これは「古いから書き換えましょう」という話ではなく、 意味が確定していない名前を使っているという話です。 utf8mb4 と書けば、どのバージョンでも4バイトです。 utf8 とは書かない。これだけ守れば先々困りません。

RDSで直す

デフォルトのパラメータグループは編集できない

RDSでサーバーの既定文字セットを変えるには、DBパラメータグループを使います。 ここで最初につまずくのが、これです。

「デフォルトのパラメータグループは変更できません。」
— Amazon RDS のパラメータグループ(AWS公式)

新しくパラメータグループを作って、DBインスタンスに付け替える必要があります。 デフォルトのものを開いて編集しようとしても、値が入力できません。

表6:RDSで文字セットを変える手順
手順やること
1新しいDBパラメータグループを作る(エンジンとバージョンを合わせる)
2character_set_server を utf8mb4 にする
3collation_server も合わせる(utf8mb4_0900_ai_ci など)
4DBインスタンスの設定を変更し、作ったパラメータグループに付け替える
5反映を確認する。static なら再起動(次項)
6既存のテーブルを変換する(これが本番)

反映と再起動

AWS公式の説明はこうです。

表7:パラメータの種類と反映のタイミング
種類反映
dynamic即時に反映される
static再起動が必要

パラメータごとにどちらかはコンソールで確認できます。 「設定したのに SHOW VARIABLES が変わらない」というときは、再起動していないだけのことが多いです。

もうひとつ、すでに張られている接続には反映されません。 確認するときは、つなぎ直してから見てください。

パラメータグループでは既存データは変わらない

ここを誤解したまま進むと、再起動まで済ませてから「まだ直っていない」となります。

表8:どの操作が、何に効くか
操作効く範囲既存のテーブル
パラメータグループ(character_set_server)これから作るデータベースの既定値変わらない
ALTER DATABASE ... CHARACTER SETこれから作るテーブルの既定値変わらない
ALTER TABLE ... CONVERT TO CHARACTER SETそのテーブルの既存カラムとデータ変わる

エラーが出ているのはカラムです。 上2つは「これから」の話なので、いま落ちている INSERT は直りません。 3行目をやって、はじめて直ります。

既存のデータを変換する

順番を間違えない

変換は上から順にやります。 逆順にすると、変換したそばから古い既定値で作り直されることがあります。

-- 1. いまの状態を確認する
SHOW VARIABLES LIKE 'char%';
SHOW VARIABLES LIKE 'collation%';

-- 2. テーブルごとの実際の文字セットを見る(ここが本当の現状)
SELECT TABLE_NAME, TABLE_COLLATION
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'mydb';

-- 3. カラム単位で、utf8mb4 でないものを洗い出す
SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME, COLUMN_TYPE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'mydb'
  AND CHARACTER_SET_NAME IS NOT NULL
  AND CHARACTER_SET_NAME <> 'utf8mb4';

-- 4. データベースの既定値を変える(これから作るテーブル用)
ALTER DATABASE mydb
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_0900_ai_ci;

-- 5. テーブルを変換する(既存データはここで変わる)
ALTER TABLE mytable
  CONVERT TO CHARACTER SET utf8mb4
  COLLATE utf8mb4_0900_ai_ci;

3番のクエリが要です。 テーブル単位の TABLE_COLLATION が utf8mb4 でも、 個別に文字セットを指定されたカラムが残っていることがあります。 カラム単位で洗い出してください。

CONVERT TO の3つの危険

CONVERT TO は便利ですが、MySQL公式マニュアルが3つ警告しています。 本番でいきなり流さないでください。

表9:公式が警告していること
危険公式の記述(要約)対処
カラムの型が変わる「変換はカラムの型を変えることがあります。たとえば VARCHAR から MEDIUMTEXT へ」変換後に SHOW CREATE TABLE で型を確認する
データが失われることがある「変換前後の両方の文字セットに無い文字が含まれていると、変換は非可逆になりえます」先にバックアップ。スナップショットを取る
中身のエンコードが前提「変換前に、カラムの中身が正しい文字セットでエンコードされていることを確認することが重要です」すでに文字化けしているデータは、変換すると壊れたまま固定される

3つめが見落とされがちです。すでに文字化けしているデータを CONVERT TO しても直りません。 「latin1のカラムにUTF-8のバイトが押し込まれている」状態は、 文字セットの宣言を変えるだけでは戻らないことがあります。 先にスナップショットを取り、本番ではないところで1回やってからにしてください。

インデックス長で引っかかる

変換が Specified key was too long で止まることがあります。 1文字あたりのバイト数が増えるので、インデックスの長さが上限を超えるからです。

MySQL公式が示しているInnoDBの上限は、行フォーマットによって違います。

表10:インデックスキーの上限と、VARCHAR(255)の計算
行フォーマットキーの上限VARCHAR(255) × utf8mb3VARCHAR(255) × utf8mb4
DYNAMIC / COMPRESSED3072バイト765バイト → 通る1020バイト → 通る
REDUNDANT / COMPACT767バイト765バイト → 通る1020バイト → 超える

いまのMySQLの既定の行フォーマットは DYNAMIC なので、 新しく作った環境ならこの問題は起きません。 引っかかるのは、古い環境から持ってきた、行フォーマットが COMPACT のままのテーブルです。

よく見かける VARCHAR(191) という指定は、この名残です。 191 × 4 = 764 バイトで、767の枠に収まります。 行フォーマットが DYNAMIC なら、191に縮める必要はありません。

対処は3つあり、上から順に検討します。

表11:キー長で止まったときの対処
対処内容向き不向き
行フォーマットを DYNAMIC にするALTER TABLE ... ROW_FORMAT=DYNAMIC;まずこれ。上限が3072になる
インデックスに長さを指定するKEY (col(191)) のように前方一致で張る完全一致の検索性能が落ちる場合がある
カラムを短くするVARCHAR(255) → VARCHAR(191)データが切れる可能性がある。最後の手段

接続側も揃える

データベース側を全部 utf8mb4 にしても、接続が utf8 のままだと途中で落ちます。 アプリ側のドライバが明示的に指定していることが多いので、そこも見てください。

表12:接続側で見るところ
環境見る場所
Railsconfig/database.yml の encoding:
Laravel / PHPconfig/database.php の charset と collation
WordPresswp-config.php の DB_CHARSET
JDBC接続URLの characterEncoding
Docker / my.cnf[client] と [mysqld] の character-set 系
生のSQL接続直後の SET NAMES utf8mb4;

ここに utf8 と書かれているのを utf8mb4 に変えます。 設定ファイルを utf8 で検索すると、だいたい1つか2つ残っています。

直ったか確認する

SHOW VARIABLES LIKE 'char%' だけでは足りません。 それはサーバーと接続の話で、カラムの話ではないからです。

表13:確認する順番
確認見るもの期待
サーバーと接続SHOW VARIABLES LIKE 'char%';binary 以外がすべて utf8mb4
テーブルSHOW CREATE TABLE mytable;末尾が DEFAULT CHARSET=utf8mb4
カラムinformation_schema.COLUMNS をさっきのクエリで0件
実際に入るか4バイト文字を1件 INSERT してみる通る
読み出せるか入れたものを SELECT する化けずに返る

最後の2行をやってください。 設定が全部 utf8mb4 に見えても、接続のどこかに utf8 が残っていると、 入れるときは通って読むときに化けるという状態になります。 絵文字を1件入れて読み戻すのが、一番早い確認です。

AIに任せるときの注意

表14:AIへの頼み方
頼むこと向くか理由
エラー全文を貼って「何バイトの文字で落ちた?」向く先頭バイトを読むだけ
SHOW CREATE TABLE を貼って「直すSQLを出して」向く現状が確定しているので正確
「全テーブルの変換SQLを生成して」向くinformation_schema から機械的に作れる
「MySQLの文字コードをUTF-8にして」向かないutf8 を提案してくる。2010年代の記事が大量にある
本番への ALTER を任せる向かない型が変わる・非可逆になる(表9)。人が確認する
「文字化けしたデータを直して」向かない元のバイト列が要る。推測で直すと壊れる

判定は簡単です。出てきたSQLに utf8 とだけ書いてあったら、情報が古いと思ってください。 utf8mb4 と書いてあれば現代のものです。

よくある質問

utf8 にしたのにエラーが出ます

エラーのバイトが \xF0 で始まっていませんか。 それは4バイト文字で、utf8(=utf8mb3)には入りません(表4)。 utf8mb4 にしてください。

utf8 と utf8mb4 の違いは何ですか

utf8 は utf8mb3 の別名で、3バイトまでしか入りません。 utf8mb4 は4バイトまで入る本物のUTF-8です。 公式は utf8mb3 を非推奨とし、将来のメジャーリリースで削除する見込みとしています(表5)。

いま utf8 と書いてあるSQLは、そのままでいいですか

よくありません。公式が「将来のリリースで utf8mb4 の別名になる見込み」と書いています(詳しく)。 同じSQLの意味がバージョンによって変わります。 utf8mb4 と明示してください。

ALTER DATABASE をしたのに直りません

ALTER DATABASE が変えるのはこれから作るテーブルの既定値だけです。 既存のテーブルには何も起きません(表8)。 ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4 が要ります。

RDSのパラメータグループが編集できません

デフォルトのパラメータグループは変更できない仕様です(AWS公式)。 新しく作って、DBインスタンスに付け替えてください。

設定したのに SHOW VARIABLES が変わりません

static パラメータなら再起動が必要です(表7)。 また、すでに張られている接続には反映されません。つなぎ直して確認してください。

Specified key was too long が出ます

1文字あたりのバイト数が増えて、インデックスの上限を超えています。 行フォーマットを DYNAMIC にすると上限が3072バイトになり、たいてい解決します(表10・表11)。 VARCHAR(191) に縮めるのは最後の手段です。

「髙」は4バイトですか

3バイトです(\xE9\xAB\x99)。utf8mb3 でも入ります(表3)。 4バイトなのは「𠮷」のほうです。名前の登録で落ちたときは、どちらの字か確認してください。

入れられるのに読むと化けます

接続側のどこかに utf8 が残っています(表12)。 データベースを全部直しても、ドライバの設定が別に効いています。

すでに文字化けしているデータは変換で直りますか

直りません。むしろ壊れたまま固定されます。 公式も「変換前に、カラムの中身が正しい文字セットでエンコードされていることを確認することが重要」としています(表9)。 先にスナップショットを取ってください。

まとめ

同じ「エラー文字列から原因を切り分ける」型の話は Unexpected token '<' ... is not valid JSON の原因と直し方、 サーバー側のAPIを叩く話は WordPress REST API の認証を通す、 古いコードが検索結果に残り続ける話は CakePHPでループ中にsaveする にも書きました。

この記事は公式ドキュメントの記載と、UTF-8のバイト列の計算にもとづいています。 MySQLサーバーを実際に立てての再現は行っていません。 バージョンや行フォーマットによって挙動は変わります。 本番で ALTER を流す前に、必ずスナップショットを取り、別の環境で1回試してください。 仕様は変わるので、実行前に公式ドキュメントで最新を確認してください。