主にACL用のSQLにINDEX追加

主にACL関連のSQLでINDEX追加できる物が無いかを調査してみました。

参考
MySQLとオープンソースに捧げる毎日:MySQLチューニング - livedoor Blog(ブログ)
MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.8.2 EXPLAIN 構文
MySQL :: MySQL 5.6 リファレンスマニュアル :: 8.2.1.15 ORDER BY の最適化

結果、追加したINDEX

  • CREATE INDEX model_foriegnkey ON aros (model, foreign_key);
  • CREATE INDEX alias ON acos (alias);
  • CREATE INDEX lft_rght ON acos (lft, rght);
  • CREATE INDEX aro_aco ON aros_acos (aro_id, aco_id);


以下、検証作業ログ。

準備。

mysql> ANALYZE TABLE aros, acos, aros_acos;
+----------------------+---------+----------+----------+
| Table                | Op      | Msg_type | Msg_text |
+----------------------+---------+----------+----------+
| cake_frame.aros      | analyze | status   | OK       |
| cake_frame.acos      | analyze | status   | OK       |
| cake_frame.aros_acos | analyze | status   | OK       |
+----------------------+---------+----------+----------+
3 rows in set (0.00 sec)

Super Administratorのホームを表示して、デバッグ表示に出力されたSQLをEXPLAINで調査。
本当はスロークエリー調査してINDEX作るのですが、とりあえずsessionとACL関連は間違いなく頻繁に呼ばれるSQLなので。


cake_sessions。問題なし。
というかプライマリキーにセッションID入れてるんですね。この発想はなかった。

mysql> EXPLAIN (SELECT `cake_sessions`.`data` FROM `cake_sessions` WHERE `cake_sessions`.`id` = 'bk8sgg15thfvr20fpfa7arsam4')\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: cake_sessions
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 767
          ref: const
         rows: 1
        Extra:
1 row in set (0.00 sec)

これは先の記事でも取り上げた、INDEXの効かないSQLなので保留。

SELECT `Aro`.`id`, `Aro`.`parent_id`, `Aro`.`model`, `Aro`.`foreign_key`, `Aro`.`alias` FROM `aros` AS `Aro` LEFT JOIN `aros` AS `Aro0` ON (`Aro`.`lft` <= `Aro0`.`lft` AND `Aro`.`rght` >= `Aro0`.`rght`) WHERE `Aro0`.`model` = 'User' AND `Aro0`.`foreign_key` = 1 ORDER BY `Aro`.`lft` DESC;

controllers/Usersのアクセス制限関連。

SELECT `Aco`.`id`, `Aco`.`parent_id`, `Aco`.`model`, `Aco`.`foreign_key`, `Aco`.`alias` FROM `acos` AS `Aco` LEFT JOIN `acos` AS `Aco0` ON (`Aco0`.`alias` = 'controllers') LEFT JOIN `acos` AS `Aco1` ON (`Aco1`.`lft` > `Aco0`.`lft` AND `Aco1`.`rght` < `Aco0`.`rght` AND `Aco1`.`alias` = 'Users') WHERE ( (`Aco`.`lft` <= `Aco0`.`lft` AND `Aco`.`rght` >= `Aco0`.`rght`) OR (`Aco`.`lft` <= `Aco1`.`lft` AND `Aco`.`rght` >= `Aco1`.`rght`) ) ORDER BY `Aco`.`lft` DESC;

Using temporary; Using filesortの高負荷コンボですが、こちらはINDEXの余地がありそうです。

mysql> EXPLAIN(SELECT `Aco`.`id`, `Aco`.`parent_id`, `Aco`.`model`, `Aco`.`foreign_key`, `Aco`.`alias` FROM `acos` AS `Aco` LEFT JOIN `acos` AS `Aco0` ON (`Aco0`.`alias` = 'controllers') LEFT JOIN `acos` AS `Aco1` ON (`Aco1`.`lft` > `Aco0`.`lft` AND `Aco1`.`rght` < `Aco0`.`rght` AND `Aco1`.`alias` = 'Users') WHERE ((`Aco`.`lft` <= `Aco0`.`lft` AND `Aco`.`rght` >= `Aco0`.`rght`) OR (`Aco`.`lft` <= `Aco1`.`lft` AND `Aco`.`rght` >= `Aco1`.`rght`)) ORDER BY `Aco`.`lft` DESC)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aco
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 7
        Extra: Using temporary; Using filesort
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aco0
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 7
        Extra:
*************************** 3. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aco1
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 7
        Extra: Using where
3 rows in set (0.01 sec)

まず、acosのaliasにINDEX追加。

