condition fanout filterで爆死

これは MySQL Casual Advent Calendar 2017 の Day13 の記事です。

5.6から5.7にバージョンアップした際に実行計画絡みでトラブった事を書きます。 タイトルにあるcondition fanout filterとはMySQL5.7から追加されたOptimizer Switchで、条件句での絞り込みを実行計画生成の考慮に入れるものです。

資料

https://downloads.mysql.com/presentations/innovation-day-2016/Session_8_Performance_Improvements_in_MySQL_Optimizer.pdf

MySQL :: [MySQL 5.7入門セミナー講演資料] チューニング基礎編、SQLチューニング編 (※要Oracleアカウント登録(無料))

どれくらい絞りこめると判断したかどうかはExplainのfilterd列に表示されています(数字が少ないほど絞り込めている(と判断した))。

f:id:vidaisuki:20171213220141p:plain:w300

下記のテーブル構成(FK以外のINDEXは無し)を元に説明します。

f:id:vidaisuki:20171213160258p:plain

-- 約8,000行(一番少ない)
CREATE TABLE `company` (
  `company_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `company_name` varchar(128) CHARACTER SET utf8 NOT NULL,
  PRIMARY KEY (`company_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8056 DEFAULT CHARSET=utf8mb4

-- 約30,000行
CREATE TABLE `customer` (
  `customer_id` bigint(20) NOT NULL AUTO_INCREMENT ,
  `company_id` bigint(20) NOT NULL ,
  `customer_name` varchar(256) CHARACTER SET utf8 NOT NULL,
  `flg` char(1) CHARACTER SET utf8 NOT NULL ,
  `start_date` date NOT NULL ,
  `end_date` date NOT NULL ,
  PRIMARY KEY (`customer_id`),
  KEY `fk_company_id` (`company_id`),
  CONSTRAINT `fk_company_id` FOREIGN KEY (`company_id`) REFERENCES `company` (`company_id`)
) ENGINE=InnoDB AUTO_INCREMENT=31219 DEFAULT CHARSET=utf8mb4

--約60,000行
CREATE TABLE `customer_setting` (
  `customer_setting_id` bigint(20) NOT NULL AUTO_INCREMENT ,
  `customer_id` bigint(20) NOT NULL COMMENT ,
  `setting_code` bigint(20) DEFAULT NULL ,
  `value` bigint(20) NOT NULL ,
  `flg` char(1) CHARACTER SET utf8 NOT NULL ,
  PRIMARY KEY (`customer_setting_id`),
  KEY `fk_customer_id` (`customer_id`),
  CONSTRAINT `fk_customer_id` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`customer_id`)
) ENGINE=InnoDB AUTO_INCREMENT=62512 DEFAULT CHARSET=utf8mb4

下記のSQLを実行します。

select  
    *
from company 
    inner join customer  on customer.company_id = company.company_id 
    inner join customer_setting  on customer_setting.customer_id = customer.customer_id  
where customer_setting.flg = 'n'    
    and customer.flg = 'n'     
    and customer.start_date <= '2017-01-01'     
    and customer.end_date >= '2017-04-01'

5.6では下記のような実行計画で動いていました。一番レコード数が少ないcompany表を駆動表として各表をinner joinしています。

+----+-------------+------------------+------+-----------------------+----------------+---------+----------------------------+------+-------------+
| id | select_type | table            | type | possible_keys         | key            | key_len | ref                        | rows | Extra       |
+----+-------------+------------------+------+-----------------------+----------------+---------+----------------------------+------+-------------+
|  1 | SIMPLE      | company          | ALL  | PRIMARY               | NULL           | NULL    | NULL                       | 7948 | NULL        |
|  1 | SIMPLE      | customer         | ref  | PRIMARY,fk_company_id | fk_company_id  | 8       | sampl.company.company_id   |    1 | Using where |
|  1 | SIMPLE      | customer_setting | ref  | fk_customer_id        | fk_customer_id | 8       | sampl.customer.customer_id |    2 | Using where |
+----+-------------+------------------+------+-----------------------+----------------+---------+----------------------------+------+-------------+

さて、同じSQLを5.7+condition_fanout_filter=ON(defaultはONです)の状態で実行すると、

+----+-------------+------------------+------------+--------+-----------------------+----------------+---------+----------------------------+-------+----------+-------------+
| id | select_type | table            | partitions | type   | possible_keys         | key            | key_len | ref                        | rows  | filtered | Extra       |
+----+-------------+------------------+------------+--------+-----------------------+----------------+---------+----------------------------+-------+----------+-------------+
|  1 | SIMPLE      | customer         | NULL       | ALL    | PRIMARY,fk_company_id | NULL           | NULL    | NULL                       | 29913 |     1.11 | Using where |
|  1 | SIMPLE      | customer_setting | NULL       | ref    | fk_customer_id        | fk_customer_id | 8       | sampl.customer.customer_id |     2 |    10.00 | Using where |
|  1 | SIMPLE      | company          | NULL       | eq_ref | PRIMARY               | PRIMARY        | 8       | sampl.customer.company_id  |     1 |   100.00 | NULL        |
+----+-------------+------------------+------------+--------+-----------------------+----------------+---------+----------------------------+-------+----------+-------------+

