更新日: $Date: 2006-09-29 09:21:22 $ UTC ($Revision: 1.10 $)
公開日: 2004/04/13
今のところ(mysql 4.0.27, 5.0.24a)、MySQLのネイティブ関数REGEXPは日本語の文字列を正しく処理できません (一方、LIKEやSUBSTRINGなどは日本語の処理に対応しています)。そこで日本語をきちんと扱える正規表現関数、mregexpというものをユーザー定義関数(UDF=User Defined Function)という仕組みを用いて作りました。
REGEXPは日本語などのマルチバイト文字を考慮していないため、以下のような問題があります。
あ.う
'が文字列'あいう
'にマッチしません。
● LIKEは期待通り「あいう」がマッチするが、 mysql> SELECT * FROM regexp_test WHERE s LIKE 'あ_う'; +--------+ | あいう | | あXう | +--------+ ● REGEXPでは「あいう」がマッチしない。 mysql> SELECT * FROM regexp_test WHERE s REGEXP 'あ.う'; +-------+ | あXう | +-------+ ● mregexpはLIKEと同じように期待した通りにマッチします。 mysql> SELECT * FROM regexp_test WHERE mregexp(s, 'あ.う'); +--------+ | あいう | | あXう | +--------+
い
'が文字列'イあ
'にマッチしてしまう。0xA4A4
で、
「イあ」のが0xA5A4A4A2
であるために起こる現象です。
イ あ ┌─┐ ┌─┐ A5 A4 A4 A2 └─┘ い
● LIKEだと「イあ」はマッチしないが、 mysql> SELECT * FROM regexp_test WHERE s LIKE '%い%'; +--------+ | s | +--------+ | あいう | +--------+ ● REGEXPだと「い」を含まない「イあ」がマッチしてしまう。 mysql> SELECT * FROM regexp_test WHERE s REGEXP 'い'; +--------+ | s | +--------+ | あいう | | イあ | +--------+ ● mregexpはLIKEと同じように期待した通りにマッチします。 mysql> SELECT * FROM regexp_test WHERE mregexp(s, 'い'); +--------+ | あいう | +--------+
mregexpの正規表現の処理には鬼車(Oniguruma)を使用しています。 Onigurumaは日本語の文字エンコーディングであるEUC-JP, Shift_JIS, UTF-8をはじめ、様々な文字エンコーディングに対応しているため、mregexpを使えばこのような問題は起こりません。
ライセンスは修正済BSDライセンスです。 ライセンス条件を満たす限り、商用/非商用、クローズ/オープンソースに関わらず再配布、使用することができます。 詳細なライセンスは配布物に含まれるファイルCOPYINGを参照してください。 参考までに、FreeBSD Projectのサイトに修正済BSDライセンスの日本語訳があります。
翔泳社のムック『超・極める!MySQL』のUDFの章に、mregexpの記事を書きました。
REGEXPの問題点、mregexpを作るに至った経緯、ソースコードmregexp.cの詳説などなどを書きましたので、手にとって見てもらえればと思います。
mregexp(expr,pat[,encoding])
文字列exprに対して、正規表現patのパターンマッチを実行します。 デフォルトでは大文字小文字を区別してパターンマッチを実行します。
mregexp関数の返値は、パターンにマッチした場合は1、マッチしなかった場合は0になります。
encodingでは、exprとpatの文字エンコーディングを指定します。
encodingとして指定できるものは以下の通りで、これらは大文字小文字を区別しません。つまり、'euc-jp'と'Euc-Jp'とは同値です。
また、encodingは省略可能で、そのときはコンパイル時にDEFAULT_ENCODING
で指定したものになります。
Shift_JIS
, sjis
, cp932
EUC-JP
, ujis
, eucjpms
UTF-8
, utf8
UTF-16BE
UTF-16LE
UTF-32BE
UTF-32LE
ASCII
ISO-8859-1
, latin1
ISO-8859-2
, latin2
ISO-8859-3
ISO-8859-4
ISO-8859-5
ISO-8859-6
ISO-8859-7
, greek
ISO-8859-8
, hebrew
ISO-8859-9
, latin5
ISO-8859-10
ISO-8859-11
ISO-8859-13
, latin7
ISO-8859-14
ISO-8859-15
ISO-8859-16
EUC-TW
EUC-KR
, euckr
EUC-CN
KOI8-R
, koi8r
Big5
GB18030
もう一つ、 mregexpのバージョン、mregexp関数のencodingの初期値、Onigurumaのバージョンを含む文字列を返す関数mregexp_versionがあります。
mregexp_version()
mysql> select mregexp_version(); mregexp-1.0 [EUC-JP] (oniguruma-4.4.4)
以下 に使用例を示します。 使用できる正規表現の詳細は、 Oniguruma 正規表現を参照してください。 mregexpで使用している正規表現の種類はRuby(= ONIG_SYNTAX_DEFAULT = ONIG_SYNTAX_RUBY)です。
SELECT * FROM table_name WHERE mregexp(s1, '^あ', 'ujis');
SELECT * FROM table_name WHERE mregexp(s1, '^あ.*お$');
SELECT * FROM table_name WHERE mregexp(s1, '(あいう|かきく)');
SELECT * FROM table_name WHERE mregexp(s1, '^[あか]');
SELECT * FROM table_name WHERE mregexp(s1, '\\d');
\d
ではなく\\d
である点に注意してください。
SELECT * FROM table_name WHERE mregexp(s1, 'abc');
SELECT * FROM table_name WHERE mregexp(s1, '(?-i)abc');
SELECT * FROM table_name WHERE mregexp(s1, '(?i)abc');
abcd
'(末尾にスペースが1つある)は、
パターン'd$
'にマッチしますが、'd $
'や'd\\s$
'にはマッチしません。
name | version | date | size |
---|---|---|---|
mregexp-1.0.tar.gz | 1.0 | 2006-09-29 18:25:04 | 13.6 KB |
以降ではUNIX likeな環境でのインストール方法を示します。
Windowsの場合は、VC++でのインストール方法が、MySQL ML [mysql 09205] <http://www.mysql.gr.jp/mysqlml/mysql/msg/9205> と [mysql 09206] <http://www.mysql.gr.jp/mysqlml/mysql/msg/9206> とで報告されているのでそちらを参照してください。 また、VC2005 Expressを使った場合のコンパイル方法が、 『超・極める!MySQL』に紹介されているのでそちらも参考になると思います。
Makefileの
INSTALL_LIBDIR
とDEFAULT_ENCODING
の値を適宜変更してください。
INSTALL_LIBDIR
はUDFのオブジェクトファイルを配置するディレクトリ
(以下、ここで指定したディレクトリをINSTALL_LIBDIRと表記します)
です。
DEFAULT_ENCODING
で指定したものは、
mregexp関数の実行時に、mregexpの引数で文字エンコーディングが指定されなかった場合の初期値となります。my.cnfの[mysqld]セクションのdefault-character-setと同じ文字エンコーディングにするといいでしょう。
MakefileにはShift_JIS, EUC-JP, UTF8の例がありますが、これ以外の文字エンコーディング(EUC-KRなど)をここで指定したい場合は、
OnigurumaのAPIにあるONIG_ENCODING_XXX
を指定してください。
Makefileを変更したら、コンパイルしてrootになってインストールします。
$ make # make install
環境変数LD_LIBRARY_PATHにINSTALL_LIBDIRを指定してmysqldを起動し直します。
MySQL AB提供のrpmパッケージを使っている場合は、mysqldの起動スクリプト/etc/init.d/mysql
を以下のように変更してから/etc/init.d/mysql restart
と実行して起動し直せばいいです。
--- /etc/init.d/mysql 2004/04/12 08:40:57 1.1 +++ /etc/init.d/mysql 2004/04/12 08:47:27 @@ -151,6 +151,7 @@ then # Give extra arguments to mysqld with the my.cnf file. This script may # be overwritten at next upgrade. + env LD_LIBRARY_PATH=/usr/local/lib/mysql \ $bindir/mysqld_safe --datadir=$datadir --pid-file=$pid_file >/dev/null 2>&1 & # Make lock for RedHat / SuSE if test -w /var/lock/subsys
mysqldが起動したら、mysqlのユーザーrootで接続し、以下のSQL文を発行します。
CREATE FUNCTION mregexp RETURNS INTEGER SONAME 'mregexp.so'; CREATE FUNCTION mregexp_version RETURNS STRING SONAME 'mregexp.so';
以上でmregexp関数が使えるようになったと思います。
必須ではありませんが、次のようにして動作確認を行うことができます。
default-character-set = ujis
の場合
make test_ujis
default-character-set = sjis
の場合
make test_sjis
make test41
動作確認のために、
デフォルトではmysql -s test
でデータベースtestに接続し、
DROP TABLE, CREATE TABLE, INSERT, SELECTを発行します。
接続ユーザーなどを変更したい場合は、make実行時にT_DOSQLの値を指定することで制御できます。
例えば、ユーザーscott
、パスワードtiger
、データベースemp
に接続して動作確認する場合は、
make T_DOSQL='mysql -s -uscott -ptiger emp' test_ujis
と実行します。
組み込みのREGEXPやLIKEと比べ、mregexpがどの程度の性能となるかを調べてみます。
一般的に、性能評価は非常に難しく、ハードウエアやmysqldのパラメータなどに大きく左右されます。ここでは絶対的な評価ではなく、ある環境の中でのmregexpの傾向を見ることを主眼とします。
テーブルは以下のような単純なものです。
CREATE TABLE `t_perm` ( `c1` varchar(12) default NULL, KEY `idx_c1` (`c1`) ) TYPE=InnoDB
データは177,409行格納されています。 参考までに、このデータは青空文庫で公開されている芥川龍之介の 『文芸的な、余りに文芸的な』 『偸盗』 『羅生門』 『路上』 『素戔嗚尊』 をMeCab (和布蕪)で字句解析した結果の文字列です。
テストしたクエリは以下の通りです。
select * from t_perm where c1 like 'タ%';
select * from t_perm where c1 regexp '^タ';
select * from t_perm where mregexp(c1,'^タ');
select * from t_perm where mregexp(c1,'^タ.テ');
select * from t_perm where c1 like 'タ%' and mregexp(c1,'^タ.テ');
クエリが完了するまでに要した時間は以下の通りでした。 1回目はmysqldが起動して初めて発行した場合で、2回目は1回目に続けて同じクエリを発行した場合です。
クエリ番号 | 1回目[sec] | 2回目[sec] |
---|---|---|
1 | 0.09 | 0.06 |
2 | 0.84 | 0.06 |
3 | 1.09 | 0.86 |
4 | 1.07 | 1.01 |
5 | 0.06 | 0.06 |
前出のクエリに対してEXPLAINを実行した結果のうち、type、possible_keys、rowsのみを抜き出したものは以下の通りになりました。
クエリ番号 | type | possible_keys | rows |
---|---|---|---|
1 | range | idx_c1 | 10459 |
2 | index | NULL | 176937 |
3 | index | NULL | 176937 |
4 | index | NULL | 176937 |
5 | range | idx_c1 | 10450 |
まず、クエリ3のEXPLAINの結果を見ると、mregexpでは前方一致の正規表現であってもインデックスが使われていないことがわかります。
ではREGEXPはどうかというと、クエリ2のEXPLAINの結果の通り、たとえ組み込み関数で前方一致の正規表現であってもインデックスは使用されません。
しかし、クエリ2と3で1回目と2回目の所要時間を見ると、クエリ2では2回目の所要時間が大幅に短くなっているのがわかります。これはクエリのキャッシュが効いているためで、試しにmysqldのquery_cache_sizeを極端に小さくして試してみると、2回目でも1回目と同程度の所要時間になったことからも裏付けられます。
一方、クエリ3では、2回目は多少所要時間が短くなっていますが、クエリ2ほど大幅なものではありません。推測するに、UDFを使ったクエリの結果はキャッシュされないのではないでしょうか。
この推測が正しいと、UDFであるmregexpは前方一致であってもインデックスが使われないし結果のキャッシュもされないことになり、性能が非常に悪いということになってしまいます。
そこでmregexpで処理するレコード数を少なくすることによって、所要時間にどのような変化があらわれるか見てみました。これがクエリ4と5です。
クエリ5はLIKEとmregexpを使っているのでクエリ4に比べてコストが高いように見えますが、所要時間を比べてみると圧倒的に二度手間のクエリ5の方が速いという結果になりました。
これは、まず最初にLIKEが適用され、クエリ4に比べてmregexpが処理するレコードが大幅に少なくなったためだと思います。また、この場合でのLIKEではインデックスが適用されるのでLIKEの処理は短い時間で完了します。
以上より、mregexpはLIKEやREGEXPに比べると性能面では劣りますが、LIKEとmregexpを組み合わせて使うことで高い性能が得られることがわかりました。
正規表現を使いたい全ての局面でLIKEと組み合わせられるわけではありませんが、クエリのチューニングの際にはこのようなケースも考慮すべきだと思います。