2011-12-25

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 さんです!んがんぐっ!

2011-11-17

split git またはこぼれたギットに泣かないで

CPANモジュールのdistを分割したい時などにgit repositoryも分割したいということになる。

が、分割先のrepositoryに含まれないファイルのコミットログなどが一緒に分割されてくるとログが肥大すぎていろいろとうざいときに泣かないために。

まあ、基本git filter-branch使いましょうという話です。

サブディレクトリをルートディレクトリとする場合

CPANモジュールだとこういう場合はないはずなので普通はこの方法はとれないけど一応メモ。

repository中の"hogedir"だけを取り出したい場合。

git clone ParentModule/.git SplitModule
cd SplitModule
git filter-branch --subdirectory-filter hogedir

基本これだけで十分なのだが、 git show commit-id を直接指定すると変更履歴がみれてしまう。別に問題ないんだろうけど以下もやっとくと完全に見れなくなる。

cd ../
git clone SplitModule/.git SplitModuleClean
cd SplitModuleClean
git gc --prune=now
cd ../
rm -rf SplitModule
mv SplitModuleClean SplitModule

なんか面倒ですね。多分もっと簡単な方法ある。

特定のファイルを取り除く

CPANモジュールだとこっちの方法になるとおもう。例えば

├── Changes                                                                                                                                                                       
├── MANIFEST.SKIP                                                                                                                                                                 
├── Makefile.PL                                                                                                                                                                   
├── README                                                                                                                                                                        
├── README.mkdn                                                                                                                                                                   
├── TODO                                                                                                                                                                          
├── author                                                                                                                                                                        
│   ├── assets.pl                                                                                                                                                                
│   └── test-externals.pl                                                                                                                                                        
├── lib                                                                                                                                                                           
│   ├── Amon2                                                                                                                                                                    
│   │   ├── Config                                                                                                                                                              
│   │   ├── Declare.pm                                                                                                                                                          
│   │   ├── Plugin                                                                                                                                                               
│   │   ├── Setup                                                                                                                                                               
│   │   ├── Trigger.pm                                                                                                                                                          
│   │   ├── Util.pm                                                                                                                                                              
│   │   ├── Web                                                                                                                                                                  
│   │   └── Web.pm                                                                                                                                                               
│   └── Amon2.pm                                                                                                                                                                 
└── script                                                                                                                                                                        
    └── amon2-setup.pl

のようなディレクトリ構成のモジュールがあったとして、libの下は lib/Amon2/Setup 以外は消したいとき。

git clone Amon/.git Amon2-Setup
git filter-branch --prune-empty --tree-filter '\
 rm -f lib/Amon2.pm;\
 rm -rf lib/Amon2/Config/;\
 rm -f lib/Amon2/Web.pm;\
 rm -rf lib/Amon2/Web/;\
 rm -rf lib/Amon2/Plugin/;\
 rm -f lib/Amon2/Declare.pm;\
 rm -f lib/Amon2/Trigger.pm;\
 rm -f lib/Amon2/Util.pm\
'

で、関係するコミットログが消えます。こちらもgit showで直接指定すればまだ見れますのでgit gc --prune=nowをcloneした別ディレクトリで実行するとよいです。

2011-11-17

TODO

あとで書く

  • cpanm -lとcpanm -Lの違い(いつも忘れて調べてるので)
  • cpanfileについての自分の理解
  • cartonのmultiple mirrorパッチ
  • TengとHandlerSocketとわたし
  • rjbsの言っていたMoose switch to Any::Mooseが危険なはなし
  • こぼれたギットに泣かないで => 書いた
  • うわ…私のPerlのバージョン低すぎ…
2011-11-17

DBIとforkの関係

実際ググれば正解はいっぱい出てくるしここに自分もコメントで書いてたりしていまさら書く必要もないかなと思ってたけど一応自分のブログでもまとめておくということで。

一般的な解

DBIx::ConnectorとかDBIx::Handler経由でかならず$dbhを取得してからDBIを使う。 もしくはfork-safeなORM(DBIx::Class, DBIx::Skinny, Teng)を使う。

DBIを直接使っている場合

一般的なコネクションを保持するクライアントと同様にDBIもforkした子供が親のコネクションをそのまま使うことはバグの原因になります。特にトランザクションの処理等で重大な問題が起こる可能性がある。 解決策は、

  1. DBIのコネクションを親で作らないで、子供で独自に作る
  2. 親で作ってしまったコネクションを子供が安全にDESTROYし、再接続する

のどちらかになります。ここで問題は2で「安全にDESTROY」しないとどうなるかというと、DBIはDESTROY時に自動的にdisconnectするようになっているので子供が勝手に親の接続を切ってしまい、その後親がその接続を使おうとすると使えないという事が起こる場合がある。(DBDによっても違うので「場合」としている)

安全にDESTROYするには、親で$dbh->{AutoInactiveDestroy} = 1;を設定する(> DBI 1.6.14)か、子で$dbh->{InactiveDestroy} = 1;を設定する必要がある。

で、再接続は$dbh->cloneを使うと楽。まとめるとこんな感じがおすすめですね。

use DBI;
my $dbh = DBI->connect('dbi:mysql:database=sandbox;host=localhost', 'sandbox', 'sandbox');

if ( fork ) {
    # 親
    wait;
    my $row = $dbh->selectrow_hashref('select * from sandbox limit 1');
}
else {
    # 子
    $dbh->{InactiveDestroy} = 1;
    $dbh = $dbh->clone({InactiveDestroy => 0}); # 新しく作成する$dbhはactive destroyでおk
    my $row = $dbh->selectrow_hashref('select * from sandbox limit 1');
}

 追記: 上記コード例のs/$dbh->clone;/$dbh->clone({InactiveDestroy => 0})/

2011-11-17

Amon2はAmon2::Setupを別distにしたほうがいいんじゃないかという話

carton をつかう人がふえてくるだろうという予測のもと、依存をへらす変更をしています

http://d.hatena.ne.jp/tokuhirom/20111114/1321232436

自分もcartonが普及するとプロジェクトのextlib(local)に依存はすべていれる方式が一般化していくと思っています。

で、そうすると開発環境のシェル上で使えるperlにはたいしてモジュールが入ってない状態になっていることが往々にしてあるというかそもそもそんなにいれる必要がないが、amon2-setup.plみたいなのはプロジェクト外のperlで使いたいという要求がでてくる。その時にAmon2自体を入れなきゃいけないと結構依存が多くて面倒。なので別distにしてあげて通常のシェル上で使っているperlのlibにはAmon2::Setupが入っていて、プロジェクトのextlib(local)にはAmon2::Setup抜きのAmon2のdistが入っていると効率がよくなると思う。

という話を#soozyでした。

追記: patches welcomeとのことなのでやってみる

2011-11-17

気楽にperlのはなしを書いていくことにする

ツイッタがはやりはじめてからずっといってるけど、エンジニーアの人はブログをかいた方がいいとおもっていて、ツイッタにたれながしているだけだと知見がなんかこう、がしっとはまらないというか、そういうかんじがしてる。

http://tokuhirom.hatenablog.com/entry/2011/11/17/070812

 

 そのとおりですなぁということで、このはてなブログでもっと吐き出すことにしていく。本ブログ的なのは

http://blog.everqueue.com/chiba/

のままでこっちは長めのものとかまとまったことを書く時にする。