第一章 问题概述
对ORACLE优化器而言,当满足某些场景时(如存在主外键关系、无意义的排序动作等)。即这部分查询动作做与不做,对最终的结果没有影响时,优化器是会省略这部分额外的查询开销的。
但在MySQL环境中,经过总结测试,上述类似场景却没有考虑到自动消除的机制。并且极有可能导致性能问题。
以下总结了部分性能问题场景。涵盖的版本经测试在主流的5.7-8.0均有包含。不排除随着版本的变化可能会有细微的差异表现。但问题的原因及解决方案基本相同。
测试库版本:MySQL8.0.26
测试脚本:
CREATE TABLE test_t1 AS SELECT * FROM information_schema.COLUMNS;
ALTER TABLE test_t1 add COLUMN ID INT AUTO_INCREMENT PRIMARY KEY;
CREATE TABLE test_t2 AS SELECT * FROM information_schema.COLUMNS;
ALTER TABLE test_t2 add COLUMN ID INT AUTO_INCREMENT PRIMARY KEY;
CREATE INDEX KEY_TABLE_SCHEMA ON TEST_T2(TABLE_SCHEMA);
第二章 场景一:GROUP BY外连接不必要的表
2.1 案例一:group by不包含聚合列
SQL文本
select test_t1.TABLE_SCHEMA
from test_t1
left join test_t2
on test_t1.TABLE_SCHEMA=test_t2.TABLE_SCHEMA
where test_t1.ID < 1000
group by test_t1.TABLE_SCHEMA;
查询表现

执行计划:

问题分析
SQL中只查询了test_t1表的列。且在关联test_t2后。有一个聚合动作。也是按照test_t1做聚合。因此整段SQL,除了表关联之外,没有访问test_t2表的任何数据。
也就是外连接关联test_t2时,无论关联出多少数据量(0-N行)。均不影响最终的结果。这种情况下对test_t2表的访问完全是可以省略的。
省略后并不影响任何最终结果,但却可以减少大量的数据访问。
优化建议
去掉不必要的表关联。
select test_t1.TABLE_SCHEMA
from test_t1
where test_t1.ID < 1000
group by test_t1.TABLE_SCHEMA;
调整后的执行效率:

2.2 案例二:包含聚合列但与关联表无关
SQL文本
select test_t1.TABLE_SCHEMA,max(test_t1.id)
from test_t1
left join test_t2
on test_t1.TABLE_SCHEMA=test_t2.TABLE_SCHEMA
where test_t1.ID < 1000
group by test_t1.TABLE_SCHEMA;
查询表现

执行计划:

问题分析
SQL中只查询了test_t1表的列。且在关联test_t2后。有一个聚合动作。也是按照test_t1做聚合。因此整段SQL,除了表关联之外,没有访问test_t2表的任何数据。
也就是外连接关联test_t2时,无论关联出多少数据量(0-N行)。均不影响最终的结果。这种情况下对test_t2表的访问完全是可以省略的。
省略后并不影响任何最终结果,但却可以减少大量的数据访问。
优化建议
去掉不必要的表关联。
select test_t1.TABLE_SCHEMA,max(test_t1.id)
from test_t1
where test_t1.ID < 1000
group by test_t1.TABLE_SCHEMA;
调整后的执行效率:

第三章 场景二:关联唯一性列
3.1 案例一:外键表关联主键表
给T1表添加外键引用T2表主键:
alter table test_t1 add constraint FK_t1_ID foreign key(ID) REFERENCES test_t2(ID);
主键表的主键列具有唯一约束。且外键列一定在主键表中存在。
SQL文本
select test_t1.TABLE_SCHEMA
from test_t1
inner join test_t2
on test_t1.id=test_t2.id
where test_t1.TABLE_SCHEMA < 'x';
查询表现

问题分析
SQL中只查询了test_t1表的列。尽管使用内连接关联test_t2。但存在主外键关联(主键表test_t1的外键列id一定在主键表test_t2中的主键列中存在)。
也就是关联test_t2时,只会关联出唯一的一行主键列数据。均不影响最终的结果。这种情况下对test_t2表的访问完全是可以省略的。
省略后并不影响任何最终结果,但却可以减少大量的数据访问。
优化建议
外键表关联主键表时,且没有访问主键表的列,省略关联主键表动作。
select test_t1.TABLE_SCHEMA
from test_t1
where test_t1.TABLE_SCHEMA < 'x';
调整后的执行效率:

3.2 案例二:外连接关联唯一性列
SQL文本
select test_t1.TABLE_SCHEMA
from test_t1
left join test_t2
on test_t1.id=test_t2.id
where test_t1.TABLE_SCHEMA < 'x';
查询表现

问题分析
SQL中只查询了test_t1表的列。且通过外连接关联test_t2表,而关联列上是主键列,不会关联出重复的多行数据。
因此,在查询条件不包含test_t2表时,关联test_t2表的动作可以省略。
省略后并不影响任何最终结果,但却可以减少大量的数据访问。
优化建议和问题总结
外连接关联唯一约束的表,且没有访问主键表的列,省略关联动作。
select test_t1.TABLE_SCHEMA
from test_t1
where test_t1.TABLE_SCHEMA < 'x';
调整后的执行效率:

第四章 场景三:聚合查询包含了不必要的排序
4.1 案例一:单表场景:
SQL文本
select count(*)
from test_t2
where TABLE_SCHEMA < 'x'
order by COLUMN_NAME;
查询表现

执行计划:

问题分析
SQL中只查询了count聚合列。但却在最后包含了排序动作。出现了额外的回表代价。从Extra部分的Using index condition也能确认。
这部分排序完全可以省略。
优化建议
去掉不必要排序。
select count(*)
from test_t2
where TABLE_SCHEMA < 'x';
调整后的执行效率:

4.2 案例二:多表场景:
SQL文本
select count(*)
from test_t1
left join test_t2
on test_t1.TABLE_SCHEMA=test_t2.TABLE_SCHEMA
where test_t1.ID < 1000
order by test_t2.COLUMN_NAME;
查询表现

执行计划:

问题分析
SQL中只查询了count聚合列。但却在最后包含了排序动作。出现了额外的对test_t2的回表代价。
这部分排序完全可以省略。
优化建议
去掉不必要排序。
select count(*)
from test_t1
left join test_t2
on test_t1.TABLE_SCHEMA=test_t2.TABLE_SCHEMA
where test_t1.ID < 1000;
调整后的执行效率:


调整后,尽管仍然访问了test_t2表,但是从Extra部分看为Using index,只访问索引即可完成查询。
第五章 问题总结
通过上述三个场景的分析。
场景一、二:
关联test_t2表完全不必要,但是SQL文本中包含,优化器也没有自动的消除这种不必要的关联查询。在关联条件过滤性不好的时候,造成性能问题。
场景三:
只是查询聚合列,排序动作并不影响聚合结果的返回,但是多一步排序且可能大量的数据回表问题。最终出现了性能问题。
因此SQL书写的时候,就要求只做必要的查询。省略任何不必要的动作。




