菜鸟笔记
提升您的技术认知

SQL 优化实践

工作中需要注意的 SQL 语句优化点,SQL 优化首先要对 SQL 的消耗时间或者使用频率进行分析,然后再进行其他操作。

SQL 优化的方式

1、SQL 的执行次数分析

show global status like 'com_______'; 

通过该命令可以查看当前数据库下的增删改查的使用次数,来采取对应的优化处理, value 值就是执行的次数。

2、慢查询日志

show variables like '%slow_query_log%';  

慢查询日志会记录超出自己设置的时间还没有执行完毕的 sql。

默认情况下,Mysql 数据库并不启动慢查询日志,需要我们手动来设置这个参数,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。

慢查询日志支持将日志记录写入文件,也支持将日志记录写入数据库表。

开启慢查询日志 sql:

set global slow_query_log=1;  

设置慢查询的超时时间:

set global long_query_time=2; -- 以秒为单位  

注意:使用 set global slow_query_log=1 开启了慢查询日志只对当前数据库生效,MySQL 重启后则会失效。如果要永久生效,就必须修改配置文件 my.cnf

3、profile 分析

3.1、开启 profile:

SET profiling = 1;  

3.2、查看当前会话下的所有 sql 执行时间:

show profiles;  

3.3、查看具体 sql 的每个步骤消耗时间:

show profiles for query  xx;   -- 这个xx是 `show profiles` 结果的 query_id  

注意:profile 是用户级变量,每次都得重新启用。

3.4、查看具体 sql 的 cpu 消耗时间:

show profiles cpu  for query  xx; -- 比上个步骤多了一个cpu字段  

explain 关键字

explain 是非常重要的关键字,要善于运用它,通过 explain 可以获得以下信息:

  • 表的读取顺序

  • 数据读取操作的操作类型

  • 哪些索引可以使用

  • 哪些索引被实际使用

  • 表之间的引用

  • 每张表有多少行被优化器查询

explain select * from table; -- explain + sql 语句  

explain 查询的结果包含的字段:

字段 描述
id 查询的序号,包含一组数字,表示查询中执行 select 子句或操作表的顺序。有两种情况:
1.id 相同:执行顺序从上往下;
2.id 不同:id 值越大,优先级越高,越先执行。
select_type 查询类型,主要用于区别普通查询,联合查询,子查询等的复杂查询
1.simple:简单的 select 查询,查询中不包含子查询或者 UNION;
2.primary:查询中若包含任何复杂的子部分,最外层查询被标记;
3.subquery:在 select 或 where 列表中包含了子查询;
4.derived:在 from 列表中包含的子查询被标记为 derived(衍生),MySQL 会递归执行这些子查询,把结果放到临时表中;
5.union:如果第二个 select 出现在 UNION 之后,则被标记为 UNION,如果 union 包含在 from 子句的子查询中,外层 select 被标记为 derived;
6.union result:UNION 的结果。
table 输出的行所引用的表
type 显示连结类型,显示查询使用了何种类型,按照从最佳到最坏类型排序
1.system:表中仅有一行(=系统表)这是 const 联结类型的一个特例;
2.const:表示通过索引一次就找到,const 用于比较 primary key 或者 unique 索引。因为只匹配一行数据,所以如果将主键置于 where 列表中,mysql 能将该查询转换为一个常量;
3.eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于唯一索引或者主键扫描;
4.ref:非唯一性索引扫描,返回匹配某个单独值的所有行,本质上也是一种索引访问,它返回所有匹配某个单独值的行,可能会找多个符合条件的行,属于查找和扫描的混合体;
5.range:只检索给定范围的行,使用一个索引来选择行。key 列显示使用了哪个索引,一般就是 where 语句中出现了 between、in 等范围的查询。这种范围扫描索引扫描比全表扫描要好,因为它开始于索引的某一个点,而结束另一个点,不用全表扫描;
6.index:index 与 all 区别为 index 类型只遍历索引树。通常比 all 快,因为索引文件比数据文件小很多;
7.all:遍历全表以找到匹配的行;
注意:一般保证查询至少达到 range 级别,最好能达到 ref。
possible_keys 指出 MySQL 能使用哪个索引在该表中找到行。
key 显示 MySQL 实际决定使用的键(索引)。如果没有选择索引,键是 NULL。查询中如果使用覆盖索引,则该索引和查询的 select 字段重叠。
key_len 表示索引中使用的字节数,该列计算查询中使用的索引的长度在不损失精度的情况下,长度越短越好。如果键是 NULL,则长度为 NULL。该字段显示为索引字段的最大可能长度,并非实际使用长度。
ref 显示索引的哪一列被使用了,如果有可能是一个常数,哪些列或常量被用于查询索引列上的值。
rows 根据表统计信息以及索引选用情况,大致估算出找到所需的记录所需要读取的行数。
extra 包含不适合在其他列中显示,但是十分重要的额外信息
1.Using filesort:说明 mysql 会对数据适用一个外部的索引排序。而不是按照表内的索引顺序进行读取。MySQL 中无法利用索引完成排序操作称为“文件排序”;
2.Using temporary:使用了临时表保存中间结果,mysql 在查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by;
3.Using index:表示相应的 select 操作用使用覆盖索引,避免访问了表的数据行。如果同时出现 using where,表名索引被用来执行索引键值的查找;如果没有同时出现 using where,表名索引用来读取数据而非执行查询动作;
4.Using where:表明使用 where 过滤;
5.using join buffer:使用了连接缓存;
6.impossible where:where 子句的值总是 false,不能用来获取任何元组;
7.select tables optimized away:在没有 group by 子句的情况下,基于索引优化 min、max 操作或者对于 MyISAM 存储引擎优化 count(*),不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化;
8.distinct:优化 distinct 操作,在找到第一匹配的元组后即停止找同样值的动作。

