ORDER BY導致未按預期使用索引

  • 2019 年 11 月 28 日
  • 筆記

在MySQL中經常出現未按照理想情況使用索引的情況,今天記錄一種Order by語句的使用導致未按預期使用索引的情況。

1. 問題現象

1.1 SQL語句:

SELECT DISTINCT p.*  FROM tb_name p  WHERE 1=1 AND p.createDate >= '2019-10-23'  AND p.createDate <= '2019-11-20 24:00:00'  AND p.status = '1'  AND p.areaName LIKE  '%上海%'   ORDER BY p.payDate DESC LIMIT 0 , 15

1.2 執行計劃如下:

  +----+-------------+-------+------------+-------+-------------------------------------------------------------+--------------------+---------+------+--------+----------+------------------------------------+  | id | select_type | table | partitions | type  | possible_keys                                               | key                | key_len | ref  | rows   | filtered | Extra                              |  +----+-------------+-------+------------+-------+-------------------------------------------------------------+--------------------+---------+------+--------+----------+------------------------------------+  |  1 | SIMPLE      | p     | NULL       | range | createDate,idx_status_payDate                   | idx_status_payDate | 108     | NULL | 880063 |     0.74 | Using index condition; Using where |  +----+-------------+-------+------------+-------+-------------------------------------------------------------+--------------------+---------+------+--------+----------+------------------------------------+

1.3 表中索引資訊如下:

  +------------------+------------+-------------------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  | Table            | Non_unique | Key_name                      | Seq_in_index | Column_name  | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |  +------------------+------------+-------------------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  | tb_name          |          0 | PRIMARY                       |            1 | id           | A         |     1760103 |     NULL | NULL   |      | BTREE      |         |               |  | tb_name          |          1 | idx_payDate                   |            1 | payDate      | A         |     1734626 |     NULL | NULL   | YES  | BTREE      |         |               |  | tb_name          |          1 | createDate                    |            1 | createDate   | A         |     1736316 |     NULL | NULL   | YES  | BTREE      |         |               |  | tb_name          |          1 | idx_status_payDate            |            1 | status       | A         |           2 |     NULL | NULL   | YES  | BTREE      |         |               |  | tb_name          |          1 | idx_status_payDate            |            2 | payDate      | A         |     1741214 |     NULL | NULL   | YES  | BTREE      |         |               |  +------------------+------------+-------------------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  16 rows in set (0.00 sec)

運行此SQL耗時約5.7s。從SQL及索引情況來看,使用createDate欄位的索引應該會更好才對,為驗證此情況,使用force index來強制使用createDate索引運行一次查看結果。

SQL改為如下:

SELECT DISTINCT p.*  FROM tb_name p  FORCE INDEX (createDate)  WHERE 1=1 AND p.createDate >= '2019-10-23'  AND p.createDate <= '2019-11-20 24:00:00'  AND p.status = '1'  AND p.areaName LIKE  '%上海%'  ORDER BY p.payDate DESC LIMIT 0 , 15

修改後執行計劃如下:

root@db09:03:13>explain SELECT DISTINCT p.*  FROM tb_namep  FORCE INDEX (createDate)      -> WHERE 1=1 AND p.createDate >= '2019-10-23'  AND p.createDate <= '2019-11-20 24:00:00'  AND p.status = '1'  AND p.areaName LIKE  '%上海%'      ->  ORDER BY p.payDate DESC LIMIT 0 , 15;  +----+-------------+-------+------------+-------+---------------+------------+---------+------+--------+----------+----------------------------------------------------+  | id | select_type | table | partitions | type  | possible_keys | key        | key_len | ref  | rows   | filtered | Extra                                              |  +----+-------------+-------+------------+-------+---------------+------------+---------+------+--------+----------+----------------------------------------------------+  |  1 | SIMPLE      | p     | NULL       | range | createDate    | createDate | 6       | NULL | 117858 |     1.11 | Using index condition; Using where; Using filesort |  +----+-------------+-------+------------+-------+---------------+------------+---------+------+--------+----------+----------------------------------------------------+  1 row in set, 3 warnings (0.00 sec)  

