MySQL:utf8に「騙される」な — 絵文字や文字化けをutf8mb4で完全に解決する秘策

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

午前2時の悪夢:データが疑問符に変わる時

スマートフォンが激しく震える。運用チームからのSlack通知が止まらない。「アプリでエラーが発生しました。お客様のコメントがすべて『???』や文字化けになっています」。飛び起きてログを確認すると、そこには惨状が広がっていた。データベースに保存される際、すべての絵文字やアクセント付きの文字が完全に崩れてしまっていたのだ。

失敗の原因は、utf8という名前を過信していたことにある。MySQLにおいて、utf8は私たちが一般的に知っている標準的な UTF-8 ではない。もしあなたが文字化けや、絵文字(😭など)が保存できない問題に悩まされているなら、この記事が解決の鍵となるだろう。これは、テラバイト級のデータベースクラスターを管理してきた私の苦い経験から得た教訓である。

残酷な真実:なぜMySQLのutf8は「罠」なのか?

私たちの多くは、あらゆる文字をサポートしていると思い込み、データベース作成時にutf8を選択する。しかし実際には、MySQLのutf8utf8mb3(1文字あたり最大3バイト)に過ぎない。一方で、現代の絵文字や特殊な文字は4バイトを必要とする。

特徴 latin1 utf8 (utf8mb3) utf8mb4
1文字あたりの最大バイト数 1バイト 3バイト 4バイト
絵文字対応 非対応 失敗(100%エラー) フルサポート
ストレージ容量 最小 標準 utf8より約10-20%増加
適した用途 純粋な英語データ レガシーシステム すべてのモダンなプロジェクト

4バイトの文字をutf8mb3の列に挿入しようとすると、MySQLはデータを切り捨てるか、Incorrect string valueエラーを吐き出す。安全を期すなら、utf8のことは忘れ、常にデフォルトでutf8mb4を使用すべきだ。

正しいCollation(照合順序)の選び方

Character Set(文字セット)が保存方法であるなら、Collationは比較やソートのための規則だ。Collationの選択を誤ると、「a」を検索したのに「á」までヒットしてしまうといった「不可解な」挙動を招く。

  • utf8mb4_general_ci: 複雑な規則を省略しているため、速度は最速。しかし、特殊文字の扱いがやや「雑」であり、例えば ‘ß’ と ‘s’ を同一視することがある。
  • utf8mb4_unicode_ci: Unicode標準に準拠した正確な比較を行う。言語ごとの差異を正しく認識するが、計算のためにCPU負荷がわずかに(クエリにより5-10%程度)高くなる。
  • utf8mb4_0900_ai_ci: MySQL 8.0における最適解。unicode_ciよりも高速で、Accent Insensitive(アクセントを区別しない)のサポートも非常に優れている。

新規プロジェクトのための黄金律

データベースが数十GBに膨れ上がってから変換しようと考えてはいけない。初日から以下のルールを適用しよう:

  1. MySQL 8.0+ を使用する: utf8mb4utf8mb4_0900_ai_ci の組み合わせを優先する。
  2. MySQL 5.7 を使用する: utf8mb4utf8mb4_unicode_ci を使用する。
  3. カラムサイズに注意: VARCHAR(255) には注意が必要だ。utf8mb4では1文字最大4バイト消費するため、旧世代のInnoDBにおけるインデックス制限(767バイト)に抵触しやすくなる。

導入手順と標準設定

稼働中のデータベースを変更するのは繊細な作業だ。ALTERコマンドを実行する前には、必ずデータのバックアップを取ること

1. 現在の状態を確認する

-- 現在のデータベースの文字コードと照合順序を確認する
SELECT @@character_set_database, @@collation_database;

2. データベースとテーブルを変換する

各カラムを個別に修正するのではなく、テーブル全体を変換してMySQLにメタデータを自動処理させるのが賢明だ。

-- データベース全体を変換する
ALTER DATABASE my_project CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- テーブルを変換する(注:このコマンドは一時的にテーブルをロックします)
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

3. サーバー側の設定 (my.cnf)

データベースを修正しても、クライアントからの接続がlatin1のままだとエラーが続く。設定ファイルを編集して、すべての接続でutf8mb4を強制しよう:

[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# クライアントが勝後にlatin1に変更するのを防ぐため、文字セットのハンドシェイクを無視する
character-set-client-handshake = FALSE

4. アプリケーション側の同期

コード側でも、どの言語で対話しているかを認識させる必要がある。Node.js (mysql2) や PHP では、接続文字列の中で文字セットを指定する。

// Node.jsの設定
const db = mysql.createConnection({
  host: 'localhost',
  charset: 'utf8mb4' // この行がないと、DBがどれほど優秀でも文字化けが発生します
});

実戦経験:意外な落とし穴

以前、DBもコードも完璧にutf8mb4に設定したはずなのに、半日かけてデバッグしたことがあった。原因は、接続を仲介するProxySQLにあった。当時のProxySQLはデフォルトでutf8を使用しており、DBに届く前に絵文字の4バイト目を密かに切り捨てていたのだ。教訓はこうだ:App -> Proxy -> DBというスタック全体で同期を確認しなければならない。

もう一つの古典的なエラーは Specified key was too long; max key length is 767 bytes だ。utf8mb4に切り替えると、VARCHAR(255)のカラムは最大1020バイト(255×4)を占有し、古いInnoDBのインデックス制限を超えてしまう。解決策は、MySQL 8.0にアップグレードするか、カラム長を VARCHAR(191) に短縮してインデックスの安全性を確保することだ。

文字セットをマスターすることは難しくない。必要なのは細部へのこだわりだ。今すぐ utf8mb4 に標準化し、ユーザーがハート ❤️ やロケット 🚀 のアイコンを、無機質な疑問符に変わる心配なく自由に送れるようにしよう。

Share: