2018年4月12日木曜日

「危険な正規表現」 vs MySQL 8.0

他にもいくつかあると思うけれど、俺の一番のお気に入り(?)はこれ。
(.*)*^
試してみよう。
mysql80 15> SELECT * FROM t1 LIMIT 3;
+-------------+---------------------+------------------------------------------------------------+--------------------------------------------------------------------+
| tweet_id    | timestamp           | source                                                     | text                                                               |
+-------------+---------------------+------------------------------------------------------------+--------------------------------------------------------------------+
| 22431873995 | 2010-08-29 09:00:00 | <a href="http://twicca.r246.jp/" rel="nofollow">twicca</a> | 明日一年ぶり夜勤。                                                 |
| 22575355920 | 2010-08-31 09:00:00 | <a href="http://twicca.r246.jp/" rel="nofollow">twicca</a> | 夜勤明けの電車、なんでこんなに人い るんだ。。                       |
| 22628108281 | 2010-08-31 09:00:00 | <a href="http://twicca.r246.jp/" rel="nofollow">twicca</a> | androidにunix likeな機能求める方が 。。か?                         |
+-------------+---------------------+------------------------------------------------------------+--------------------------------------------------------------------+
3 rows in set (0.00 sec)

mysql80 15> SELECT COUNT(*) FROM t1;
+----------+
| COUNT(*) |
+----------+
|    41123 |
+----------+
1 row in set (0.02 sec)
俺のTweet履歴を食わせたこんなテーブルに対して
mysql80 17> SELECT COUNT(*) FROM t1 WHERE text RLIKE '(.*)*^';
ERROR 3700 (HY000): Timeout exceeded in regular expression match.
サクッとタイムアウトが起こった。3700番なので新しいヤーツ。
このタイムアウトは1回の正規表現の評価が regexp_time_limit で設定された値を超えると出るヤーツ。単位は およそ ミリ秒。デフォルト32ミリ秒。
mysql80 17> SHOW VARIABLES LIKE '%reg%';
+--------------------+---------+
| Variable_name      | Value   |
+--------------------+---------+
| regexp_stack_limit | 8000000 |
| regexp_time_limit  | 32      |
+--------------------+---------+
2 rows in set (0.01 sec)
なんで およそ なんて珍しい枕詞がついてるかというと、オリジナルのICUのドキュメントにもそう書いてあるから。
mysql80 17> WITH t AS (SELECT REPEAT('a', 16) AS a) SELECT * FROM t WHERE a RLIKE '(.*)*^';
+------------------+
| a                |
+------------------+
| aaaaaaaaaaaaaaaa |
+------------------+
1 row in set (0.03 sec)

mysql80 17> WITH t AS (SELECT REPEAT('a', 17) AS a) SELECT * FROM t WHERE a RLIKE '(.*)*^';
ERROR 3700 (HY000): Timeout exceeded in regular expression match.
16文字で0.03sec = 30ms前後ってことはこれCPUぶん回したらタイムアウトするようになるかな? と思ってぶん回してみたけど、フツーに60ms前後かかってもタイムアウトする様子はない。何故だろ。
mysql80 18> WITH t AS (SELECT REPEAT('a', 16) AS a) SELECT * FROM t WHERE a RLIKE '(.*)*^';
+------------------+
| a                |
+------------------+
| aaaaaaaaaaaaaaaa |
+------------------+
1 row in set (0.06 sec)
regexp_time_limit=0 にするとタイムアウトが無効になるので、この正規表現で線形に時間を食われていく様がまざまざと観測できる。1行でこれなので、複数行これを評価させれば更に倍々ゲームで逝くことになるので、0にすることはないだろうなぁ…。
ちなみにセッション値を持たない、グローバル値のみなので SET GLOBAL でやらないといけない。
mysql80 18> SET GLOBAL regexp_time_limit= 0;
Query OK, 0 rows affected (0.00 sec)

mysql80 18> WITH t AS (SELECT REPEAT('a', 17) AS a) SELECT * FROM t WHERE a RLIKE '(.*)*^';
+-------------------+
| a                 |
+-------------------+
| aaaaaaaaaaaaaaaaa |
+-------------------+
1 row in set (0.02 sec)

mysql80 18> WITH t AS (SELECT REPEAT('a', 18) AS a) SELECT * FROM t WHERE a RLIKE '(.*)*^';
+--------------------+
| a                  |
+--------------------+
| aaaaaaaaaaaaaaaaaa |
+--------------------+
1 row in set (0.04 sec)

mysql80 18> WITH t AS (SELECT REPEAT('a', 19) AS a) SELECT * FROM t WHERE a RLIKE '(.*)*^';
+---------------------+
| a                   |
+---------------------+
| aaaaaaaaaaaaaaaaaaa |
+---------------------+
1 row in set (0.08 sec)

..

mysql80 18> WITH t AS (SELECT REPEAT('a', 26) AS a) SELECT * FROM t WHERE a RLIKE '(.*)*^';
+----------------------------+
| a                          |
+----------------------------+
| aaaaaaaaaaaaaaaaaaaaaaaaaa |
+----------------------------+
1 row in set (11.31 sec)
あとちなみに、 regexp_time_limit なんて設定できなかったMySQL 5.7とそれ以前でこれをやるとどうなるかというと
mysql57 8> SELECT * FROM (SELECT REPEAT('a', 26) AS a) AS t WHERE a RLIKE  '(.*)*^';
+----------------------------+
| a                          |
+----------------------------+
| aaaaaaaaaaaaaaaaaaaaaaaaaa |
+----------------------------+
1 row in set (0.00 sec)

mysql57 8> SELECT * FROM (SELECT REPEAT('a', 32) AS a) AS t WHERE a RLIKE  '(.*)*^';
+----------------------------------+
| a                                |
+----------------------------------+
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
+----------------------------------+
1 row in set (0.00 sec)
一瞬で帰ってくる。これはMySQL 5.7とそれ以前の正規表現は繰り返しマッチをしないからだと思う。

まま、デフォルトの regexp_time_limit はそれなりに良い値なのではないかと思う。

0 件のコメント :

コメントを投稿