読者です 読者をやめる 読者になる 読者になる

Rails の awesome_nested_set がひどい


ということでやっぱり気になって色々見てしまう自分です。

RailsActiveRecord でツリー構造を表現するのには awesome_nested_set というプラグインがいいらしいというので使ってみました。

awesome_nested_set を使って acts_as_nested_set を指定したモデルにデータを登録すると、次のようなクエリが実行されるようです。

BEGIN;
SELECT `sections`.* FROM `sections` ORDER BY `rgt` desc LIMIT 1 FOR UPDATE;
INSERT INTO `sections` (`created_at`, `lft`, `name`, `parent_id`, `rgt`, `updated_at`)
 VALUES ('2012-06-08 15:26:40', 25, 'hoge', 10, 26, '2012-06-08 15:26:40');
SELECT `sections`.* FROM `sections` WHERE `sections`.`id` = 10 ORDER BY `lft` LIMIT 1;
SELECT `lft`, `rgt`, `parent_id` FROM `sections` WHERE `sections`.`id` = 22 LIMIT 1 FOR UPDATE;
UPDATE `sections` SET
 `lft` = CASE WHEN `lft` BETWEEN 24 AND 24 THEN `lft` + 26 - 24 WHEN `lft` BETWEEN 25 AND 26 THEN `lft` + 24 - 25 ELSE `lft` END,
 `rgt` = CASE WHEN `rgt` BETWEEN 24 AND 24 THEN `rgt` + 26 - 24 WHEN `rgt` BETWEEN 25 AND 26 THEN `rgt` + 24 - 25 ELSE `rgt` END,
 `parent_id` = CASE WHEN id = 22 THEN 10 ELSE `parent_id` END
 ORDER BY `lft`;
SELECT `lft`, `rgt`, `parent_id` FROM `sections` WHERE `sections`.`id` = 10 LIMIT 1 FOR UPDATE;
SELECT `sections`.* FROM `sections` WHERE `sections`.`parent_id` = 22 ORDER BY lft;
SELECT `lft`, `rgt`, `parent_id` FROM `sections` WHERE `sections`.`id` = 22 LIMIT 1 FOR UPDATE;
COMMIT;

ここで UPDATE 文に注目です! なんと WHERE がありません!! テーブルの全レコードを更新しちゃってます!!

大量のレコードが存在するテーブルで、頻繁にレコードの登録/削除があったりすると、その度に全レコードを更新しようとするので非常に効率が悪いです。

あと BEGIN の直後の SELECT ですが、条件なしに rgt でソートして FOR UPDATE してます。rgt にはインデックスが張ってないので、ここでもテーブルの全レコードをロックしてしまいます。

そこで rgt にインデックスを張ってみます。すると今度は、複数のクライアントで同時にデータを登録するとエラーになってしまいました。

ActiveRecord::StatementInvalid: Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction: SELECT  `sections`.* FROM `sections`  ORDER BY `rgt` desc LIMIT 1 FOR UPDATE

インデックスを張る前は全レコードをロックしていたのですが、インデックスを張ったために特定のレコード以外はロックされず、同時に更新しようとしてデッドロックエラーが発生したのです。

デッドロックエラーが発生した時はトランザクションをやり直さないといけないのですが、Rails はこの場合も特に何もしてくれないのでアプリがエラーで落ちてしまいます。

ということで、まとめ。

  • awesome_nested_set のカラム rgt (lft も?) にはインデックスを設定してはいけません。
  • awesome_nested_set は大量レコードを保持して、頻繁に更新するようなテーブルでは使えません。