ucs2エンコーディングのススメ

はじめに

MySQL Casual Advent Calendar 2011 - MySQL Casualで24日目を担当します。nihenです。 といいながら、25日になっちゃっています。すみませんすみません。

さて、みなさんはテキスト系カラムのキャラクタセットは何を指定していますか?

  • cp932 に決まってるでござる
  • いやむしろ sjis でござる
  • いやいや5c問題を避けるために ujis でござる
  • 当然决定为Big5
  • 2010年代に utf8mb4 使わないで許されるのは小学生までだよねー
  • 漢はだまって binary

ここから述べることは、カジュアルに"utf8"キャラクタセットを使っている方々へのカジュアル情報です。上記に当てはまった漢のみなさまはそっとブラウザをお閉じください。

診断

SELECT SUM(LENGTH(column) - CHAR_LENGTH(column)*2) FROM table;

おもむろに、上記のクエリを適当なデータ量の多いutf8キャラクタセットにしているカラムに対して実行してみましょう。(column, tableを書き換えて実行してください)

帰ってきた値が正の値だった場合はその値のバイト数だけディスクスペースを節約できる可能性があります。負の値だった場合は残念でした。

方法

簡単です。そのカラムを"ucs2"キャラクタセットに変更するだけです。

ALTER TABLE table CHANGE COLUMN column column VARCHAR(255) CHARACTER SET 'ucs2' COLLATE 'ucs2_bin' NOT NULL;

などの方法で行えますね。これだけでディスクスペースが縮小されます。utf8とucs2は文字集合が共通(unicodeのBMP)なため、round-trip問題は発生しません。

え、でもプログラムからはutf-8mysqlに接続してるよと思った方はご安心ください。mysqlはカラムのキャラクタセットからcharacter_set_clientに指定されているキャラクタセットに変換を自動で行います。プログラム側に修正は必要ありません。

ucs2にするとutf8の場合の3byteキャラクタ(日本語とかがそうですね!)が2byteになるというメリットがあります。反面、1byteキャラクタ(asciiですね)が2byteに増えるというデメリットがあります。ですので日本語とasciiのどちらが多いかということが判断基準となります

実演

mysqlでのディスクスペースの削減といえば、InnoDB PluginのROW_FORMAT=COMPRESSEDなわけですが、以前id:sh2さんがMySQL InnoDB Pluginのデータ圧縮機能 - SH2の日記においてWikipedia日本語版のデータベースの圧縮を実演されてましたので、真似してみたいと思います。とはいえ時間があまりなかったので変更対象はpageテーブルのpage_titleカラムだけにしています。

xml2sql

wget http://ftp.tietew.jp/pub/wikipedia/xml2sql-0.5.tar.gz
tar xzf xml2sql-0.5.tar.gz
cd xml2sql-0.5
./configure
make
sudo make install

インポート用sql用意

mysqlimport用のデータを作るのがデフォルトなのですが、ucs2でのインポートはできないのでsqlファイルの作成を行っています。

wget http://dumps.wikimedia.org/jawiki/20111217/jawiki-20111217-pages-articles.xml.bz2
bunzip2 jawiki-20111217-pages-articles.xml.bz2
cat jawiki-20111217-pages-articles.xml | sed -e 's/<redirect \/>//' | xml2sql -m

sandbox_dbの用意

wget "http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/maintenance/tables.sql?view=co" -O tables.sql

echo 'create database sandbox_utf8;' | mysql -u root -p
echo 'create database sandbox_ucs2;' | mysql -u root -p

mysql -u root -p sandbox_utf8 < tables.sql
mysql -u root -p sandbox_ucs2 < tables.sql
echo "ALTER TABLE page CHANGE COLUMN page_title page_title VARCHAR(255) COLLATE 'ucs2_bin' NOT NULL;" | mysql -u root -p sandbox_ucs2

インポート

時間かかりますよー。

mysql -u root -p sandbox_utf8 --default-character-set=utf8 < page.sql
mysql -u root -p sandbox_ucs2 --default-character-set=utf8 < page.sql

確認

mysql>use sandbox_utf8
mysql> show table status like 'page';
+------+--------+---------+------------+---------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------+----------+----------------+---------+
| Name | Engine | Version | Row_format | Rows    | Avg_row_length | Data_length | Max_data_length | Index_length | Data_free | Auto_increment | Create_time         | Update_time | Check_time | Collation | Checksum | Create_options | Comment |
+------+--------+---------+------------+---------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------+----------+----------------+---------+
| page | InnoDB |      10 | Compact    | 1541446 |             98 |   151683072 |               0 |    220758016 |   5242880 |        2484792 | 2011-12-24 22:46:27 | NULL        | NULL       | utf8_bin  |     NULL |                |         |
+------+--------+---------+------------+---------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------+----------+----------------+---------+
1 row in set (0.00 sec)

mysql>use sandbox_ucs2
mysql> show table status like 'page';

+------+--------+---------+------------+---------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------+----------+----------------+---------+
| Name | Engine | Version | Row_format | Rows    | Avg_row_length | Data_length | Max_data_length | Index_length | Data_free | Auto_increment | Create_time         | Update_time | Check_time | Collation | Checksum | Create_options | Comment |
+------+--------+---------+------------+---------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------+----------+----------------+---------+
| page | InnoDB |      10 | Compact    | 1553463 |             93 |   145375232 |               0 |    210255872 |   5242880 |        2484792 | 2011-12-25 00:27:57 | NULL        | NULL       | utf8_bin  |     NULL |                |         |
+------+--------+---------+------------+---------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------+----------+----------------+---------+
1 row in set (0.01 sec)

というわけで、Data_lengthが151683072 => 145375232(6MB,5%弱削減)、Index_lengthが220758016 => 210255872(10MB,5%弱削減)という結果になりました。もうちょい減るかなーと期待してたのですが若干しょぼい結果にはなりましたが削減自体は成功ということで…。この5%を多いとみるか少ないと見るかは…。いややっぱ少ないかなー。まあカジュアル!ということで。

まとめ

utf8のキャラクタセットを採用しているカラムをucs2に変更するとスペースの削減が行える場合があることを紹介しました。ucs2にした場合のベンチマークなどの話は3年前の下記記事に書いてありますので興味のある方は併せてご覧ください

mysqlの内部キャラセットはucs2にするといいんじゃないだろうか | へぼい日記

mysqlでcharsetをucs2にした場合のasciiのみのフィールド | へぼい日記

さ〜て、明日は大トリ、 y_wakai さんです!んがんぐっ!