1.1、强制执行
1.1.1、
【强制】不要使用 count(列名)或 count(常量)来替代 count(*),count(*)是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。
执行效果上:
- count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL
- count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL
- count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。
执行效率上:
- 列名为主键,count(列名)会比count(1)快
- 列名不为主键,count(1)会比count(列名)快
- 如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*)
- 如果有主键,则 select count(主键)的执行效率是最优的
- 如果表只有一个字段,则 select count(*)最优
1.1.2、
【强制】count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1, col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。
1.1.3、
【强制】禁止使用SELECT *,只获取必要的字段,需要显示说明列属性,表关联必须使用“表名.字段名”;
读取不需要的列会增加CPU、IO、NET消耗;
不能有效的利用覆盖索引;
使用SELECT *容易在增加或者删除字段后出现程序BUG;
1.1.4、
【强制】禁止在WHERE条件的属性上使用函数或者表达式;
举例:SELECT * from do_repair_form WHERE DATE_FORMAT(create_datetime,'%Y-%m-%d') BETWEEN '2020-03-01' AND '2020-03-15'
正确的写法是:
SELECT * from do_repair_form WHERE create_datetime BETWEEN '2020-03-01 00:00:00' AND '2020-03-15 23:59:59'
1.1.5、
【强制】当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为 NULL,因此使用 sum()时需注意 NPE 问题(NullPointerException 空指针,主要是对一个null的对象进行了操作)
正例:
使用如下方式来避免 sum 的 NPE 问题:
SELECT IFNULL(SUM(column), 0) FROM table;
1.1.6、
【强制】使用 IS NULL来判断是否为 NULL 值。
说明:NULL 与任何值的直接比较都为 NULL。
1) NULL<>NULL 的返回结果是 NULL,而不是 false。
2) NULL=NULL 的返回结果是 NULL,而不是 true。
3) NULL<>1 的返回结果是 NULL,而不是 true。
SELECT * from do_repair_form where overdue_status is null
1.1.7、
【强制】代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。
示例:
1.1.8、
【强制】数据订正(特别是删除、修改记录操作)时,要先 select,避免出现误删除,确认无误才能执行更新语句。
1.1.9、
【?】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
1.2、推荐执行
1.2.1、
【推荐】in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。
1.2.2、
【推荐】SQL语句大小写问题
(1)SQL语句中出现的所有表名、表别名、字段名、序列等数据库对象都应小写。
(2)SQL 语句中出现的系统保留字、内置函数名、SQL保留字、绑定变量等都应大写
示例:
SELECT
name,
password,
mobile
FROM
user
WHERE
id = **;
1.2.3、
【推荐】SQL语句的缩进
(1)如果一行有多列并超过80个字符,基于列对齐原则,应采用下行缩进
(2)缩进应为1个Tab或者4个字符
(3)同层次的SQL语句缩进应保持一致(纵向对齐)
(4)SELECT/FROM/WHERE/ORDER BY/GROUP BY等子句应独占一行
(5)SELECT子句内容如果只有一项,应与 SELECT 同占一行
(6)SELECT子句内容如果多于一项,每一项都应独占一行,并在对应 SELECT的基础上向右缩进2个Tab或者8个字符
(7)FROM子句内容如果只有一项,应与 FROM同占一行
(8)FROM子句内容如果多于一项,每一项都应独占一行,并在对应FROM的基础上向右缩进1个Tab或者4个字符
(9)WHERE子句内容如果只有一项,应与 WHERE同占一行
(10)WHERE子句的条件如果有多项,每一个条件应独占一行,并以AND开头,并在对应WHERE的基础上向右缩进1个Tab或者4个字符
(11)(UPDATE)SET子句内容如果有一项,应与 SET同占一行
(12)(UPDATE)SET子句内容如果有多项,每一项应独占一行,并在对应SET的基础上向右缩进1个Tab或者4个字符
(13) INSERT 子句左/右括号以及每个表字段应独占一行,其中括号无缩进,表字段在对应括号的基础上向右缩进1个Tab或者4个字符
(14) VALUES子句左/右括号以及每一项的值应独占一行,其中括号无缩进,每一项的值在对应括号的基础上向右缩进1个Tab或者4个字符
二、SQL调优指南
2.1、针对where
2.1.1、
对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
2.1.2、
应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
2.1.3、
应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
示例:
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0
2.1.4、
应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描
示例:
select id from t where num=10 or num=20
可以这样查询:
select id from t where num=10
union all
select id from t where num=20
2.1.5、
如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。
示例:
下面语句将进行全表扫描:
select id from t where num=@num
可以改为强制查询使用索引:
select id from t with(index(索引名)) where num=@num
2.1.6、
应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。
示例:
select id from t where num/2=100
应改为:
select id from t where num=100*2
2.1.7、
应尽量避免在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'
2.1.8、
不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
2.2、针对in
2.2.1、
in子查询优化
原始语句:
select a.depno,a.status from t_test_1 a where a.depno in (select b.depno from t_test_2 b where b.name='test')
一般功力稍微深厚一点的人,会将in改写成exists
select a.depno,a.status from t_test_1 a where exists (select 1 from t_test_2 b where a.depno=b.depno and b.name='test')
但是这种写法不是最优的写法,性能最高的写法是将in改成inner join
select a.depno,a.status from t_test_1 a inner join t_test_2 b on a.depno=b.depno where b.name='test'
select a.depno,a.status from t_test_1 a inner join (select depno from t_test2 where name='test') b on a.depno=b.depno
2.2.2、
in非子查询优化
有很多业务场景 ,例如要查询某些订单状态,就有如下sql语句
select a.orderid,a.status from t_order a where a.status in (3,4,5)
大家都知道sql语句走索引,可以用range scan方式去扫描索引,那可以将in改写成>=,<=
select a.orderid,a.status from t_order a where a.status >=3 and a.status <=5
还有一种方式,将in改写成=
select a.orderid,a.status from t_order a where a.status=3
union all
select a.orderid,a.status from t_order a where a.status=4
union all
select a.orderid,a.status from t_order a where a.status=5
这种写法之后,你的sql语句就会走索引,执行效率会提升很大,当然这种写法只适合某些特定的业务场景
2.2.3、
对于连续的数值,能用 between 就不要用 in 了
示例:
select id from t where num in(1,2,3)
对于连续的数值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3
2.3、针对like
2.3.1、
有一些业务场景需要用到like去做查询条件,例如
select a.orderid,a.status from t_order a where a.orderid like '%20190108%'
这个写法,在因为第一个字符是通配符,会不走索引,导致查询效率非常差,在这里强烈建议首字符,不要使用通配符
select a.orderid,a.status from t_order a where a.orderid like '20190108%'
2.4、其他
2.4.1、
在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
2.4.2、
索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
2.4.3、
尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
2.4.4、
任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
2.4.5、
is null优化
对于写sql还在用is null的同学,我无话可说,要不就是前人给你挖的坑,要不就是你自己给自己挖的坑,如果是前者还能理解,如果是后者,真的不值得同情。在此特别强调,在建表或者添加字段时,每个字段都必须有默认值,这样你写sql语句时,就不需要用is null这种写法,只要你用=做查询条件,就有办法去走索引,提升查询效率。
三、MySQL Explain详解
在日常工作中,我们会有时会开慢查询去记录一些执行时间比较久的SQL语句,找出这些SQL语句并不意味着完事了,些时我们常常用到explain这个命令来查看一个这些SQL语句的执行计划,查看该SQL语句有没有使用上了索引,有没有做全表扫描,这都可以通过explain命令来查看。所以我们深入了解MySQL的基于开销的优化器,还可以获得很多可能被优化器考虑到的访问策略的细节,以及当运行SQL语句时哪种策略预计会被优化器采用。
-- 实际SQL,查找用户名为Jefabc的员工select * from emp where name = 'Jefabc';-- 查看SQL是否使用索引,前面加上explain即可
explain select * from emp where name = 'Jefabc';
expain出来的信息有10列,分别是id、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra
概要描述:
- id:选择标识符
- select_type:表示查询的类型。
- table:输出结果集的表
- partitions:匹配的分区
- type:表示表的连接类型
- possible_keys:表示查询时,可能使用的索引
- key:表示实际使用的索引
- key_len:索引字段的长度
- ref:列与索引的比较
- rows:扫描出的行数(估算的行数)
- filtered:按表条件过滤的行百分比
- Extra:执行情况的描述和说明
下面对这些字段出现的可能进行解释:
一、 id
SELECT识别符。这是SELECT的查询序列号
我的理解是SQL执行的顺序的标识,SQL从大到小的执行
1. id相同时,执行顺序由上至下
2. 如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
3. id如果相同,可以认为是一组,从上往下顺序执行;在所有组中,id值越大,优先级越高,越先执行
-- 查看在研发部并且名字以Jef开头的员工,经典查询
explain select e.no, e.name from emp e left join dept d on e.dept_no = d.no where e.name like 'Jef%' and d.name = '研发部';
二、select_type
查询中每个select子句的类型
(1) SIMPLE(简单SELECT,不使用UNION或子查询等)
(2) PRIMARY(子查询中最外层查询,查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY)
(3) UNION(UNION中的第二个或后面的SELECT语句)
(4) DEPENDENT UNION(UNION中的第二个或后面的SELECT语句,取决于外面的查询)
(5) UNION RESULT(UNION的结果,union语句中第二个select开始后面所有select)
(6) SUBQUERY(子查询中的第一个SELECT,结果不依赖于外部查询)
(7) DEPENDENT SUBQUERY(子查询中的第一个SELECT,依赖于外部查询)
(8) DERIVED(派生表的SELECT, FROM子句的子查询)
(9) UNCACHEABLE SUBQUERY(一个子查询的结果不能被缓存,必须重新评估外链接的第一行)
三、table
显示这一步所访问数据库中表名称(显示这一行的数据是关于哪张表的),有时不是真实的表名字,可能是简称,例如上面的e,d,也可能是第几步执行的结果的简称
四、type
对表访问方式,表示MySQL在表中找到所需行的方式,又称“访问类型”。
常用的类型有: ALL、index、range、 ref、eq_ref、const、system、NULL(从左到右,性能从差到好)
ALL:Full Table Scan, MySQL将遍历全表以找到匹配的行
index: Full Index Scan,index与ALL区别为index类型只遍历索引树
range:只检索给定范围的行,使用一个索引来选择行
ref: 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
eq_ref: 类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件
const、system: 当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量,system是const类型的特例,当查询的表只有一行的情况下,使用system
NULL: MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。
五、possible_keys
指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用(该查询可以利用的索引,如果没有任何索引显示 null)
该列完全独立于EXPLAIN输出所示的表的次序。这意味着在possible_keys中的某些键实际上不能按生成的表次序使用。
如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查WHERE子句看是否它引用某些列或适合索引的列来提高你的查询性能。如果是这样,创造一个适当的索引并且再次用EXPLAIN检查查询
六、Key
key列显示MySQL实际决定使用的键(索引),必然包含在possible_keys中
如果没有选择索引,键是NULL。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。
七、key_len
表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的)
不损失精确性的情况下,长度越短越好
八、ref
列与索引的比较,表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
九、rows
估算出结果集行数,表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数
十、Extra
该列包含MySQL解决查询的详细信息,有以下几种情况:
Using where:不用读取表中所有信息,仅通过索引就可以获取所需数据,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示mysql服务器将在存储引擎检索行后再进行过滤
Using temporary:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询,常见 group by ; order by
Using filesort:当Query中包含 order by 操作,而且无法利用索引完成的排序操作称为“文件排序”
-- 测试Extra的filesort
explain select * from emp order by name;
Using join buffer:改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。
Impossible where:这个值强调了where语句会导致没有符合条件的行(通过收集统计信息不可能存在结果)。
Select tables optimized away:这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行
No tables used:Query语句中使用from dual 或不含任何from子句
-- explain select now() from dual;
总结:
- EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查的影响情况
- EXPLAIN不考虑各种Cache
- EXPLAIN不能显示MySQL在执行查询时所作的优化工作
- 部分统计信息是估算的,并非精确值
- EXPALIN只能解释SELECT操作,其他操作要重写为SELECT后查看执行计划。