index dive 导致同类sql执行计划不一致

Posted 渔夫数据库笔记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了index dive 导致同类sql执行计划不一致相关的知识,希望对你有一定的参考价值。

1.版本

1)操作系统

cat /proc/version

Linux version 3.10.0-1127.18.2.el7.x86_64 (mockbuild@kbuilder.bsys.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC) )

2)mysql数据库版本

mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.19    |
+-----------+
1 row in set (0.00 sec)
 

 

2.问题描述

2.1 发现问题

   某类sql大多数情况下执行时间正常,有时执行缺比较慢

1)执行慢的sql 及执行计划

mysql> SELECT *
    -> FROM test_table_exchange tce
    ->         LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
    -> WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
    ->         AND tce.exchange_status = 2
    ->         AND (tce.product_id = 1
    ->                 OR tce.product_id = ''
    ->                 OR tce.product_id IS NULL)
    -> LIMIT 1;
Empty set (41.54 sec)

慢日志
# Query_time: 41.545647  Lock_time: 0.000767 Rows_sent: 0  Rows_examined: 12035955
use test_db;
SET timestamp=1610690522;
SELECT *
FROM test_table_exchange tce
        LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
        AND tce.exchange_status = 2
        AND (tce.product_id = 1
                OR tce.product_id = ''
                OR tce.product_id IS NULL)
LIMIT 1;


执行计划
mysql> explain SELECT *
    -> FROM test_table_exchange tce
    ->         LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
    -> WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
    ->         AND tce.exchange_status = 2
    ->         AND (tce.product_id = 1
    ->                 OR tce.product_id = ''
    ->                 OR tce.product_id IS NULL)
    -> LIMIT 1;
+----+-------------+-------+------------+------+--------------------------------------------------------+-----------------+---------+--------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys                                          | key             | key_len | ref                | rows | filtered | Extra       |
+----+-------------+-------+------------+------+--------------------------------------------------------+-----------------+---------+--------------------+------+----------+-------------+
|  1 | SIMPLE      | tce   | NULL       | ALL  | PRIMARY                                                | NULL            | NULL    | NULL               |  672 |     1.90 | Using where |
|  1 | SIMPLE      | cec   | NULL       | ref  | idx_exchange_id,idx_exchange_code_exchange_code_status | idx_exchange_id | 8       | test_db.tce.id |  832 |     0.24 | Using where |
+----+-------------+-------+------------+------+--------------------------------------------------------+-----------------+---------+--------------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec

##通过 optimize trace 我们看到,优化器预计 tce 表经过过滤后只剩下 12 条记录,那么该执行计划需要扫描的记录数 预估为 12*832+12=9996 ,
如果是先通过 idx_exchange_code_exchange_code_status 索引访问 cec 表然后再同 tce 关联,那么预估需要扫描的记录数为 30228*2=60456
所以就选择先全表扫描 tce表,然后再通过 idx_exchange_id 关联 cec表。但是其实 idx_exchange_id 的 Cardinality 很小,每次关联远远不止要扫描832 行(),所以导致该执行计划时间效果很差

2) 执行快的sql

mysql> SELECT *
    -> FROM test_table_exchange tce
    ->         LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
    -> WHERE cec.exchange_code = 'ac9081d1cb7903d5435681b41b8ef38f'
    ->         AND tce.exchange_status = 2
    ->         AND (tce.product_id = 1
    ->                 OR tce.product_id = ''
    ->                 OR tce.product_id IS NULL)
    -> LIMIT 1;
......(输出了一条记录)
1 row in set (0.01 sec)

mysql> explain SELECT *
    -> FROM test_table_exchange tce
    ->         LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
    -> WHERE cec.exchange_code = 'ac9081d1cb7903d5435681b41b8ef38f'
    ->         AND tce.exchange_status = 2
    ->         AND (tce.product_id = 1
    ->                 OR tce.product_id = ''
    ->                 OR tce.product_id IS NULL)
    -> LIMIT 1;
