2007年12月02日

MySQLの照合順序:utf8_unicode_ciってなんぞ?

前回はヴァリデートのコードをそれぞれのメソッドの頭に記述して、なんちゃって入力チェックのようなことをやった経緯を書きました。あんな実装でも変なエラーが出なくなってきたので、まあヨシとしてます。

んで、ようやく今回は検索機能の強化?について書けるかな?と考えていたんですが、どうやろうか?などと思案しながらスクリプトをいじっていると奇妙な現象?に遭遇したので、検索機能の強化はまた後回しにして今回はそれについて書きます。

結論から言うと自分がMySQLの照合順序なるものを全く理解していなかったというだけの話です。「あ〜それね」「今更何言ってんの?」と言い切れる方は以下は読む必要はありません。なので以下は個人的なメモです(愚痴とも言う…マタカヨ)

まず奇妙な現象というのは以下のようなモノです。

例の五十音パッドで「た」で始まる新市町村を検索します。

伊達市 2006-03-01
大仙市 2005-03-22
田村市 2005-03-01
伊達市 2006-01-01
高崎市 2006-01-23
高崎市 2006-10-01
胎内市 2005-09-01
 :

簡易表示だと上記のような感じに結果が表示されます。

ここで伊達市や高崎市がダブって表示されているのは別段問題ではなく、合併日を見ればわかりますが要するに二段階で合併してる?というか、高崎市の場合だとまず2006-01-23に高崎市・倉渕村・箕郷町・群馬町・新町の5つの市町村が合併して高崎市になり、その後2006-10-01に榛名町が合併したということです。伊達市の場合は単純に同名の市が北海道と福島県にあったというだけです。

ん? 伊達市? だてし? 「だ」てし?

これが奇妙な現象で、「なんで伊達市が検索されてるんだ?」という話です。SQLは下記のようになっています。
SELECT pid, pref, nid, nc, yomi, oid, oc, oyomi, gdate FROM (SELECT DISTINCT pid, pref, nid, nc, yomi, gdate FROM cities WHERE SUBSTRING(yomi FROM 1 FOR 1) = 'た' ORDER BY yomi asc LIMIT 10, 10) AS vn JOIN newcities_oldcities j ON vn.nid = j.newcity_id JOIN oldcities o ON o.oid = j.oldcity_id WHERE SUBSTRING(yomi FROM 1 FOR 1) = 'た'

改行してないから見づらいですが、問題はWHERE句のところで…。

