Time to Read
10分
なぜ
MySQLは、WITH RECURSIVE文をサポートしていないため、たとえば以下のような自己参照テーブル “comments”
+------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | parent_id | int(11) | YES | | NULL | | | body | varchar(255) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | +------------+--------------+------+-----+---------+----------------+
において、あるレコードを起点とした、その子/孫レコードすべてを取ってくる、ということがSQL一回ではしづらい。
Ruby on Railsなら acts_as_tree 等を用いることができそうだが、何度もSQLを投げるため、よりパフォーマンスを上げたいのであれば、例えばストアドプロシージャに頼る等が考えられる。
実コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | delimiter // CREATE PROCEDURE find_root_and_children(IN root_id INT) BEGIN -- 空の一時テーブルを二つ用意 CREATE TEMPORARY TABLE comments_stack ( `id` INT(11) NULL, `parent_id` INT(11) NULL, `body` VARCHAR(255) COLLATE utf8_unicode_ci NULL, `created_at` datetime NULL, `updated_at` datetime NULL ) ENGINE = MEMORY; CREATE TEMPORARY TABLE comments_old_stack ( `id` INT(11) NULL, `parent_id` INT(11) NULL, `body` VARCHAR(255) COLLATE utf8_unicode_ci NULL, `created_at` datetime NULL, `updated_at` datetime NULL ) ENGINE = MEMORY; -- スタックに、Rootコメントを挿入 INSERT INTO comments_stack SELECT * FROM comments WHERE id = root_id; -- 新旧のDISTINCTのテーブル件数が一致する(増えなくなる)まで繰り返し WHILE (SELECT COUNT(DISTINCT id) FROM comments_stack) <> \ (SELECT COUNT(DISTINCT id) FROM comments_old_stack) DO -- 旧テーブルに前回ループの件数をコピー TRUNCATE TABLE comments_old_stack; INSERT INTO comments_old_stack SELECT * FROM comments_stack; -- 親IDが、それまでに取得した集合中に存在するレコードを挿入 INSERT INTO comments_stack SELECT * FROM comments c1 WHERE c1.parent_id IN (SELECT c2.id FROM comments_old_stack c2); END WHILE; -- 結果表示 SELECT DISTINCT * FROM comments_stack; -- 後処理 DROP TEMPORARY TABLE comments_stack; DROP TEMPORARY TABLE comments_old_stack; END // delimiter ; |
カーソルを使ってないし、DISTINCTとか冗長に見えると思うんデスが、この書き方だと、SELECT文の回数が要素ツリーの「件数」ではなく「深さ」に比例するんで、割と高速なはず。と思う。
mysql> call find_root_and_children(1); +------+-----------+------+---------------------+---------------------+ | id | parent_id | body | created_at | updated_at | +------+-----------+------+---------------------+---------------------+ | 1 | NULL | root | 2010-08-20 07:07:02 | 2010-08-20 07:07:02 | | 2 | 1 | c1 | 2010-08-20 07:07:26 | 2010-08-20 07:07:26 | | 3 | 1 | c2 | 2010-08-20 07:07:32 | 2010-08-20 07:07:32 | | 4 | 2 | c1-1 | 2010-08-20 07:07:38 | 2010-08-20 07:07:38 | | 5 | 2 | c1-2 | 2010-08-20 07:07:40 | 2010-08-20 07:07:40 | | 6 | 2 | c1-3 | 2010-08-20 07:07:43 | 2010-08-20 07:07:43 | | 7 | 3 | c2-1 | 2010-08-20 07:07:49 | 2010-08-20 07:07:49 | +------+-----------+------+---------------------+---------------------+ 7 rows in set (0.01 sec) Query OK, 0 rows affected (0.01 sec) mysql> call find_root_and_children(2); +------+-----------+------+---------------------+---------------------+ | id | parent_id | body | created_at | updated_at | +------+-----------+------+---------------------+---------------------+ | 2 | 1 | c1 | 2010-08-20 07:07:26 | 2010-08-20 07:07:26 | | 4 | 2 | c1-1 | 2010-08-20 07:07:38 | 2010-08-20 07:07:38 | | 5 | 2 | c1-2 | 2010-08-20 07:07:40 | 2010-08-20 07:07:40 | | 6 | 2 | c1-3 | 2010-08-20 07:07:43 | 2010-08-20 07:07:43 | +------+-----------+------+---------------------+---------------------+ 4 rows in set (0.00 sec) Query OK, 0 rows affected (0.00 sec)
たとえば、Railsと統合するなら
上記のストプロを定義/DROPするSQLを直に投げる感じのMigrationを作成、db:migrateしとく。
Commentモデルに以下のようなクラスメソッドを追加。
1 2 3 4 5 6 7 8 9 | class Comment < ActiveRecord::Base class << self def find_root_with_children_by_id(rid) ret = find_by_sql(["call find_root_and_children(?)", rid]) connection.reconnect! ret end end end |
reconnect! してるのは、バグっつーか、そもそもストプロ使うな!って話かもしれんけどね:
It looks like CALLing a stored procedure will drop a ActiveRecord
connection. Maybe a “reconnect! if !active?” can be a quick patch…
ということなので、クイックに追加。でも、最新の2.3系ではパッチが当てられてるの? 未検証(gem経由で入れた2.3.8では全然ダメです)。。。
所感
- ストプロは、まあ、何度もSQL投げるより全然速いよね。
- ストプロって読み辛いしテストもろくに書けないので、追加するなら慎重に管理したほうがいいと思いますよ。




