MySQLの「Incorrect string value」の直し方 — utf8 に変えると、また同じエラーが出る
先に結論
Incorrect string value: '\xE3\x83\x9E...' for column 'name' at row 1
のようなエラーが出たとき、やってはいけないのは「とりあえず utf8 にする」です。
MySQL の utf8 は UTF-8 ではありません。
3バイトまでしか入らない別物(utf8mb3)の別名です。
これに変えると日本語は通るようになりますが、絵文字や一部の漢字で、同じエラーがまた出ます。
そして、原因はエラーに出ているバイト列を見れば分かります。
| エラーのバイト | 入れようとした文字 | 今の文字セット | 直す先 |
|---|---|---|---|
\xE3 \xE6 \xE9 \xE2 など\xEで始まる | 3バイト文字(ふつうの日本語) | latin1 など | utf8mb4 |
\xF0 で始まる | 4バイト文字(絵文字・一部の漢字) | utf8(=utf8mb3) | utf8mb4 |
どちらの場合も、行き先は utf8mb4 の一択です。
MySQL公式も「推奨される文字セットは utf8mb4 であり、すべての新しいアプリケーションは utf8mb4 を使うべき」と書いています。
直す場所は1か所ではありません。全部揃っていないと、どこかで落ちます。
| 場所 | 確認の仕方 | 直し方 |
|---|---|---|
| サーバーの既定値 | character_set_server | RDSならパラメータグループ(詳しく) |
| データベース | character_set_database | ALTER DATABASE。既存テーブルは変わらない |
| テーブル・カラム | SHOW CREATE TABLE | ALTER TABLE ... CONVERT TO(詳しく) |
| 接続 | character_set_client ほか | 接続文字列・ドライバ設定(詳しく) |
一番多い失敗は、サーバーやデータベースだけ直して終わりにすることです。
エラーはカラムで出ています。
ALTER DATABASE が変えるのは「これから作るテーブルの既定値」であって、
すでにあるテーブルには何も起きません。
エラーの最初のバイトを読む
3バイトか4バイトかで原因が違う
エラーに出ている \xE3\x83\x9E のような文字列は、
入れようとした文字をUTF-8でエンコードした生のバイトです。
UTF-8は文字によってバイト数が変わるので、ここを見れば何バイトの文字で落ちたかが分かります。
実際に日本語でよく使う文字をUTF-8にエンコードすると、こうなります。
| 文字 | UTF-8のバイト列 | バイト数 | utf8mb3に入るか |
|---|---|---|---|
| あ | \xE3\x81\x82 | 3 | 入る |
| マ | \xE3\x83\x9E | 3 | 入る |
| 漢 | \xE6\xBC\xA2 | 3 | 入る |
| ① | \xE2\x91\xA0 | 3 | 入る |
| 髙(はしごだか) | \xE9\xAB\x99 | 3 | 入る |
| 𠮷(つちよし) | \xF0\xA0\xAE\xB7 | 4 | 入らない |
| 𩸽(ほっけ) | \xF0\xA9\xB8\xBD | 4 | 入らない |
| 😀 | \xF0\x9F\x98\x80 | 4 | 入らない |
| 🍣 | \xF0\x9F\x8D\xA3 | 4 | 入らない |
「髙」が3バイトである点は押さえておいてください。
旧字体や異体字はすべて4バイトだと思われがちですが、違います。
「髙」は utf8mb3 でも入ります。入らないのは「𠮷」のほうです。
名前の登録で落ちたとき、どちらの字かで原因が変わります。
バイトから原因を引く
UTF-8は、先頭バイトを見れば何バイトの文字か分かるように作られています。 ここが診断に使えます。
| 先頭バイト | 文字のバイト数 | 落ちた相手の文字セット | 意味 |
|---|---|---|---|
\xC2〜\xDF | 2 | latin1 など | 記号・欧文の拡張文字で落ちている |
\xE0〜\xEF | 3 | latin1 など | ふつうの日本語で落ちている。utf8mb3なら通る |
\xF0〜\xF4 | 4 | utf8mb3(=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(要約)
| 名前 | 正体 | 最大バイト数 | 扱い |
|---|---|---|---|
utf8mb4 | 本物のUTF-8 | 4 | 公式の推奨。これを使う |
utf8mb3 | 3バイトまでのUTF-8 | 3 | 非推奨。将来削除予定 |
utf8 | utf8mb3 の別名 | 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インスタンスに付け替える必要があります。 デフォルトのものを開いて編集しようとしても、値が入力できません。
| 手順 | やること |
|---|---|
| 1 | 新しいDBパラメータグループを作る(エンジンとバージョンを合わせる) |
| 2 | character_set_server を utf8mb4 にする |
| 3 | collation_server も合わせる(utf8mb4_0900_ai_ci など) |
| 4 | DBインスタンスの設定を変更し、作ったパラメータグループに付け替える |
| 5 | 反映を確認する。static なら再起動(次項) |
| 6 | 既存のテーブルを変換する(これが本番) |
反映と再起動
AWS公式の説明はこうです。
| 種類 | 反映 |
|---|---|
| dynamic | 即時に反映される |
| static | 再起動が必要 |
パラメータごとにどちらかはコンソールで確認できます。
「設定したのに SHOW VARIABLES が変わらない」というときは、再起動していないだけのことが多いです。
もうひとつ、すでに張られている接続には反映されません。 確認するときは、つなぎ直してから見てください。
パラメータグループでは既存データは変わらない
ここを誤解したまま進むと、再起動まで済ませてから「まだ直っていない」となります。
| 操作 | 効く範囲 | 既存のテーブル |
|---|---|---|
パラメータグループ(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つ警告しています。
本番でいきなり流さないでください。
| 危険 | 公式の記述(要約) | 対処 |
|---|---|---|
| カラムの型が変わる | 「変換はカラムの型を変えることがあります。たとえば VARCHAR から MEDIUMTEXT へ」 | 変換後に SHOW CREATE TABLE で型を確認する |
| データが失われることがある | 「変換前後の両方の文字セットに無い文字が含まれていると、変換は非可逆になりえます」 | 先にバックアップ。スナップショットを取る |
| 中身のエンコードが前提 | 「変換前に、カラムの中身が正しい文字セットでエンコードされていることを確認することが重要です」 | すでに文字化けしているデータは、変換すると壊れたまま固定される |
3つめが見落とされがちです。すでに文字化けしているデータを CONVERT TO しても直りません。
「latin1のカラムにUTF-8のバイトが押し込まれている」状態は、
文字セットの宣言を変えるだけでは戻らないことがあります。
先にスナップショットを取り、本番ではないところで1回やってからにしてください。
インデックス長で引っかかる
変換が Specified key was too long で止まることがあります。
1文字あたりのバイト数が増えるので、インデックスの長さが上限を超えるからです。
MySQL公式が示しているInnoDBの上限は、行フォーマットによって違います。
| 行フォーマット | キーの上限 | VARCHAR(255) × utf8mb3 | VARCHAR(255) × utf8mb4 |
|---|---|---|---|
| DYNAMIC / COMPRESSED | 3072バイト | 765バイト → 通る | 1020バイト → 通る |
| REDUNDANT / COMPACT | 767バイト | 765バイト → 通る | 1020バイト → 超える |
いまのMySQLの既定の行フォーマットは DYNAMIC なので、 新しく作った環境ならこの問題は起きません。 引っかかるのは、古い環境から持ってきた、行フォーマットが COMPACT のままのテーブルです。
よく見かける VARCHAR(191) という指定は、この名残です。
191 × 4 = 764 バイトで、767の枠に収まります。
行フォーマットが DYNAMIC なら、191に縮める必要はありません。
対処は3つあり、上から順に検討します。
| 対処 | 内容 | 向き不向き |
|---|---|---|
| 行フォーマットを DYNAMIC にする | ALTER TABLE ... ROW_FORMAT=DYNAMIC; | まずこれ。上限が3072になる |
| インデックスに長さを指定する | KEY (col(191)) のように前方一致で張る | 完全一致の検索性能が落ちる場合がある |
| カラムを短くする | VARCHAR(255) → VARCHAR(191) | データが切れる可能性がある。最後の手段 |
接続側も揃える
データベース側を全部 utf8mb4 にしても、接続が utf8 のままだと途中で落ちます。
アプリ側のドライバが明示的に指定していることが多いので、そこも見てください。
| 環境 | 見る場所 |
|---|---|
| Rails | config/database.yml の encoding: |
| Laravel / PHP | config/database.php の charset と collation |
| WordPress | wp-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%' だけでは足りません。
それはサーバーと接続の話で、カラムの話ではないからです。
| 確認 | 見るもの | 期待 |
|---|---|---|
| サーバーと接続 | 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に任せるときの注意
| 頼むこと | 向くか | 理由 |
|---|---|---|
| エラー全文を貼って「何バイトの文字で落ちた?」 | 向く | 先頭バイトを読むだけ |
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)。 先にスナップショットを取ってください。
まとめ
- エラーの最初のバイトを読む。
\xEで始まれば3バイト文字=latin1、\xF0なら4バイト文字=utf8mb3(表1)。 - MySQLの
utf8は UTF-8 ではない。3バイトまでのutf8mb3の別名で、公式が非推奨としている。 utf8に変えると、絵文字と「𠮷」で同じエラーが再発する。行き先はutf8mb4の一択。- 公式は
utf8が将来utf8mb4の別名になる見込みと書いている。同じSQLの意味が変わるので、utf8mb4と明示する。 - 「髙」は3バイトなので
utf8mb3でも入る。4バイトなのは「𠮷」。 - RDSではデフォルトのパラメータグループは編集できない。新しく作って付け替える。
- パラメータグループも
ALTER DATABASEも、既存のテーブルには効かない。効くのはALTER TABLE ... CONVERT TOだけ。 CONVERT TOは型が変わることがあり、非可逆になることがある。先にスナップショットを取る。Specified key was too longは行フォーマットをDYNAMICにすれば上限が3072バイトになる。191に縮めるのは最後の手段。- 確認はカラム単位まで。最後に4バイト文字を1件入れて読み戻す。
同じ「エラー文字列から原因を切り分ける」型の話は Unexpected token '<' ... is not valid JSON の原因と直し方、 サーバー側のAPIを叩く話は WordPress REST API の認証を通す、 古いコードが検索結果に残り続ける話は CakePHPでループ中にsaveする にも書きました。
この記事は公式ドキュメントの記載と、UTF-8のバイト列の計算にもとづいています。
MySQLサーバーを実際に立てての再現は行っていません。
バージョンや行フォーマットによって挙動は変わります。
本番で ALTER を流す前に、必ずスナップショットを取り、別の環境で1回試してください。
仕様は変わるので、実行前に公式ドキュメントで最新を確認してください。