こうなりました。WHERE句で絞り込まれている( and customer.flg = 'n' and customer.start_date <= 'YYYY-MM-DD' and customer.end_date >= 'YYYY-MM-DD') customer表が駆動表に入れ替わりましたが、INDEXが無いのでFULL SCAN。5.6の実行計画も駆動表はFULL SCANなので問題ないかと思いきや、元の表よりレコード数が多いので遅くなってしまい、爆死しました。

対策としては

1) INDEXを貼る。駆動表の条件句+結合句に貼ってあげるとおそらく一番妥当な実行計画になります。

sql > create index ix_customer_flg_enddate_customerid on customer(flg,end_date,customer_id);
+----+-------------+------------------+------------+--------+----------------------------------------------------------+------------------------------------+---------+----------------------------+------+----------+------------------------------------+
| id | select_type | table            | partitions | type   | possible_keys                                            | key                                | key_len | ref                        | rows | filtered | Extra                              |
+----+-------------+------------------+------------+--------+----------------------------------------------------------+------------------------------------+---------+----------------------------+------+----------+------------------------------------+
|  1 | SIMPLE      | customer         | NULL       | range  | PRIMARY,fk_company_id,ix_customer_flg_enddate_customerid | ix_customer_flg_enddate_customerid | 6       | NULL                       | 5374 |    33.33 | Using index condition; Using where |
|  1 | SIMPLE      | customer_setting | NULL       | ref    | fk_customer_id                                           | fk_customer_id                     | 8       | sampl.customer.customer_id |    2 |    10.00 | Using where                        |
|  1 | SIMPLE      | company          | NULL       | eq_ref | PRIMARY                                                  | PRIMARY                            | 8       | sampl.customer.company_id  |    1 |   100.00 | NULL                               |
+----+-------------+------------------+------------+--------+----------------------------------------------------------+------------------------------------+---------+----------------------------+------+----------+------------------------------------+

2) 駆動表を固定する。とりあえず元の実行計画に戻したい場合はstraight join で結合順序を固定するのも手です。

sql > explain  select  
    *
 from company 
 straight_join customer  on customer.company_id = company.company_id 
 inner join customer_setting  on customer_setting.customer_id = customer.customer_id  
 where customer_setting.flg = 'n'    
 and customer.flg = 'n'
 and customer.start_date <= '2017-06-26'
 and customer.end_date >= '2017-06-26';
+----+-------------+------------------+------------+------+----------------------------------------------------------+----------------+---------+----------------------------+------+----------+-------------+
| id | select_type | table            | partitions | type | possible_keys                                            | key            | key_len | ref                        | rows | filtered | Extra       |
+----+-------------+------------------+------------+------+----------------------------------------------------------+----------------+---------+----------------------------+------+----------+-------------+
|  1 | SIMPLE      | company          | NULL       | ALL  | PRIMARY                                                  | NULL           | NULL    | NULL                       | 7948 |   100.00 | NULL        |
|  1 | SIMPLE      | customer         | NULL       | ref  | PRIMARY,fk_company_id,ix_customer_flg_enddate_customerid | fk_company_id  | 8       | sampl.company.company_id   |    3 |     5.99 | Using where |
|  1 | SIMPLE      | customer_setting | NULL       | ref  | fk_customer_id                                           | fk_customer_id | 8       | sampl.customer.customer_id |    2 |    10.00 | Using where |
+----+-------------+------------------+------------+------+----------------------------------------------------------+----------------+---------+----------------------------+------+----------+-------------+

3) condition_fanout_filterをOFFにする。そもそもfilterをoffにしてしまえば元に戻るのはという事でoffにしたら元に戻りました(filteredはすべて100になる)。

sql > set optimizer_swith='condition_fanout_filter=off';
sql > select @@optimizer_switch\G
*************************** 1. row ***************************
@@optimizer_switch: 〜,condition_fanout_filter=off,〜
+----+-------------+------------------+------------+------+-----------------------+----------------+---------+----------------------------+------+----------+-------------+
| id | select_type | table            | partitions | type | possible_keys         | key            | key_len | ref                        | rows | filtered | Extra       |
+----+-------------+------------------+------------+------+-----------------------+----------------+---------+----------------------------+------+----------+-------------+
|  1 | SIMPLE      | company          | NULL       | ALL  | PRIMARY               | NULL           | NULL    | NULL                       | 7948 |   100.00 | NULL        |
|  1 | SIMPLE      | customer         | NULL       | ref  | PRIMARY,fk_company_id | fk_company_id  | 8       | sampl.company.company_id   |    1 |   100.00 | Using where |
|  1 | SIMPLE      | customer_setting | NULL       | ref  | fk_customer_id        | fk_customer_id | 8       | sampl.customer.customer_id |    2 |   100.00 | Using where |
+----+-------------+------------------+------------+------+-----------------------+----------------+---------+----------------------------+------+----------+-------------+

トラブル発生時はfilterオプションの存在を知らずINDEX追加で対処しようとしましたが、同じようにパフォーマンスが劣化したSQLが雨後の筍のように発生した為、同僚の人が見つけてくれた(3)condition_fanout_filter=OFFで対処しました。資料にも、パフォーマンス悪かったらoffにしろと書いてありますね。ただINDEXが適切に貼ってあればこのオプションが有効に働くケースの方が多いと思われるので、メジャーバージョンアップ時はSQLのパフォーマンステストは必須という事と、INDEXはちゃんと貼りましょうという教訓を得ました。