+----+-------------+-------+------------+--------+--------------------------------------------------------+----------------------------------------+---------+-----------------------------+------+----------+-------------+
| id | select_type | table | partitions | type   | possible_keys                                          | key                                    | key_len | ref                         | rows | filtered | Extra       |
+----+-------------+-------+------------+--------+--------------------------------------------------------+----------------------------------------+---------+-----------------------------+------+----------+-------------+
|  1 | SIMPLE      | cec   | NULL       | ref    | idx_exchange_id,idx_exchange_code_exchange_code_status | idx_exchange_code_exchange_code_status | 767     | const                       |    1 |   100.00 | Using where |
|  1 | SIMPLE      | tce   | NULL       | eq_ref | PRIMARY                                                | PRIMARY                                | 8       | test_db.cec.exchange_id |    1 |     5.00 | Using where |
+----+-------------+-------+------------+--------+--------------------------------------------------------+----------------------------------------+---------+-----------------------------+------+----------+-------------+
2 rows in set, 1 warning (0.01 sec)

3) 表结构

CREATE TABLE `test_table_exchange` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `title` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'xxxx',
  `exchange_type` int NOT NULL COMMENT 'xxxx',
  `begin_time` datetime NOT NULL COMMENT 'xxxx',
  `end_time` datetime NOT NULL COMMENT 'xxxx',
  `exchange_word` varchar(1024) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'xxxx',
  `total_count` bigint NOT NULL COMMENT 'xxxx',
  `distribute_count` bigint NOT NULL DEFAULT '0' COMMENT 'xxxx',
  `day_max_exchage_count` int NOT NULL DEFAULT '0' COMMENT 'xxxx',
  `total_max_exchage_count` int NOT NULL DEFAULT '0' COMMENT 'xxxx',
  `exchange_status` int NOT NULL COMMENT 'xxxx',
  `coupon_status` int NOT NULL COMMENT 'xxxx',
  `exchange_code_url` varchar(1024) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'xxxx',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'xxxx',
  `create_by` varchar(64) COLLATE utf8_bin NOT NULL COMMENT 'xxxx',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'xxxx',
  `update_by` varchar(64) COLLATE utf8_bin NOT NULL COMMENT 'xxxx',
  `city_code` varchar(1400) COLLATE utf8_bin DEFAULT '' COMMENT 'xxxx',
  `audit_status` int NOT NULL DEFAULT '2' COMMENT 'xxxx',
  `auditor` varchar(255) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'xxxx',
  `product_id` int NOT NULL DEFAULT '1' COMMENT 'xxxx',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=677 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='xxxx'


CREATE TABLE `test_table_exchange_code` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `exchange_id` bigint NOT NULL COMMENT 'xxxx',
  `exchange_code` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'xxxx',
  `exchange_code_status` int NOT NULL COMMENT 'xxxx',
  `user_id` varchar(64) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'xxxx',
  `exchange_time` datetime DEFAULT NULL COMMENT 'xxxx',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'xxxx',
  `create_by` varchar(64) COLLATE utf8_bin NOT NULL COMMENT 'xxxx',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'xxxx',
  `update_by` varchar(64) COLLATE utf8_bin NOT NULL COMMENT 'xxxx',
  `product_id` int DEFAULT '1' COMMENT 'xxxx',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_exchange_id` (`exchange_id`) USING BTREE,
  KEY `idx_user_id` (`user_id`),
  KEY `idx_exchange_code_exchange_code_status` (`exchange_code`,`exchange_code_status`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=21300254 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='xxxx'

4) 两个不同 exchange_code 值的分布

mysql> explain select * from test_table_exchange_code where exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac';
+----+-------------+------------------------+------------+------+----------------------------------------+----------------------------------------+---------+-------+-------+----------+-------+
| id | select_type | table                  | partitions | type | possible_keys                          | key                                    | key_len | ref   | rows  | filtered | Extra |
+----+-------------+------------------------+------------+------+----------------------------------------+----------------------------------------+---------+-------+-------+----------+-------+
|  1 | SIMPLE      | test_table_exchange_code | NULL       | ref  | idx_exchange_code_exchange_code_status | idx_exchange_code_exchange_code_status | 767     | const | 30228 |   100.00 | NULL  |
+----+-------------+------------------------+------------+------+----------------------------------------+----------------------------------------+---------+-------+-------+----------+-------+
1 row in set, 1 warning (0.00 sec)

mysql> select count(*) from test_table_exchange_code where exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac';
+----------+
| count(*) |
+----------+
|    17080 |
+----------+
1 row in set (0.01 sec)

mysql> explain select * from test_table_exchange_code where exchange_code = 'ac9081d1cb7903d5435681b41b8ef38f';
+----+-------------+------------------------+------------+------+----------------------------------------+----------------------------------------+---------+-------+------+----------+-------+
| id | select_type | table                  | partitions | type | possible_keys                          | key                                    | key_len | ref   | rows | filtered | Extra |
+----+-------------+------------------------+------------+------+----------------------------------------+----------------------------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | test_table_exchange_code | NULL       | ref  | idx_exchange_code_exchange_code_status | idx_exchange_code_exchange_code_status | 767     | const |    1 |   100.00 | NULL  |
+----+-------------+------------------------+------------+------+----------------------------------------+----------------------------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

mysql> select count(*) from test_table_exchange_code where exchange_code = 'ac9081d1cb7903d5435681b41b8ef38f';
+----------+
| count(*) |
+----------+
|        1 |
+----------+
1 row in set (0.00 sec)

2.2 问题原因分析

    通过上面的执行计划我们很清楚的看出来两条sql 因为where条件中 exchange_code 的值不同(符合条件的行数不同),导致执行计划不同,所以执行时间不同。本质上是mysql估算出符合这两个条件的行数一个是17080行,另一个是1行,从而选择了不同的执行计划,那么 mysql 是通过什么估算出符合条件的行数的呢?
    在没有直方图之前(mysql 8.0 之前是没有直方图的,虽然我们的数据库版本是8.0但是我们没有对该列收集直方图),仅仅靠mysql的统计信息是无法比较精确的估算出某个列值存在的个数的。也就是说如果仅仅通过统计信息来生成执行计划不论 exchange_code 值是什么,上面两个sql的执行计划都应该是一致的(这一点后面我们会进行验证)。那么我们的例子中为什么优化器能估算出符合 exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac' 有17080行,而符合 exchange_code = 'ac9081d1cb7903d5435681b41b8ef38f' 只有一行的呢?这个靠的是 index dives,具体什么是 index dives 不在本文展开讨论,以后有机会再说。我们只需要知道对于等值范围查询(in 查询或者=查询),当查询范围个数(in中有几个值或者说=''查询中有几个 or)小于 eq_range_index_dive_limit(该参数从MySQL 5.6 开始引入) 参数设定的值时,使用index dives 来估计每个查询范围内的行数。否则使用统计信息来估算每个范围内的行数。
NOTE1:index dives能够比较精确的估计出范围内的行数,但是比较消耗资源。
NOTE2: index dives 只适用于非唯一索引(都唯一索引了哪还需要估算等值范围内行数,一个范围最多就一行)

3.问题处理

    现在我们弄清楚了问题的原因,下面来想办法优化上面的慢查询。下面给出几个优化方案,你可以选择资源消耗最低的方案,或者对你的业务来说影响最小的方案。在开始说明优化方案前,我们先来验证下前面提到的,如果只是通过统计信息来生成执行计划,那么上面例子中两个查询的执行计划应该是一致的

mysql> show variables like 'eq_range_index_dive_limit';
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| eq_range_index_dive_limit | 200   |
+---------------------------+-------+
1 row in set (0.01 sec)

mysql> set eq_range_index_dive_limit=1;
Query OK, 0 rows affected (0.00 sec)

mysql> explain select * from test_table_exchange_code where exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac';
+----+-------------+------------------------+------------+------+----------------------------------------+----------------------------------------+---------+-------+------+----------+-------+
| id | select_type | table                  | partitions | type | possible_keys                          | key                                    | key_len | ref   | rows | filtered | Extra |
+----+-------------+------------------------+------------+------+----------------------------------------+----------------------------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | test_table_exchange_code | NULL       | ref  | idx_exchange_code_exchange_code_status | idx_exchange_code_exchange_code_status | 767     | const |    1 |   100.00 | NULL  |
+----+-------------+------------------------+------------+------+----------------------------------------+----------------------------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

mysql> explain SELECT *
    -> FROM test_table_exchange tce
    ->         LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
    -> WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
    ->         AND tce.exchange_status = 2
    ->         AND (tce.product_id = 1
    ->                 OR tce.product_id = ''
    ->                 OR tce.product_id IS NULL)
    -> LIMIT 1;
+----+-------------+-------+------------+--------+--------------------------------------------------------+----------------------------------------+---------+-----------------------------+------+----------+-------------+
| id | select_type | table | partitions | type   | possible_keys                                          | key                                    | key_len | ref                         | rows | filtered | Extra       |
+----+-------------+-------+------------+--------+--------------------------------------------------------+----------------------------------------+---------+-----------------------------+------+----------+-------------+
|  1 | SIMPLE      | cec   | NULL       | ref    | idx_exchange_id,idx_exchange_code_exchange_code_status | idx_exchange_code_exchange_code_status | 767     | const                       |    1 |   100.00 | Using where |
|  1 | SIMPLE      | tce   | NULL       | eq_ref | PRIMARY                                                | PRIMARY                                | 8       | test_db.cec.exchange_id |    1 |     5.00 | Using where |
+----+-------------+-------+------------+--------+--------------------------------------------------------+----------------------------------------+---------+-----------------------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)


#不使用 index dives时查询的慢查日志如下
# Query_time: 0.102908  Lock_time: 0.000357 Rows_sent: 0  Rows_examined: 34160
SET timestamp=1610695549;
SELECT *
FROM test_table_exchange tce
        LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
        AND tce.exchange_status = 2
        AND (tce.product_id = 1
                OR tce.product_id = ''
                OR tce.product_id IS NULL)
LIMIT 1;


mysql> 
mysql> set eq_range_index_dive_limit=200;
Query OK, 0 rows affected (0.00 sec)

mysql> explain select * from test_table_exchange_code where exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac';
+----+-------------+------------------------+------------+------+----------------------------------------+----------------------------------------+---------+-------+-------+----------+-------+
| id | select_type | table                  | partitions | type | possible_keys                          | key                                    | key_len | ref   | rows  | filtered | Extra |
+----+-------------+------------------------+------------+------+----------------------------------------+----------------------------------------+---------+-------+-------+----------+-------+
|  1 | SIMPLE      | test_table_exchange_code | NULL       | ref  | idx_exchange_code_exchange_code_status | idx_exchange_code_exchange_code_status | 767     | const | 30228 |   100.00 | NULL  |
+----+-------------+------------------------+------------+------+----------------------------------------+----------------------------------------+---------+-------+-------+----------+-------+
1 row in set, 1 warning (0.01 sec)

mysql> explain SELECT *
    -> FROM test_table_exchange tce
    ->         LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
    -> WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
    ->         AND tce.exchange_status = 2
    ->         AND (tce.product_id = 1
    ->                 OR tce.product_id = ''
    ->                 OR tce.product_id IS NULL)
    -> LIMIT 1;
+----+-------------+-------+------------+------+--------------------------------------------------------+-----------------+---------+--------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys                                          | key             | key_len | ref                | rows | filtered | Extra       |
+----+-------------+-------+------------+------+--------------------------------------------------------+-----------------+---------+--------------------+------+----------+-------------+
|  1 | SIMPLE      | tce   | NULL       | ALL  | PRIMARY                                                | NULL            | NULL    | NULL               |  672 |     1.90 | Using where |
|  1 | SIMPLE      | cec   | NULL       | ref  | idx_exchange_id,idx_exchange_code_exchange_code_status | idx_exchange_id | 8       | test_db.tce.id |  832 |     0.24 | Using where |
+----+-------------+-------+------------+------+--------------------------------------------------------+-----------------+---------+--------------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

##我们把会话级别 eq_range_index_dive_limit 设置为1,这样所有的等值范围估算都使用统计信息了,看到估算的行数为1,并且执行计划也同另一条sql一致了。
好了至此我们完全搞清楚为什么上面两个同类型的sql,执行计划为什么不一致了。

 

3.1 优化方案——关联顺序优化

优化前慢查询的执行计划及慢日志信息

# Query_time: 40.277456  Lock_time: 0.000889 Rows_sent: 1  Rows_examined: 11546125
use test_db;
SET timestamp=1609399180;
SELECT *
FROM test_table_exchange tce
        LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
        AND tce.exchange_status = 2
        AND (tce.product_id = 1
                OR tce.product_id = ''
                OR tce.product_id IS NULL)
LIMIT 1;

执行计划为
mysql> explain SELECT *
    -> FROM test_table_exchange tce
    ->         LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
    -> WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
    ->         AND tce.exchange_status = 2
    ->         AND (tce.product_id = 1
    ->                 OR tce.product_id = ''
    ->                 OR tce.product_id IS NULL)
    -> LIMIT 1;
+----+-------------+-------+------------+------+--------------------------------------------------------+-----------------+---------+--------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys                                          | key             | key_len | ref                | rows | filtered | Extra       |
+----+-------------+-------+------------+------+--------------------------------------------------------+-----------------+---------+--------------------+------+----------+-------------+
|  1 | SIMPLE      | tce   | NULL       | ALL  | PRIMARY                                                | NULL            | NULL    | NULL               |  672 |     1.90 | Using where |
|  1 | SIMPLE      | cec   | NULL       | ref  | idx_exchange_id,idx_exchange_code_exchange_code_status | idx_exchange_id | 8       | test_db.tce.id |  832 |     0.24 | Using where |
+----+-------------+-------+------------+------+--------------------------------------------------------+-----------------+---------+--------------------+------+----------+-------------+
2 rows in set, 1 warning (0.01 sec)
#  慢日志中显示,Rows_examined: 11546125 该查询调用了 11546125 次引擎。如果看explain 执行计划的话 估算该查询大概会调用 672*832+676=559776 次引擎。这个差距还是很大的(数据倾斜)

1) 使用 hint 变更表的关联顺序

#因为是left join 所以无法使用 STRAIGHT_JOIN 来固话关联顺序,此处通过 use index 也可以改变两个表的关联顺序

mysql> explain SELECT *
    -> FROM test_table_exchange tce
    ->         LEFT JOIN test_table_exchange_code cec use index(idx_exchange_code_exchange_code_status) ON cec.exchange_id = tce.id
    -> WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
    ->         AND tce.exchange_status = 2
    ->         AND (tce.product_id = 1
    ->                 OR tce.product_id = ''
    ->                 OR tce.product_id IS NULL)
    -> LIMIT 1;
+----+-------------+-------+------------+--------+----------------------------------------+----------------------------------------+---------+-----------------------------+-------+----------+-------------+
| id | select_type | table | partitions | type   | possible_keys                          | key                                    | key_len | ref                         | rows  | filtered | Extra       |
+----+-------------+-------+------------+--------+----------------------------------------+----------------------------------------+---------+-----------------------------+-------+----------+-------------+
|  1 | SIMPLE      | cec   | NULL       | ref    | idx_exchange_code_exchange_code_status | idx_exchange_code_exchange_code_status | 767     | const                       | 30228 |   100.00 | Using where |
|  1 | SIMPLE      | tce   | NULL       | eq_ref | PRIMARY                                | PRIMARY                                | 8       | test_db.cec.exchange_id |     1 |     5.00 | Using where |
+----+-------------+-------+------------+--------+----------------------------------------+----------------------------------------+---------+-----------------------------+-------+----------+-------------+
2 rows in set, 1 warning (0.01 sec)

##我们看到这样执行计划就跟 exchange_code = 'ac9081d1cb7903d5435681b41b8ef38f' 一致了
预计扫描行数为30228*2


# Query_time: 0.050697  Lock_time: 0.000270 Rows_sent: 0  Rows_examined: 34160
use test_db;
SET timestamp=1610694806;
SELECT *
FROM test_table_exchange tce
        LEFT JOIN test_table_exchange_code cec use index(idx_exchange_code_exchange_code_status) ON cec.exchange_id = tce.id
WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
        AND tce.exchange_status = 2
        AND (tce.product_id = 1
                OR tce.product_id = ''
                OR tce.product_id IS NULL)
LIMIT 1;
#符合 exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac' 记录有 17080 记录,所以 cec 表中扫描 17080 行,然后通过主键和 tce 表关联,tce 表也总计扫描 17080 行
(说明cec表中每个符合条件的行,都能通过tce的主键匹配到值)所以总过扫描 17080*2=34160 行

 

2) 通过 调整 eq_range_index_dive_limit 值来改变执行计划

#这个我们上面已经详细解释如果生成执行计划时不使用 index dives,则类似查询的执行计划都是一致的。上面已经验证过 index dives 对执行计划的影响了。

 

3.2 优化方案2 ——索引优化

mysql> explain SELECT *
    -> FROM test_table_exchange tce
    ->         LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
    -> WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
    ->         AND tce.exchange_status = 2
    ->         AND (tce.product_id = 1
    ->                 OR tce.product_id = ''
    ->                 OR tce.product_id IS NULL)
    -> LIMIT 1;
+----+-------------+-------+------------+------+--------------------------------------------------------+-----------------+---------+--------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys                                          | key             | key_len | ref                | rows | filtered | Extra       |
+----+-------------+-------+------------+------+--------------------------------------------------------+-----------------+---------+--------------------+------+----------+-------------+
|  1 | SIMPLE      | tce   | NULL       | ALL  | PRIMARY                                                | NULL            | NULL    | NULL               |  672 |     1.90 | Using where |
|  1 | SIMPLE      | cec   | NULL       | ref  | idx_exchange_id,idx_exchange_code_exchange_code_status | idx_exchange_id | 8       | test_db.tce.id |  832 |     0.24 | Using where |
+----+-------------+-------+------------+------+--------------------------------------------------------+-----------------+---------+--------------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)


mysql> select count(*),count(distinct exchange_id),count(distinct exchange_id)/count(*) from test_table_exchange_code;
+----------+-----------------------------+--------------------------------------+
| count(*) | count(distinct exchange_id) | count(distinct exchange_id)/count(*) |
+----------+-----------------------------+--------------------------------------+
| 13452730 |                         538 |                               0.0000 |
+----------+-----------------------------+--------------------------------------+
1 row in set (6.28 sec)

mysql> select count(*),count(distinct exchange_id,exchange_code),count(distinct exchange_id,exchange_code)/count(*) from test_table_exchange_code;
+----------+-------------------------------------------+----------------------------------------------------+
| count(*) | count(distinct exchange_id,exchange_code) | count(distinct exchange_id,exchange_code)/count(*) |
+----------+-------------------------------------------+----------------------------------------------------+
| 13452730 |                                  12062004 |                                             0.8966 |
+----------+-------------------------------------------+----------------------------------------------------+
#(exchange_id)索引的选择性十分糟糕,(exchange_id,exchange_code)索引的选择性好很多

所以如果我们给 cec 表加上 (exchange_id,exchange_code) 全表扫描 tce 表后,通过(exchange_id,exchange_code)同 cec 表关联,则需要扫描的行数及需要回表的次数大大降低
 

mysql> alter table test_table_exchange_code add index ix_exchange_id_code(exchange_id,exchange_code);

mysql> explain SELECT * 
    -> FROM test_table_exchange tce
    ->         LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
    -> WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
    ->         AND tce.exchange_status = 2
    ->         AND (tce.product_id = 1
    ->                 OR tce.product_id = ''
    ->                 OR tce.product_id IS NULL)
    -> LIMIT 1;
+----+-------------+-------+------------+------+----------------------------------------------------------------------------+---------------------+---------+--------------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys                                                              | key                 | key_len | ref                      | rows | filtered | Extra       |
+----+-------------+-------+------------+------+----------------------------------------------------------------------------+---------------------+---------+--------------------------+------+----------+-------------+
|  1 | SIMPLE      | tce   | NULL       | ALL  | PRIMARY                                                                    | NULL                | NULL    | NULL                     |  672 |     1.90 | Using where |
|  1 | SIMPLE      | cec   | NULL       | ref  | idx_exchange_id,idx_exchange_code_exchange_code_status,ix_exchange_id_code | ix_exchange_id_code | 775     | test_db.tce.id,const |    1 |   100.00 | NULL        |
+----+-------------+-------+------------+------+----------------------------------------------------------------------------+---------------------+---------+--------------------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

mysql> SELECT *
    -> FROM test_table_exchange tce
    ->         LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
    -> WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
    ->         AND tce.exchange_status = 2
    ->         AND (tce.product_id = 1
    ->                 OR tce.product_id = ''
    ->                 OR tce.product_id IS NULL)
    -> LIMIT 1;
Empty set (0.00 sec)


慢查日志
# Query_time: 0.003749  Lock_time: 0.000316 Rows_sent: 0  Rows_examined: 673
SET timestamp=1610696615;
SELECT *
FROM test_table_exchange tce
        LEFT JOIN test_table_exchange_code cec ON cec.exchange_id = tce.id
WHERE cec.exchange_code = '643b9ad40c80b4c65f635dcdd3f86cac'
        AND tce.exchange_status = 2
        AND (tce.product_id = 1
                OR tce.product_id = ''
                OR tce.product_id IS NULL)
LIMIT 1;

#因为 tce 表中有 673 行,并且通过关联和 where 条件在 cec表中并没有符合条件的记录,所以总的调用引擎次数即为 673

 

以上是关于index dive 导致同类sql执行计划不一致的主要内容,如果未能解决你的问题,请参考以下文章

Oracle固定SQL的执行计划---SQL Profile

SQL Server中参数化SQL写法遇到parameter sniff ,导致不合理执行计划重用的一种解决方案

SQL Server中参数化SQL写法遇到parameter sniff ,导致不合理执行计划重用的一种解决方案

表收集错误导致执行计划错误

使用apache livy导致的结果集不一致问题记录

使用apache livy导致的结果集不一致问题记录