實際運行該SQL耗時約為0.15s,相差約50倍的差距。

1.5 簡單分析

從執行計劃情況對比來看,使用createDate會進行額外的排序(Using filesort),這個不難理解。

2 各種不太合理嘗試

2.1 強制使用索引

使用force index (createDate)是可以解決的,此方式上面已經測試過了

2.2 忽略不理想的索引

類似於force index,可以使用IGNORE INDEX ,其實目的也在於使用上createDate 索引,例如:

SELECT DISTINCT p.*  FROM tb_name p  IGNORE INDEX (idx_status_payDate,idx_payDate)  WHERE 1=1 AND p.createDate >= '2019-10-23'  AND p.createDate <= '2019-11-20 24:00:00'  AND p.status = '1'  AND p.areaName LIKE  '%上海%'  ORDER BY p.payDate DESC LIMIT 0 , 15

其效果和force index 一致,運行耗時也在0.15s左右。

2.3 添加組合索引

將payDate 及createDate 添加為組合索引,但是此舉不是一個好辦法,執行計劃也未按理想情況運行。

3. 相對合理的方式

無論使用force index 還是 ignore index都會影響MySQL優化器自身的執行情況。例如createDate 如果範圍很大,那麼其實走payDate 的索引取前15條記錄會更快,為了讓應用改動最少且不會因為其他條件的變化而導致未能走合理的索引,選擇另一種優化方案,將SQL改為如下情況:

SELECT DISTINCT p.*  FROM tb_name p  WHERE 1=1 AND p.createDate >= '2019-10-23'  AND p.createDate <= '2019-11-20 24:00:00'  AND p.status = '1'  AND p.areaName LIKE  '%上海%'   ORDER BY p.payDate DESC, createDate LIMIT 0 , 15

此時執行執行計劃如下:

+----+-------------+-------+------------+-------+-------------------------------+------------+---------+------+--------+----------+----------------------------------------------------+  | id | select_type | table | partitions | type  | possible_keys                 | key        | key_len | ref  | rows   | filtered | Extra                                              |  +----+-------------+-------+------------+-------+-------------------------------+------------+---------+------+--------+----------+----------------------------------------------------+  |  1 | SIMPLE      | p     | NULL       | range | createDate,idx_status_payDate | createDate | 6       | NULL | 123024 |     5.55 | Using index condition; Using where; Using filesort |  +----+-------------+-------+------------+-------+-------------------------------+------------+---------+------+--------+----------+----------------------------------------------------+  1 row in set, 3 warnings (0.00 sec)

調整createDate 之後,執行執行計劃:

root@db 09:51:00>EXPLAIN      -> SELECT DISTINCT p.*  FROM tb_name p   IGNORE INDEX (idx_status_synIs_deleteStatus)      -> WHERE 1=1 AND p.createDate >= '2009-10-23'  AND p.createDate <= '2019-11-20 24:00:00'  AND p.status = '1'  AND p.areaName LIKE  '%上海%'      ->  ORDER BY p.payDate DESC,createDate DESC  LIMIT 0 , 15;  +----+-------------+-------+------------+------+-------------------------------+--------------------+---------+-------+--------+----------+----------------------------------------------------+  | id | select_type | table | partitions | type | possible_keys                 | key                | key_len | ref   | rows   | filtered | Extra                                              |  +----+-------------+-------+------------+------+-------------------------------+--------------------+---------+-------+--------+----------+----------------------------------------------------+  |  1 | SIMPLE      | p     | NULL       | ref  | createDate,idx_status_payDate | idx_status_payDate | 108     | const | 880205 |     5.56 | Using index condition; Using where; Using filesort |  +----+-------------+-------+------------+------+-------------------------------+--------------------+---------+-------+--------+----------+----------------------------------------------------+  1 row in set, 3 warnings (0.00 sec)

也按預期的情況正常。由此看來此方式相對之前的方案更佳理想的。