mysql> CREATE INDEX alias ON acos (alias);
Query OK, 7 rows affected (0.00 sec)
Records: 7  Duplicates: 0  Warnings: 0

mysql> EXPLAIN(SELECT `Aco`.`id`, `Aco`.`parent_id`, `Aco`.`model`, `Aco`.`foreign_key`, `Aco`.`alias` FROM `acos` AS `Aco` LEFT JOIN `acos` AS `Aco0` ON (`Aco0`.`alias` = 'controllers') LEFT JOIN `acos` AS `Aco1` ON (`Aco1`.`lft` > `Aco0`.`lft` AND `Aco1`.`rght` < `Aco0`.`rght` AND `Aco1`.`alias` = 'Users') WHERE ((`Aco`.`lft` <= `Aco0`.`lft` AND `Aco`.`rght` >= `Aco0`.`rght`) OR (`Aco`.`lft` <= `Aco1`.`lft` AND `Aco`.`rght` >= `Aco1`.`rght`)) ORDER BY `Aco`.`lft` DESC)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aco
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 7
        Extra: Using filesort
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aco0
         type: ref
possible_keys: alias
          key: alias
      key_len: 768
          ref: const
         rows: 1
        Extra:
*************************** 3. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aco1
         type: ref
possible_keys: alias
          key: alias
      key_len: 768
          ref: const
         rows: 1
        Extra: Using where
3 rows in set (0.00 sec)

しかる後にlft, rghtにINDEX作成で、Using filesortも解消!

mysql> CREATE INDEX lft_rght ON acos (lft, rght);
Query OK, 7 rows affected (0.00 sec)
Records: 7  Duplicates: 0  Warnings: 0

mysql> EXPLAIN(SELECT `Aco`.`id`, `Aco`.`parent_id`, `Aco`.`model`, `Aco`.`foreign_key`, `Aco`.`alias` FROM `acos` AS `Aco` LEFT JOIN `acos` AS `Aco0` ON (`Aco0`.`alias` = 'controllers') LEFT JOIN `acos` AS `Aco1` ON (`Aco1`.`lft` > `Aco0`.`lft` AND `Aco1`.`rght` < `Aco0`.`rght` AND `Aco1`.`alias` = 'Users') WHERE ((`Aco`.`lft` <= `Aco0`.`lft` AND `Aco`.`rght` >= `Aco0`.`rght`) OR (`Aco`.`lft` <= `Aco1`.`lft` AND `Aco`.`rght` >= `Aco1`.`rght`)) ORDER BY `Aco`.`lft` DESC)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aco
         type: index
possible_keys: lft_rght
          key: lft_rght
      key_len: 10
          ref: NULL
         rows: 7
        Extra:
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aco0
         type: ref
possible_keys: alias
          key: alias
      key_len: 768
          ref: const
         rows: 1
        Extra:
*************************** 3. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aco1
         type: ref
possible_keys: alias,lft_rght
          key: alias
      key_len: 768
          ref: const
         rows: 1
        Extra: Using where
3 rows in set (0.00 sec)


次。

mysql> SELECT `Permission`.`id`, `Permission`.`aro_id`, `Permission`.`aco_id`, `Permission`.`_create`, `Permission`.`_read`, `Permission`.`_update`, `Permission`.`_delete`, `Aro`.`id`, `Aro`.`parent_id`, `Aro`.`model`, `Aro`.`foreign_key`, `Aro`.`alias`, `Aro`.`lft`, `Aro`.`rght`, `Aco`.`id`, `Aco`.`parent_id`, `Aco`.`model`, `Aco`.`foreign_key`, `Aco`.`alias`, `Aco`.`lft`, `Aco`.`rght` FROM `aros_acos` AS `Permission` LEFT JOIN `aros` AS `Aro` ON (`Permission`.`aro_id` = `Aro`.`id`) LEFT JOIN `acos` AS `Aco` ON (`Permission`.`aco_id` = `Aco`.`id`) WHERE `Permission`.`aro_id` = 9 AND `Permission`.`aco_id` IN (2, 1) ORDER BY `Aco`.`lft` desc;
Empty set (0.00 sec)

お約束のようにUsing where; Using temporary; Using filesort
こちらもWHERE とORDER BYが異なるパターン。

