【SQL】 自己相関サブクエリ でパフォーマンスが悪かった話とその解決策

■ はじめに

自己相関サブクエリ でパフォーマンスが悪かったので
その際の解決策などを記録する

目次

【1】現象
【2】動作環境
【3】パフォーマンスが悪かったSQL文
【4】原因
【5】解決案
 1)インデックス追加
 2)SQL文変更

【1】現象

* 50万レコードほどのテーブルで相関サブクエリを使った結果、
 数十分掛かってしまった

【2】動作環境

 * OS : CentOS7
 * DB : MySQL

【3】パフォーマンスが悪かったSQL

イメージ

SELECT
 (中略)
 main_ol.remarks AS remarks
FROM
 user AS u
 (中略)
INNER JOIN
  operation AS o
ON
  (中略)
LEFT OUTER JOIN
 operation_log AS main_ol
ON
 u.id = main_ol.user_id
 AND o.operation_type = main_ol.operation_type
 AND main_ol.operation_level IN ('1', '2') -- '1':ERROR, '2':WARN
 AND main_ol.operation_datetime =
  -- ★最新のoperation_datetimeを取得
  (SELECT
     MAX(sub_ol.operation_datetime)
   FROM
     operation_log AS sub_ol
   WHERE
      sub_ol.user_id = main_ol.user_id
      AND sub_ol.operation_type = main_ol.operation_type
      AND sub_ol.operation_level IN ('1', '2') -- '1':ERROR, '2':WARN
      AND sub_ol.operation_datetime < CURRENT_TIMESTAMP
   GROUP BY
     sub_ol.user_id,
     sub_ol.operation_type

【4】原因

 * 最新のoperation_datetimeを取得するための自己相関サブクエリ(★)が
   毎行動作してしまい、使いまわしができていない

【5】解決案

 * 「1)インデックス追加」および「2)SQL文変更」で解決できた

1)インデックス追加

 * インデックス追加については、条件に関わる項目に対して行う
 * 複数の場合、複合インデックスで追加する
  => 上記のSQL文だと「user_id」「operation_type」「operation_level」「operation_datetime」
 * どの位、どの順番で行うかは、実際のデータを使い、検証して決める

2)SQL文変更

* 処理が使い回しできるようなSQL文に工夫

イメージ

SELECT
 (中略)
 main_ol.remarks AS remarks
FROM
 user AS u
 (中略)
INNER JOIN
  operation AS o
ON
  (中略)
LEFT OUTER JOIN
 -- (iii) operation IDに紐づく remarks を取得
 (SELECT
   sub_ol.user_id AS user_id,
   sub_ol.operation_type AS operation_type,
   sub_ol.remarks AS remarks
  FROM
   operation_log AS sub_ol
  INNER JOIN
    -- (ii) user_id / operation_type / operation_datetime 単位(※1)で、最大operation IDを取得
    (SELECT
       latest_ol.user_id AS user_id,
       latest_ol.operation_type AS operation_type,
       MAX(latest_ol.id) AS latest_id
     FROM
       operation_log AS latest_ol
     INNER JOIN
       -- (i) user_id / operation_type 単位(※1)で、最新operation_datetimeを取得
       (SELECT
          latest_each_usr_ol.user_id AS user_id,
          latest_each_usr_ol.operation_type AS operation_type,
          MAX(latest_each_usr_ol.operation_datetime) AS latest_operation_datetime
        FROM
          operation_log AS latest_each_usr_ol
        WHERE
          latest_each_usr_ol.operation_datetime < CURRENT_TIMESTAMP
          AND latest_each_usr_ol.operation_level IN ('1', '2') -- '1':ERROR, '2':WARN
        GROUP BY
          latest_each_usr_ol.user_id,
          latest_each_usr_ol.operation_type
        ) AS leu_ol
     ON
       latest_ol.user_id = leu_ol.user_id,
       latest_ol.operation_type = leu_ol.operation_type,
       latest_ol.operation_datetime = leu_ol.operation_datetime
     WHERE
       latest_ol.operation_level IN ('1', '2') -- '1':ERROR, '2':WARN
     GROUP BY
       latest_ol.user_id,
       latest_ol.operation_type
       latest_ol.operation_datetime
    ) AS l_ol
    ON
      sub_ol.id = l_ol.id
 ) AS main_ol
 ON
   u.id = main_ol.user_id
   AND o.operation_type = main_ol.operation_type

補足 ※1:XXXX単位について

 * GROUP BY句により、実現

補足 ※2:修正したSQL文について

* 以下のようなイメージで間引いていく

【間引きのイメージ】

+--(i) user_id / operation_type 単位で、最新operation_datetimeを取得 ---------------------------+
|                                                                                               |
|  +--(ii) user_id / operation_type / operation_datetime 単位で、最大operation IDを取得 --+     |
|  |                                                                                      |     |
|  | +--(iii) operation IDに紐づく remarks を取得 --------------------------------+       |     |
|  | |                                                                            |       |     |
|  | |                                                                            |       |     |
|  | |                                                                            |       |     |
|  | +----------------------------------------------------------------------------+       |     |
|  |                                                                                      |     |
|  +--------------------------------------------------------------------------------------+     |
|                                                                                               |
+-----------------------------------------------------------------------------------------------+