extended 关键字

extended 关键字仅对 select 语句有效,在 Explain 后使用 extended 关键字,可以显示 filtered 列显示了通过条件过滤出的行数的百分比估计值。

filtered 列给出了一个百分比的值,这个百分比值和 rows 列的值一起使用,可以估计出那些将要和 explain 中的前一个表进行连接的行的数目,前一个表就是指 explain 的 id 列的值比当前表的 id 小的表。

SQL 执行顺序

(9) select   
(10) distinct column,  
(6) AGG_FUNC(column or expression), ...  
(1) from left_table  
(3) join right_table  
(2) on tablename.column = other_tablename.column  
(4) where constraint_expression  
(5) group by column  
(7) with cube|rollup  
(8) having constraint_expression  
(11) order by column asc|desc  
(12) limit count offset count;  

实际上 SQL 执行顺序:

  • 先执行 from,join 来确定表之间的连接关系,得到初步的数据;

  • where 对数据进行普通的初步的筛选;

  • group by 分组;

  • 各组分别执行 having 中的普通筛选或者聚合函数筛选;

  • 然后再根据我们要的数据进行 select,可以是普通字段查询也可以是获取聚合函数的查询结果,如果是集合函数,select 的查询结果会新增一条字段;

  • 将查询结果去重 distinct;

  • 最后合并各组的查询结果,按照 order by 的条件进行排序。

关键字的顺序是不能颠倒的:

SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ...  

SELECT 语句的执行顺序(在 MySQL 和 Oracle 中,SELECT 执行顺序基本相同):

FROM > WHERE > GROUP BY > HAVING > SELECT的字段 > DISTINCT > ORDER BY > LIMIT  

优化实践注意点

1、对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

2、应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:

select id from t where num is null;  

  • 最好不要给数据库留 NULL,尽可能的使用 NOT NULL 填充数据库;

  • 备注、描述、评论之类的可以设置为 NULL,其他的,最好不要使用 NULL;

  • 不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,空间就固定了, 不管是否插入值(NULL 也包含在内),都是占用 100 个字符的空间的,如果是 varchar 这样的变长字段,null 不占用空间;

  • 可以在 num 上设置默认值 0,确保表中 num 列没有 null 值。

3、应尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描。4.应尽量避免在 where 子句中使用 or 来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描,如:

select id from t where num=10 or Name = 'admin';  

可以这样查询:select id from t where num = 10  
union all  
select id from t where Name = 'admin'  

5、in 和 not in 也要慎用,否则会导致全表扫描,如:

select id from t where num in(1,2,3);  

对于连续的数值,能用 between 就不要用 in 了:

select id from t where num between 1 and 3;  

很多时候用 exists 代替 in 是一个好的选择:

select num from a where num in(select num from b);  

用下面的语句替换:

select num from a where exists(select 1 from b where num=a.num);  

6、下面的查询也将导致全表扫描:

select id from t where name like '%abc%';  

若要提高效率,可以考虑全文检索。

7、如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:

select id from t where num = @num;  

可以改为强制查询使用索引:

select id from t with(index(索引名)) where num = @num;  

应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:

select id from t where num/2 = 100;  

应改为:

select id from t where num = 100*2;  

9、应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:

select id from t where substring(name,1,3) = 'abc';      -– name 以abc 开头的 id  
select id from t where datediff(day,createdate,'2005-11-30') = 0;   -– '2005-11-30' 生成的id  

应改为:

select id from t where name like 'abc%';  
select id from t where createdate >= '2005-11-30' and createdate < '2005-12-1';  

10、不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。

11、在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。

12、不要写一些没有意义的查询,如需要生成一个空表结构:

select col1,col2 into #t from t where 1=0;  

这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:

create table #t(…);  

13、Update 语句,如果只更改 1、2 个字段,不要 Update 全部字段,否则频繁调用会引起明显的性能消耗,同时带来大量日志。

14、对于多张大数据量(这里几百条就算大了)的表 JOIN,要先分页再 JOIN,否则逻辑读会很高,性能很差。

15、select count(*) from table 这样不带任何条件的 count 会引起全表扫描,并且没有任何业务意义,是一定要杜绝的。

16、索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过 6 个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。

17、应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。

18、尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连 接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

19、尽可能的使用 varchar/nvarchar 代替 char/nchar,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

20、任何地方都不要使用 select * from t ,用具体的字段列表代替 “*” ,不要返回用不到的任何字段。

21、尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。

22、避免频繁创建和删除临时表,以减少系统表资源的消耗。临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件, 最好使用导出表。

23、在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后 insert。

24、如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table,然后 drop table,这样可以避免系统表的较长时间锁定。

25、尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过 1 万行,那么就应该考虑改写。

26、使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。

27、与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时 间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。

28、在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。

29、尽量避免大事务操作,提高系统并发能力。

30、尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。