ちなみに外側のクエリとサブクエリに同じWHERE句が発行されていますが、これはバグだとか今回の件に関連することではなくて仕様です。詳細は五十音パッドの検索部分の実装(switch case編)の後半部分で述べているとおりですが、旧市町村が検索された場合だけでなく常に外側のクエリにもWHERE句を生成する実装にしてあります。どうしてかというと条件判断を書きたくなかったもんで(--;

で、それはともかく検索条件は
WHERE SUBSTRING(yomi FROM 1 FOR 1) = 'た'

なわけです。「だ」なんてどこにも書いてないじゃん!実はこの部分のクエリを生成しているコードは手抜き?というか、気づいてはいたんだけどとりあえず後回しにしていた部分でした。

具体的には「た」だけではなくて「た」or「だ」のように検索したかったのです。クエリを書くとすれば…
WHERE SUBSTRING(yomi FROM 1 FOR 1) = 'た' or SUBSTRING(yomi FROM 1 FOR 1) = 'だ'

みたいな感じでしょうか?当然半濁点の可能性のあるハ行などは「は」「ば」「ぱ」に関してorで繋げなければなりません。しかも濁点のつかない行とかもあるし(マ行、ラ行)。そんなわけで「こんな場合分けやってられるか!」と半ば放棄して後回しにしていたわけです。

ところがなんと!いざ検索してみれば実装もしてないのに期待通りの結果が表示されてるぅ?!これは寝ている間にPCの中の人が勝手に…ナワケナイ

そもそも自分の中では↑のように考えていたわけで、これは動作は期待通りですが、コード的にはバグのようなもんなわけです(個人的には…)。

なので五十音パッドを出力する際にもわざわざ半角カナに変換して、濁点・半濁点を取り去って、また平仮名に戻して、重複削除して〜のようなコードも書いていたわけです。
function _getCap($field)
{
//濁点・半濁点を清音に吸収するため、半角仮名で処理した後に重複削除
foreach($this->Gdb->getCap($field)->result_array() as $row)
{
$cap = mb_convert_kana($row['cap'], 'h', 'UTF-8');
$cap = mb_substr($cap, 0, 1, 'UTF-8');
$kana[] = mb_convert_kana($cap, 'H', 'UTF-8');
}

$kana = array_unique($kana);

return $kana;
}

このメソッドはデータベースから取ってくる時のコードなので、このようにmb系の関数を使ってなんとか濁点を吸収(濁点・半濁点などを清音の文字と同一視するの意)できていますが、上述のように(清音で)検索する際にはこれの逆変換?というか、とにかく面倒くさそうだなぁということで後回しにしていたのです。

んで、まあおおよそのところMySQLがなんかいたづら?してるんだろう?というところまでは想像したんですが、これがネットで検索してもなかなか情報がでてこない。(ってか検索の仕方が悪いという噂が…)

おそらくこういう事って、MySQL使いの方やPHPerの方々の中では常識に類するぐらい当たり前のことなんでしょうね。なのでいちいちBlogとかに書く人もいないし、質問すらする人もいない。当たり前すぎて知らない奴はアホ、なことなんだろうと思いました。

で、散々調べてようやく、どうも照合順序とかいうのが関係してるっぽいということが判明。それでもそのものズバリを説明してるところは発見できず、それでもなんとかutf8_unicode_ciのciの部分は大文字/小文字を同一視するってことだけはわかりました。

でも日本語の大文字/小文字ってなんだ???

こういう部分は本当に海外製っつーか1バイト圏のソフトなんだなぁ?と思い知らされる部分で、アルファベットのことしか考えてない、っつーかもちろんこういう機能が実装されているので何も考えてないわけではないとは思いますが、根底にある発想というかなんというか…。

日本語(の文字)の大文字/小文字といわれてピンときますか?自分は全くわからないです。「あ」と「ぁ」のことかと…。「ゃ」とか「ぃ」とか、「ι」とか…ってこれは日本語じゃね〜ヨ。

逆の立場で考えればアルファベットの楷書体/行書体とか、ローマ字の永字八法とか言われてピンとくるガイジンがいるのか?…と。

で、愚痴はこれぐらいにして、とにかくこのutf8_unicode_ciを指定していると濁点・半濁点はおろかカタカナもなんと半角カタカナも同一視してしまうということが実験で確かめられました(^^;実験…っていったい

なので例えば新市町村フィールドに「る」と入力して検索を実行すると…

うるま市 2005-04-01
つがる市 2005-02-11
つるぎ町 2005-03-01
南アルプス市 2003-04-01

う「る」ま市・つが「る」市・つ「る」ぎ町というトンデモなひらがな市町村に加えて、南ア「ル」プス市というさらに上をいくトンデモな市も検索されて和ませてくれます。

んで、もしこういうことならば↑の方で晒した五十音パッドの出力に関する部分のコードはほとんど必要ないんじゃないか?とおもい以下のようにしてみました。
function _getCap($field)
{
//濁点・半濁点を清音に吸収するため、半角仮名で処理した後に重複削除
foreach($this->Gdb->getCap($field)->result_array() as $row)
{
$kana[] = $row['cap']; //単純に代入してるだけ

/*以下は無駄な処理っぽかったのでコメントアウト
$cap = mb_convert_kana($row['cap'], 'h', 'UTF-8');
$cap = mb_substr($cap, 0, 1, 'UTF-8');
$kana[] = mb_convert_kana($cap, 'H', 'UTF-8');
*/
}

//$kana = array_unique($kana);

return $kana;
}

すると今度は五十音パッドがなんかおかしい!

error_50on_pad.png

「てねへめらりるれろ」は元々頭文字にない文字なのでディム化されていますが、修正前は存在していたはずの「た」「こ」が修正後ではディム化されています。これはどうしたことだろうか?とそもそものテーブル(というかビューですが)を調べてみると…

phpMyAdminで表示(SELECT * FROM ncap)
ncap.png

のように「こ」「た」の(あるべき)部分に「ご」「だ」と出力されています。そこで今度はこのビューを生成しているSQLを確認してみると…
CREATE VIEW ncap AS select distinct substr(newcities.yomi,1,1) AS cap from newcities order by newcities.yomi;

と、特に問題なさそうなのですが、実はここに照合順序が影響してきていて、まずORDER BY句であいうえお順に並び替えようとしているのですが、utf8_unicode_ciという照合順序だと「た」も「だ」も同じモノとして扱われてしまいます。つまり「た」と「だ」など同一視される文字間ではソートはされないで、DB格納順に出力されるということです。

さらにDISTINCTで重複を削除した際にDBに格納された順で重複削除が行われてしまい、たまたま「こ」と「た」で始まる市町村は「ご」や「だ」ではじまる市町村が先に格納されていたためこのような結果になったわけです。

そしてその結果を濁点・半濁点を含まない五十音(「あ〜わ」まで及び「を」「ん」を除くので正確には44文字)と比較しているので、最終的に「こ」と「た」はディム化されてしまったというわけです。

というわけで↑の五十音パッド出力のコードは、$kana配列をarray_uniqueで重複削除している部分だけが不必要で、半角カナに変換して濁点・半濁点を清音に吸収している部分は必要だったということになります。

では当初の予定?どおりに動作させるにはどうしたらよいか?というと、これはあっけないくらい簡単な話で、単に照合順序を変えてやればいいだけです。

ncap(およびocap)はビューなので、大元のnewcitiesとoldcitiesのyomi, oyomiフィールドの照合順序をutf8_binにすれば、修正前のコードの意図した通りの動作になります。(つまりビュー時点では濁点・半濁点・清音が違うものとして認識され、↑のコードではarray_uniqueでさらに重複削除しないと、例えば「た、た」とか「は、は、は」のように出力されてしまうということです)

但し照合順序をutf8_binにしてしまうと、五十音パッド出力のコードは↑の修正前でよいのですが、今度は検索する方のコードが上述したとおり非常に煩雑なものになりかねません。

というわけで結論としては

照合順序:utf8_unicode_ci
五十音パッド出力コード:array_unique部分を削除
検索コード:そのまま

ということにしました。

今回の例のようにふりがなの頭一文字のような場合はutf8_unicode_ciが便利かとおもいます。ただこの照合順序にした場合は濁点・半濁点・清音の区別をつけたい場合に不都合が生じます。その場合はutf8_binにするべきだとおもいます。

またutf8_binの場合でも今回の例のようなふりがなの頭一文字だけを取得したい(検索したい)場合は、もっと単純にふりがなの頭一文字だけのフィールドを作って、入力時点で濁点・半濁点を清音に統一してしまうという方法もあります(つまり伊達市でも「た」と予め入力しておく)。実際大元のスクリプトでは全部の読みではなく頭一文字だけのフィールドになっていました。

ということでもしかしたら意外と知られていないことだったりして?ナワケナイッスネ(--;
ラベル:MySQL
posted by ciallost at 06:38| Comment(2) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
記事が面白かったので、ちょっと調べてみました。
http://dev.mysql.com/doc/refman/5.5/en/faqs-cjk.html#qandaitem-23-11-1-11

自分なりの簡単な説明は、MySQLはUnicode Orgの4.0.0 "allkeys" 表を使っていて、さらにこの表のPrimary Weightという部分を参照しているそうです。
このPrimary Weightが清音と濁音を区別しないから見たいですよ。
かなり言い訳くさいですけど。。。
Posted by Hajime at 2010年04月17日 18:58
moncler
Posted by モンクレー モンクレール at 2013年08月05日 14:53
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック