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 ,导致不合理执行计划重用的一种解决方案