mysql> EXPLAIN(SELECT `Permission`.`id`, `Permission`.`aro_id`, `Permission`.`aco_id`, `Permission`.`_create`, `Permission`.`_read`, `Permission`.`_update`, `Permission`.`_delete`, `Aro`.`id`, `Aro`.`parent_id`, `Aro`.`model`, `Aro`.`foreign_key`, `Aro`.`alias`, `Aro`.`lft`, `Aro`.`rght`, `Aco`.`id`, `Aco`.`parent_id`, `Aco`.`model`, `Aco`.`foreign_key`, `Aco`.`alias`, `Aco`.`lft`, `Aco`.`rght` FROM `aros_acos` AS `Permission` LEFT JOIN `aros` AS `Aro` ON (`Permission`.`aro_id` = `Aro`.`id`) LEFT JOIN `acos` AS `Aco` ON (`Permission`.`aco_id` = `Aco`.`id`) WHERE `Permission`.`aro_id` = 9 AND `Permission`.`aco_id` IN (2, 1) ORDER BY `Aco`.`lft` desc)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: Permission
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20
        Extra: Using where; Using temporary; Using filesort
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aro
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 1
        Extra:
*************************** 3. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aco
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: cake_frame.Permission.aco_id
         rows: 1
        Extra:
3 rows in set (0.00 sec)

INDEXを貼りましたが、完全解決にはなっていません・・・

mysql> CREATE INDEX aro_aco ON aros_acos (aro_id, aco_id);
Query OK, 20 rows affected (0.00 sec)
Records: 20  Duplicates: 0  Warnings: 0

mysql> EXPLAIN(SELECT `Permission`.`id`, `Permission`.`aro_id`, `Permission`.`aco_id`, `Permission`.`_create`, `Permission`.`_read`, `Permission`.`_update`, `Permission`.`_delete`, `Aro`.`id`, `Aro`.`parent_id`, `Aro`.`model`, `Aro`.`foreign_key`, `Aro`.`alias`, `Aro`.`lft`, `Aro`.`rght`, `Aco`.`id`, `Aco`.`parent_id`, `Aco`.`model`, `Aco`.`foreign_key`, `Aco`.`alias`, `Aco`.`lft`, `Aco`.`rght` FROM `aros_acos` AS `Permission` LEFT JOIN `aros` AS `Aro` ON (`Permission`.`aro_id` = `Aro`.`id`) LEFT JOIN `acos` AS `Aco` ON (`Permission`.`aco_id` = `Aco`.`id`) WHERE `Permission`.`aro_id` = 9 AND `Permission`.`aco_id` IN (2, 1) ORDER BY `Aco`.`lft` desc)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: Permission
         type: range
possible_keys: aro_aco
          key: aro_aco
      key_len: 8
          ref: NULL
         rows: 2
        Extra: Using where; Using temporary; Using filesort
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aro
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 1
        Extra:
*************************** 3. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aco
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: cake_frame.Permission.aco_id
         rows: 1
        Extra:
3 rows in set (0.00 sec)


変数が変わっただけで同上のSQLは飛ばして、次。
全然問題なし。

mysql> EXPLAIN(SELECT COUNT(*) AS `count` FROM `users` AS `User` WHERE `User`.`id` = 1 )\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: User
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 1
        Extra: Using index
1 row in set (0.00 sec)


先の記事の検証で追加したINDEXが別の箇所で効いていた件。

mysql> SELECT `Aro`.`id` FROM `aros` AS `Aro` WHERE `model` = 'User' AND `foreign_key` = 1 LIMIT 1;
+----+
| id |
+----+
|  9 |
+----+
1 row in set (0.00 sec)

mysql> EXPLAIN(SELECT `Aro`.`id` FROM `aros` AS `Aro` WHERE `model` = 'User' AND `foreign_key` = 1)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aro
         type: ref
possible_keys: model_foreign_key
          key: model_foreign_key
      key_len: 773
          ref: const,const
         rows: 1
        Extra: Using where; Using index
1 row in set (0.00 sec)

以下、問題ない件をさくさくと。

mysql> EXPLAIN(SELECT COUNT(*) AS `count` FROM `aros` AS `Aro` WHERE `Aro`.`id` = 9)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: Aro
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 1
        Extra: Using index
1 row in set (0.00 sec)

mysql> EXPLAIN(SELECT `User`.`id`, `User`.`group_id`, `User`.`name`, `User`.`username`, `User`.`password`, `User`.`pcmail`, `User`.`mobile_mail`, `User`.`useragent`, `User`.`host`, `User`.`created`, `User`.`modified`, `Group`.`id`, `Group`.`name`, `Group`.`parent_id` FROM `users` AS `User` LEFT JOIN `groups` AS `Group` ON (`User`.`group_id` = `Group`.`id`) WHERE `User`.`id` = 1)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: User
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 1
        Extra:
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: Group
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 1
        Extra:
2 rows in set (0.00 sec)

で、終了。
・・・・で、冒頭の追加INDEXまとめに繋がります。