前言
SQL调优这块呢,大厂面试必问的。最近金九银十嘛,所以整理了SQL的调优思路,并且附几个经典案例分析。
1.慢SQL优化思路。
- 慢查询日志记录慢SQL
- explain分析SQL的执行计划
- profile 分析执行耗时
- Optimizer Trace分析详情
- 确定问题并采用相应的措施
1.1 慢查询日志记录慢SQL
如何定位慢SQL呢、我们可以通过慢查询日志来查看慢SQL。默认的情况下呢,MySQL数据库时不开启慢查询日志(slow query log)呢。所以我们需要手动把它打开。
查看下慢查询日志配置,我们可以使用show variables like 'slow_query_log%'命令,如下:
- slow query log表示慢查询开启的状态
- slow_query_log_file表示慢查询日志存放的位置
我们还可以使用show variables like 'long_query_time'命令,查看超过多少时间,才记录到慢查询日志,如下:
- long_query_time表示查询超过多少秒才记录到慢查询日志。
我们可以通过慢查日志,定位那些执行效率较低的SQL语句,重点关注分析。
1.2 explain查看分析SQL的执行计划
当定位出查询效率低的SQL后,可以使用explain查看SQL的执行计划。
当explain与SQL一起使用时,MySQL将显示来自优化器的有关语句执行计划的信息。即MySQL解释了它将如何处理该语句,包括有关如何连接表以及以何种顺序连接表等信息。
一条简单SQL,使用了explain的效果如下:
一般来说,我们需要重点关注type、rows、filtered、extra、key。
1.2.1 type
type表示连接类型,查看索引执行情况的一个重要指标。以下性能从好到坏依次:system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
- system:这种类型要求数据库表中只有一条数据,是const类型的一个特例,一般情况下是不会出现的。
- const:通过一次索引就能找到数据,一般用于主键或唯一索引作为条件,这类扫描效率极高,,速度非常快。
- eq_ref:常用于主键或唯一索引扫描,一般指使用主键的关联查询
- ref : 常用于非主键和唯一索引扫描。
- ref_or_null:这种连接类型类似于ref,区别在于MySQL会额外搜索包含NULL值的行
- index_merge:使用了索引合并优化方法,查询使用了两个以上的索引。
- unique_subquery:类似于eq_ref,条件用了in子查询
- index_subquery:区别于unique_subquery,用于非唯一索引,可以返回重复值。
- range:常用于范围查询,比如:between ... and 或 In 等操作
- index:全索引扫描
- ALL:全表扫描
1.2.2 rows
该列表示MySQL估算要找到我们所需的记录,需要读取的行数。对于InnoDB表,此数字是估计值,并非一定是个准确值。
1.2.3 filtered
该列是一个百分比的值,表里符合条件的记录数的百分比。简单点说,这个字段表示存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例。
1.2.4 extra
该字段包含有关MySQL如何解析查询的其他信息,它一般会出现这几个值:
- Using filesort:表示按文件排序,一般是在指定的排序和索引排序不一致的情况才会出现。一般见于order by语句
- Using index :表示是否用了覆盖索引。
- Using temporary: 表示是否使用了临时表,性能特别差,需要重点优化。一般多见于group by语句,或者union语句。
- Using where : 表示使用了where条件过滤.
- Using index condition:MySQL5.6之后新增的索引下推。在存储引擎层进行数据过滤,而不是在服务层过滤,利用索引现有的数据减少回表的数据。
1.2.5 key
该列表示实际用到的索引。一般配合possible_keys列一起看。
注意:有时候,explain配合show WARNINGS; (可以查看优化后,最终执行的sql),效果更佳哦。
1.3 profile 分析执行耗时
explain只是看到SQL的预估执行计划,如果要了解SQL真正的执行线程状态及消耗的时间,需要使用profiling。开启profiling参数后,后续执行的SQL语句都会记录其资源开销,包括IO,上下文切换,CPU,内存等等,我们可以根据这些开销进一步分析当前慢SQL的瓶颈再进一步进行优化。
profiling默认是关闭,我们可以使用show variables like '%profil%'查看是否开启,如下:
可以使用set profiling=ON开启。开启后,可以运行几条SQL,然后使用show profiles查看一下。
show profiles会显示最近发给服务器的多条语句,条数由变量profiling_history_size定义,默认是15。如果我们需要看单独某条SQL的分析,可以show profile查看最近一条SQL的分析。也可以使用show profile for query id(其中id就是show profiles中的QUERY_ID)查看具体一条的SQL语句分析。
除了查看profile ,还可以查看cpu和io,如上图。
1.4 Optimizer Trace分析详情
profile只能查看到SQL的执行耗时,但是无法看到SQL真正执行的过程信息,即不知道MySQL优化器是如何选择执行计划。这时候,我们可以使用Optimizer Trace,它可以跟踪执行语句的解析优化执行的全过程。
我们可以使用set optimizer_trace="enabled=on"打开开关,接着执行要跟踪的SQL,最后执行select * from
information_schema.optimizer_trace跟踪,如下:
大家可以查看分析其执行树,会包括三个阶段:
- join_preparation:准备阶段
- join_optimization:分析阶段
- join_execution:执行阶段
1.5 确定问题并采用相应的措施
最后确认问题,就采取对应的措施。
- 多数慢SQL都跟索引有关,比如不加索引,索引不生效、不合理等,这时候,我们可以优化索引。
- 我们还可以优化SQL语句,比如一些in元素过多问题(分批),深分页问题(基于上一次数据过滤等),进行时间分段查询
- SQl没办法很好优化,可以改用ES的方式,或者数仓。
- 如果单表数据量过大导致慢查询,则可以考虑分库分表
- 如果数据库在刷脏页导致慢查询,考虑是否可以优化一些参数,跟DBA讨论优化方案
- 如果存量数据量太大,考虑是否可以让部分数据归档
我之前写了一篇文章,有关于导致慢查询的12个原因,大家看一看一下哈:盘点MySQL慢查询的12个原因
2. 慢查询经典案例分析
2.1 案例1:隐式转换
我们创建一个用户user表
CREATE TABLE user (
id int(11) NOT NULL AUTO_INCREMENT,
userId varchar(32) NOT NULL,
age varchar(16) NOT NULL,
name varchar(255) NOT NULL,
PRIMARY KEY (id),
KEY idx_userid (userId) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
复制代码
userId字段为字串类型,是B+树的普通索引,如果查询条件传了一个数字过去,会导致索引失效。如下:
如果给数字加上'',也就是说,传的是一个字符串呢,当然是走索引,如下图:
为什么第一条语句未加单引号就不走索引了呢?这是因为不加单引号时,是字符串跟数字的比较,它们类型不匹配,MySQL会做隐式的类型转换,把它们转换为浮点数再做比较。隐式的类型转换,索引会失效。
2.2 案例2:最左匹配
MySQl建立联合索引时,会遵循最左前缀匹配的原则,即最左优先。如果你建立一个(a,b,c)的联合索引,相当于建立了(a)、(a,b)、(a,b,c)三个索引。
假设有以下表结构:
CREATE TABLE user (
id int(11) NOT NULL AUTO_INCREMENT,
user_id varchar(32) NOT NULL,
age varchar(16) NOT NULL,
name varchar(255) NOT NULL,
PRIMARY KEY (id),
KEY idx_userid_name (user_id,name) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
复制代码
假设有一个联合索引idx_userid_name,我们现在执行以下SQL,如果查询列是name,索引是无效的:
explain select * from user where name ='捡田螺的小男孩';
复制代码
因为查询条件列name不是联合索引idx_userid_name中的第一个列,不满足最左匹配原则,所以索引不生效。在联合索引中,只有查询条件满足最左匹配原则时,索引才正常生效。如下,查询条件列是user_id
2.3 案例3:深分页问题
limit深分页问题,会导致慢查询,应该大家都司空见惯了吧。
limit深分页为什么会变慢呢? 假设有表结构如下:
CREATE TABLE account (
id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键Id',
name varchar(255) DEFAULT NULL COMMENT '账户名',
balance int(11) DEFAULT NULL COMMENT '余额',
create_time datetime NOT NULL COMMENT '创建时间',
update_time datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
KEY idx_name (name),
KEY idx_create_time (create_time) //索引
) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8 ROW_FORMAT=REDUNDANT COMMENT='账户表';
复制代码
以下这个SQL,你知道执行过程是怎样的呢?
select id,name,balance from account where create_time> '2020-09-19' limit 100000,10;
复制代码
这个SQL的执行流程酱紫:
- 通过普通二级索引树idx_create_time,过滤create_time条件,找到满足条件的主键id。
- 通过主键id,回到id主键索引树,找到满足记录的行,然后取出需要展示的列(回表过程)
- 扫描满足条件的100010行,然后扔掉前100000行,返回。
因此,limit深分页,导致SQL变慢原因有两个:
- limit语句会先扫描offset+n行,然后再丢弃掉前offset行,返回后n行数据。也就是说limit 100000,10,就会扫描100010行,而limit 0,10,只扫描10行。
- limit 100000,10 扫描更多的行数,也意味着回表更多的次数。
如何优化深分页问题?
我们可以通过减少回表次数来优化。一般有标签记录法和延迟关联法。
标签记录法
就是标记一下上次查询到哪一条了,下次再来查的时候,从该条开始往下扫描。就好像看书一样,上次看到哪里了,你就折叠一下或者夹个书签,下次来看的时候,直接就翻到啦。
假设上一次记录到100000,则SQL可以修改为:
select id,name,balance FROM account where id > 100000 limit 10;
复制代码
这样的话,后面无论翻多少页,性能都会不错的,因为命中了id索引。但是这种方式有局限性:需要一种类似连续自增的字段。
延迟关联法
延迟关联法,就是把条件转移到主键索引树,然后减少回表。如下
select acct1.id,acct1.name,acct1.balance FROM account acct1 INNER JOIN (SELECT a.id FROM account a WHERE a.create_time > '2020-09-19' limit 100000, 10) AS acct2 on acct1.id= acct2.id;
复制代码
优化思路就是,先通过idx_create_time二级索引树查询到满足条件的主键ID,再与原表通过主键ID内连接,这样后面直接走了主键索引了,同时也减少了回表。
2.4 案例4: in元素过多
如果使用了in,即使后面的条件加了索引,还是要注意in后面的元素不要过多哈。in元素一般建议不要超过200个,如果超过了,建议分组,每次200一组进行哈。
反例:
select user_id,name from user where user_id in (1,2,3...1000000);
复制代码
如果我们对in的条件不做任何限制的话,该查询语句一次性可能会查询出非常多的数据,很容易导致接口超时。尤其有时候,我们是用的子查询,in后面的子查询,你都不知道数量有多少那种,更容易采坑.如下这种子查询:
select * from user where user_id in (select author_id from artilce where type = 1);
复制代码
如果type = 1有1一千,甚至上万个呢?肯定是慢SQL。索引一般建议分批进行,一次200个,比如:
select user_id,name from user where user_id in (1,2,3...200);
复制代码
in查询为什么慢呢?
这是因为in查询在MySQL底层是通过n*m的方式去搜索,类似union。
in查询在进行cost代价计算时(代价 = 元组数 * IO平均值),是通过将in包含的数值,一条条去查询获取元组数的,因此这个计算过程会比较的慢,所以MySQL设置了个临界值(eq_range_index_dive_limit),5.6之后超过这个临界值后该列的cost就不参与计算了。因此会导致执行计划选择不准确。默认是200,即in条件超过了200个数据,会导致in的代价计算存在问题,可能会导致Mysql选择的索引不准确。
2.5 order by 走文件排序导致的慢查询
如果order by 使用到文件排序,则会可能会产生慢查询。我们来看下下面这个SQL:
select name,age,city from staff where city = '深圳' order by age limit 10;
复制代码
它表示的意思就是:查询前10个,来自深圳员工的姓名、年龄、城市,并且按照年龄小到大排序。
查看explain执行计划的时候,可以看到Extra这一列,有一个Using filesort,它表示用到文件排序。
order by文件排序效率为什么较低
大家可以看下这个下面这个图:
order by排序,分为全字段排序和rowid排序。它是拿max_length_for_sort_data和结果行数据长度对比,如果结果行数据长度超过max_length_for_sort_data这个值,就会走rowid排序,相反,则走全字段排序。
2.5.1 rowid排序
rowid排序,一般需要回表去找满足条件的数据,所以效率会慢一点。以下这个SQL,使用rowid排序,执行过程是这样:
select name,age,city from staff where city = '深圳' order by age limit 10;
复制代码
- MySQL为对应的线程初始化sort_buffer,放入需要排序的age字段,以及主键id;
- 从索引树idx_city, 找到第一个满足 city='深圳’条件的主键id,假设id为X;
- 到主键id索引树拿到id=X的这一行数据, 取age和主键id的值,存到sort_buffer;
- 从索引树idx_city拿到下一个记录的主键id,假设id=Y;
- 重复步骤 3、4 直到city的值不等于深圳为止;
- 前面5步已经查找到了所有city为深圳的数据,在sort_buffer中,将所有数据根据age进行排序; 遍历排序结果,取前10行,并按照id的值回到原表中,取出city、name 和 age三个字段返回给客户端。
2.5.2 全字段排序
同样的SQL,如果是走全字段排序是这样的:
select name,age,city from staff where city = '深圳' order by age limit 10;
复制代码
- MySQL 为对应的线程初始化sort_buffer,放入需要查询的name、age、city字段;
- 从索引树idx_city, 找到第一个满足 city='深圳’条件的主键 id,假设找到id=X;
- 到主键id索引树拿到id=X的这一行数据, 取name、age、city三个字段的值,存到sort_buffer;
- 从索引树idx_city 拿到下一个记录的主键id,假设id=13;
- 重复步骤 3、4 直到city的值不等于深圳为止;
- 前面5步已经查找到了所有city为深圳的数据,在sort_buffer中,将所有数据根据age进行排序;
- 按照排序结果取前10行返回给客户端。
sort_buffer的大小是由一个参数控制的:sort_buffer_size。
- 如果要排序的数据小于sort_buffer_size,排序在sort_buffer内存中完成
- 如果要排序的数据大于sort_buffer_size,则借助磁盘文件来进行排序。
借助磁盘文件排序的话,效率就更慢一点。因为先把数据放入sort_buffer,当快要满时。会排一下序,然后把sort_buffer中的数据,放到临时磁盘文件,等到所有满足条件数据都查完排完,再用归并算法把磁盘的临时排好序的小文件,合并成一个有序的大文件。
2.5.3 如何优化order by的文件排序
order by使用文件排序,效率会低一点。我们怎么优化呢?
- 因为数据是无序的,所以就需要排序。如果数据本身是有序的,那就不会再用到文件排序啦。而索引数据本身是有序的,我们通过建立索引来优化order by语句。
- 我们还可以通过调整max_length_for_sort_data、sort_buffer_size等参数优化;
2.6 索引字段上使用(!= 或者 < >),索引可能失效
假设有表结构:
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userId` int(11) NOT NULL,
`age` int(11) DEFAULT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_age` (`age`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
复制代码
虽然age加了索引,但是使用了!= 或者< >,not in这些时,索引如同虚设。如下:
其实这个也是跟mySQL优化器有关,如果优化器觉得即使走了索引,还是需要扫描很多很多行的哈,它觉得不划算,不如直接不走索引。平时我们用!= 或者< >,not in的时候,留点心眼哈。
2.7 索引字段上使用is null, is not null,索引可能失效
表结构:
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`card` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE,
KEY `idx_card` (`card`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
复制代码
单个name字段加上索引,并查询name为非空的语句,其实会走索引的,如下:
单个card字段加上索引,并查询name为非空的语句,其实会走索引的,如下:
但是它两用or连接起来,索引就失效了,如下:
很多时候,也是因为数据量问题,导致了MySQL优化器放弃走索引。同时,平时我们用explain分析SQL的时候,如果type=range,要注意一下哈,因为这个可能因为数据量问题,导致索引无效。
2.8 左右连接,关联的字段编码格式不一样
新建两个表,一个user,一个user_job
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL,
`age` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
CREATE TABLE `user_job` (
`id` int(11) NOT NULL,
`userId` int(11) NOT NULL,
`job` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
复制代码
user表的name字段编码是utf8mb4,而user_job表的name字段编码为utf8。
执行左外连接查询,user_job表还是走全表扫描,如下:
如果把它们的name字段改为编码一致,相同的SQL,还是会走索引。
2.9 group by使用临时表
group by一般用于分组统计,它表达的逻辑就是根据一定的规则,进行分组。日常开发中,我们使用得比较频繁。如果不注意,很容易产生慢SQL。
2.9.1 group by执行流程
假设有表结构:
CREATE TABLE `staff` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`id_card` varchar(20) NOT NULL COMMENT '身份证号码',
`name` varchar(64) NOT NULL COMMENT '姓名',
`age` int(4) NOT NULL COMMENT '年龄',
`city` varchar(64) NOT NULL COMMENT '城市',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8 COMMENT='员工表';
复制代码
我们查看一下这个SQL的执行计划:
explain select city ,count(*) as num from staff group by city;
复制代码
- Extra 这个字段的Using temporary表示在执行分组的时候使用了临时表
- Extra 这个字段的Using filesort表示使用了文件排序
group by是怎么使用到临时表和排序了呢?我们来看下这个SQL的执行流程
select city ,count(*) as num from staff group by city;
复制代码
- 创建内存临时表,表里有两个字段city和num;
- 全表扫描staff的记录,依次取出city = 'X'的记录。
- 判断临时表中是否有为city='X'的行,没有就插入一个记录 (X,1);
- 如果临时表中有city='X'的行,就将X这一行的num值加 1;
- 遍历完成后,再根据字段city做排序,得到结果集返回给客户端。这个流程的执行图如下:
临时表的排序是怎样的呢?
就是把需要排序的字段,放到sort buffer,排完就返回。在这里注意一点哈,排序分全字段排序和rowid排序
- 如果是全字段排序,需要查询返回的字段,都放入sort buffer,根据排序字段排完,直接返回
- 如果是rowid排序,只是需要排序的字段放入sort buffer,然后多一次回表操作,再返回。
2.9.2 group by可能会慢在哪里?
group by使用不当,很容易就会产生慢SQL问题。因为它既用到临时表,又默认用到排序。有时候还可能用到磁盘临时表。
- 如果执行过程中,会发现内存临时表大小到达了上限(控制这个上限的参数就是tmp_table_size),会把内存临时表转成磁盘临时表。
- 如果数据量很大,很可能这个查询需要的磁盘临时表,就会占用大量的磁盘空间。
2.9.3 如何优化group by呢
从哪些方向去优化呢?
- 方向1:既然它默认会排序,我们不给它排是不是就行啦。
- 方向2:既然临时表是影响group by性能的X因素,我们是不是可以不用临时表?
我们一起来想下,执行group by语句为什么需要临时表呢?group by的语义逻辑,就是统计不同的值出现的个数。如果这个这些值一开始就是有序的,我们是不是直接往下扫描统计就好了,就不用临时表来记录并统计结果啦?
可以有这些优化方案:
- group by 后面的字段加索引
- order by null 不用排序
- 尽量只使用内存临时表
- 使用SQL_BIG_RESULT
2.10 delete + in子查询不走索引!
之前见到过一个生产慢SQL问题,当delete遇到in子查询时,即使有索引,也是不走索引的。而对应的select + in子查询,却可以走索引。
MySQL版本是5.7,假设当前有两张表account和old_account,表结构如下:
CREATE TABLE `old_account` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`name` varchar(255) DEFAULT NULL COMMENT '账户名',
`balance` int(11) DEFAULT NULL COMMENT '余额',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8 ROW_FORMAT=REDUNDANT COMMENT='老的账户表';
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`name` varchar(255) DEFAULT NULL COMMENT '账户名',
`balance` int(11) DEFAULT NULL COMMENT '余额',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8 ROW_FORMAT=REDUNDANT COMMENT='账户表';
复制代码
执行的SQL如下:
delete from account where name in (select name from old_account);
复制代码
查看执行计划,发现不走索引:
但是如果把delete换成select,就会走索引。如下:
为什么select + in子查询会走索引,delete + in子查询却不会走索引呢?
我们执行以下SQL看看:
explain select * from account where name in (select name from old_account);
show WARNINGS; //可以查看优化后,最终执行的sql
复制代码
结果如下:
select `test2`.`account`.`id` AS `id`,`test2`.`account`.`name` AS `name`,`test2`.`account`.`balance` AS `balance`,`test2`.`account`.`create_time` AS `create_time`,`test2`.`account`.`update_time` AS `update_time` from `test2`.`account`
semi join (`test2`.`old_account`)
where (`test2`.`account`.`name` = `test2`.`old_account`.`name`)
复制代码
可以发现,实际执行的时候,MySQL对select in子查询做了优化,把子查询改成join的方式,所以可以走索引。但是很遗憾,对于delete in子查询,MySQL却没有对它做这个优化。
日常开发中,大家注意一下这个场景哈
52条SQL语句性能优化策略,建议收藏
1、对查询进行优化,应尽量避免全表扫描,首先应考虑在 WHERE 及 ORDER BY 涉及的列上建立索引。
2、应尽量避免在 WHERE 子句中对字段进行 NULL 值判断,创建表时 NULL 是默认值,但大多数时候应该使用 NOT NULL,或者使用一个特殊的值,如 0,-1 作为默认值。
3、应尽量避免在 WHERE 子句中使用 != 或 <> 操作符。MySQL 只有对以下操作符才使用索引:<,<=,=,>,>=,BETWEEN,IN,以及某些时候的 LIKE。
4、应尽量避免在 WHERE 子句中使用 OR 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,可以使用 UNION 合并查询:select id from t where num=10 union all select id from t where num=20。
5、IN 和 NOT IN 也要慎用,否则会导致全表扫描。对于连续的数值,能用 BETWEEN 就不要用 IN:select id from t where num between 1 and 3。
6、下面的查询也将导致全表扫描:select id from t where name like‘%abc%’ 或者select id from t where name like‘%abc’若要提高效率,可以考虑全文检索。而select id from t where name like‘abc%’才用到索引。
7、如果在 WHERE 子句中使用参数,也会导致全表扫描。
8、应尽量避免在 WHERE 子句中对字段进行表达式操作,应尽量避免在 WHERE 子句中对字段进行函数操作。
9、很多时候用 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)。
10、索引固然可以提高相应的 SELECT 的效率,但同时也降低了 INSERT 及 UPDATE 的效。因为 INSERT 或 UPDATE 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过 6 个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
11、应尽可能的避免更新 clustered 索引数据列, 因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。
12、尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。
13、尽可能的使用 varchar, nvarchar 代替 char, nchar。因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
14、最好不要使用返回所有:select from t ,用具体的字段列表代替 “*”,不要返回用不到的任何字段。
15、尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
16、使用表的别名(Alias):当在 SQL 语句中连接多个表时,请使用表的别名并把别名前缀于每个 Column 上。这样一来,就可以减少解析的时间并减少那些由 Column 歧义引起的语法错误。
17、使用“临时表”暂存中间结果 :
简化 SQL 语句的重要方法就是采用临时表暂存中间结果。但是临时表的好处远远不止这些,将临时结果暂存在临时表,后面的查询就在 tempdb 中了,这可以避免程序中多次扫描主表,也大大减少了程序执行中“共享锁”阻塞“更新锁”,减少了阻塞,提高了并发性能。
18、一些 SQL 查询语句应加上 nolock,读、写是会相互阻塞的,为了提高并发性能。对于一些查询,可以加上 nolock,这样读的时候可以允许写,但缺点是可能读到未提交的脏数据。
使用 nolock 有3条原则:
- 查询的结果用于“插、删、改”的不能加 nolock;
- 查询的表属于频繁发生页分裂的,慎用 nolock ;
- 使用临时表一样可以保存“数据前影”,起到类似 Oracle 的 undo 表空间的功能,能采用临时表提高并发性能的,不要用 nolock。
19、常见的简化规则如下:
不要有超过 5 个以上的表连接(JOIN),考虑使用临时表或表变量存放中间结果。少用子查询,视图嵌套不要过深,一般视图嵌套不要超过 2 个为宜。
20、将需要查询的结果预先计算好放在表中,查询的时候再Select。这在SQL7.0以前是最重要的手段,例如医院的住院费计算。
21、用 OR 的字句可以分解成多个查询,并且通过 UNION 连接多个查询。他们的速度只同是否使用索引有关,如果查询需要用到联合索引,用 UNION all 执行的效率更高。多个 OR 的字句没有用到索引,改写成 UNION 的形式再试图与索引匹配。一个关键的问题是否用到索引。
22、在IN后面值的列表中,将出现最频繁的值放在最前面,出现得最少的放在最后面,减少判断的次数。
23、尽量将数据的处理工作放在服务器上,减少网络的开销,如使用存储过程。
存储过程是编译好、优化过、并且被组织到一个执行规划里、且存储在数据库中的 SQL 语句,是控制流语言的集合,速度当然快。反复执行的动态 SQL,可以使用临时存储过程,该过程(临时表)被放在 Tempdb 中。
24、当服务器的内存够多时,配制线程数量 = 最大连接数+5,这样能发挥最大的效率;否则使用配制线程数量< 最大连接数,启用 SQL SERVER 的线程池来解决,如果还是数量 = 最大连接数+5,严重的损害服务器的性能。
25、查询的关联同写的顺序 :
select a.personMemberID, * from chineseresume a,personmember b where personMemberID = b.referenceid and a.personMemberID = 'JCNPRH39681' (A = B, B = '号码') select a.personMemberID, * from chineseresume a,personmember b where a.personMemberID = b.referenceid and a.personMemberID = 'JCNPRH39681' and b.referenceid = 'JCNPRH39681' (A = B, B = '号码', A = '号码') select a.personMemberID, * from chineseresume a,personmember b where b.referenceid = 'JCNPRH39681' and a.personMemberID = 'JCNPRH39681' (B = '号码', A = '号码')
复制代码
26、尽量使用 EXISTS 代替 select count(1) 来判断是否存在记录。count 函数只有在统计表中所有行数时使用,而且 count(1) 比 count(*) 更有效率。
27、尽量使用 “>=”,不要使用 “>”。
28、索引的使用规范:
- 索引的创建要与应用结合考虑,建议大的 OLTP 表不要超过 6 个索引;
- 尽可能的使用索引字段作为查询条件,尤其是聚簇索引,必要时可以通过 index index_name 来强制指定索引;
- 避免对大表查询时进行 table scan,必要时考虑新建索引;
- 在使用索引字段作为条件时,如果该索引是联合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用;
- 要注意索引的维护,周期性重建索引,重新编译存储过程。
29、下列 SQL 条件语句中的列都建有恰当的索引,但执行速度却非常慢:
SELECT * FROM record WHERE substrINg(card_no, 1, 4) = '5378' --13秒 SELECT * FROM record WHERE amount/30 < 1000 --11秒 SELECT * FROM record WHERE convert(char(10), date, 112) = '19991201' --10秒
复制代码
分析:
WHERE 子句中对列的任何操作结果都是在 SQL 运行时逐列计算得到的,因此它不得不进行表搜索,而没有使用该列上面的索引。
如果这些结果在查询编译时就能得到,那么就可以被 SQL 优化器优化,使用索引,避免表搜索,因此将 SQL 重写成下面这样:
SELECT * FROM record WHERE card_no like '5378%' -- < 1秒 SELECT * FROM record WHERE amount < 1000*30 -- < 1秒 SELECT * FROM record WHERE date = '1999/12/01' -- < 1秒
复制代码
30、当有一批处理的插入或更新时,用批量插入或批量更新,绝不会一条条记录的去更新。
31、在所有的存储过程中,能够用 SQL 语句的,我绝不会用循环去实现。
例如:列出上个月的每一天,我会用 connect by 去递归查询一下,绝不会去用循环从上个月第一天到最后一天。
32、选择最有效率的表名顺序(只在基于规则的优化器中有效):
Oracle 的解析器按照从右到左的顺序处理 FROM 子句中的表名,FROM 子句中写在最后的表(基础表 driving table)将被最先处理,在 FROM 子句中包含多个表的情况下,你必须选择记录条数最少的表作为基础表。
如果有 3 个以上的表连接查询,那就需要选择交叉表(intersection table)作为基础表,交叉表是指那个被其他表所引用的表。
33、提高 GROUP BY 语句的效率,可以通过将不需要的记录在 GROUP BY 之前过滤掉。下面两个查询返回相同结果,但第二个明显就快了许多。
低效:
SELECT JOB, AVG(SAL) FROM EMP GROUP BY JOB HAVING JOB = 'PRESIDENT' OR JOB = 'MANAGER'
复制代码
高效:
SELECT JOB, AVG(SAL) FROM EMPWHERE JOB = 'PRESIDENT' OR JOB = 'MANAGER' GROUP BY JOB
复制代码
34、SQL 语句用大写,因为 Oracle 总是先解析 SQL 语句,把小写的字母转换成大写的再执行。
35、别名的使用,别名是大型数据库的应用技巧,就是表名、列名在查询中以一个字母为别名,查询速度要比建连接表快 1.5 倍。
36、避免死锁,在你的存储过程和触发器中访问同一个表时总是以相同的顺序;事务应经可能地缩短,在一个事务中应尽可能减少涉及到的数据量;永远不要在事务中等待用户输入。
37、避免使用临时表,除非却有需要,否则应尽量避免使用临时表,相反,可以使用表变量代替。大多数时候(99%),表变量驻扎在内存中,因此速度比临时表更快,临时表驻扎在 TempDb 数据库中,因此临时表上的操作需要跨数据库通信,速度自然慢。
38、最好不要使用触发器:
- 触发一个触发器,执行一个触发器事件本身就是一个耗费资源的过程;
- 如果能够使用约束实现的,尽量不要使用触发器;
- 不要为不同的触发事件(Insert、Update 和 Delete)使用相同的触发器;
- 不要在触发器中使用事务型代码。
39、索引创建规则:
- 表的主键、外键必须有索引;
- 数据量超过 300 的表应该有索引;
- 经常与其他表进行连接的表,在连接字段上应该建立索引;
- 经常出现在 WHERE 子句中的字段,特别是大表的字段,应该建立索引;
- 索引应该建在选择性高的字段上;
- 索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
- 复合索引的建立需要进行仔细分析,尽量考虑用单字段索引代替;
- 正确选择复合索引中的主列字段,一般是选择性较好的字段;
- 复合索引的几个字段是否经常同时以 AND 方式出现在 WHERE 子句中?单字段查询是否极少甚至没有?如果是,则可以建立复合索引;否则考虑单字段索引;
- 如果复合索引中包含的字段经常单独出现在 WHERE 子句中,则分解为多个单字段索引;
- 如果复合索引所包含的字段超过 3 个,那么仔细考虑其必要性,考虑减少复合的字段;
- 如果既有单字段索引,又有这几个字段上的复合索引,一般可以删除复合索引;
- 频繁进行数据操作的表,不要建立太多的索引;
- 删除无用的索引,避免对执行计划造成负面影响;
- 表上建立的每个索引都会增加存储开销,索引对于插入、删除、更新操作也会增加处理上的开销。另外,过多的复合索引,在有单字段索引的情况下,一般都是没有存在价值的;相反,还会降低数据增加删除时的性能,特别是对频繁更新的表来说,负面影响更大。
- 尽量不要对数据库中某个含有大量重复的值的字段建立索引。
40、MySQL 查询优化总结:
使用慢查询日志去发现慢查询,使用执行计划去判断查询是否正常运行,总是去测试你的查询看看是否他们运行在最佳状态下。
久而久之性能总会变化,避免在整个表上使用 count(*),它可能锁住整张表,使查询保持一致以便后续相似的查询可以使用查询缓存,在适当的情形下使用 GROUP BY 而不是 DISTINCT,在 WHERE、GROUP BY 和 ORDER BY 子句中使用有索引的列,保持索引简单,不在多个索引中包含同一个列。
有时候 MySQL 会使用错误的索引,对于这种情况使用 USE INDEX,检查使用 SQL_MODE=STRICT 的问题,对于记录数小于5的索引字段,在 UNION 的时候使用LIMIT不是是用OR。
为了避免在更新前 SELECT,使用 INSERT ON DUPLICATE KEY 或者 INSERT IGNORE;不要用 UPDATE 去实现,不要使用 MAX;使用索引字段和 ORDER BY子句 LIMIT M,N 实际上可以减缓查询在某些情况下,有节制地使用,在 WHERE 子句中使用 UNION 代替子查询,在重新启动的 MySQL,记得来温暖你的数据库,以确保数据在内存和查询速度快,考虑持久连接,而不是多个连接,以减少开销。
基准查询,包括使用服务器上的负载,有时一个简单的查询可以影响其他查询,当负载增加在服务器上,使用 SHOW PROCESSLIST 查看慢的和有问题的查询,在开发环境中产生的镜像数据中测试的所有可疑的查询。
41、MySQL 备份过程:
- 从二级复制服务器上进行备份;
- 在进行备份期间停止复制,以避免在数据依赖和外键约束上出现不一致;
- 彻底停止 MySQL,从数据库文件进行备份;
- 如果使用 MySQL dump 进行备份,请同时备份二进制日志文件 – 确保复制没有中断;
- 不要信任 LVM 快照,这很可能产生数据不一致,将来会给你带来麻烦;
- 为了更容易进行单表恢复,以表为单位导出数据——如果数据是与其他表隔离的。
- 当使用 mysqldump 时请使用 –opt;
- 在备份之前检查和优化表;
- 为了更快的进行导入,在导入时临时禁用外键约束。;
- 为了更快的进行导入,在导入时临时禁用唯一性检测;
- 在每一次备份后计算数据库,表以及索引的尺寸,以便更够监控数据尺寸的增长;
- 通过自动调度脚本监控复制实例的错误和延迟;
- 定期执行备份。
42、查询缓冲并不自动处理空格,因此,在写 SQL 语句时,应尽量减少空格的使用,尤其是在 SQL 首和尾的空格(因为查询缓冲并不自动截取首尾空格)。
43、member 用 mid 做标准进行分表方便查询么?一般的业务需求中基本上都是以 username 为查询依据,正常应当是 username 做 hash 取模来分表。
而分表的话 MySQL 的 partition 功能就是干这个的,对代码是透明的;在代码层面去实现貌似是不合理的。
44、我们应该为数据库里的每张表都设置一个 ID 做为其主键,而且最好的是一个 INT 型的(推荐使用 UNSIGNED),并设置上自动增加的 AUTO_INCREMENT 标志。
45、在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON,在结束时设置 SET NOCOUNT OFF。无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。
46、MySQL 查询可以启用高速查询缓存。这是提高数据库性能的有效MySQL优化方法之一。当同一个查询被执行多次时,从缓存中提取数据和直接从数据库中返回数据快很多。
47、EXPLAIN SELECT 查询用来跟踪查看效果:
使用 EXPLAIN 关键字可以让你知道 MySQL 是如何处理你的 SQL 语句的。这可以帮你分析你的查询语句或是表结构的性能瓶颈。EXPLAIN 的查询结果还会告诉你你的索引主键被如何利用的,你的数据表是如何被搜索和排序的。
48、当只要一行数据时使用 LIMIT 1 :
当你查询表的有些时候,你已经知道结果只会有一条结果,但因为你可能需要去fetch游标,或是你也许会去检查返回的记录数。
在这种情况下,加上 LIMIT 1 可以增加性能。这样一来,MySQL 数据库引擎会在找到一条数据后停止搜索,而不是继续往后查少下一条符合记录的数据。
49、选择表合适存储引擎:
- **myisam:**应用时以读和插入操作为主,只有少量的更新和删除,并且对事务的完整性,并发性要求不是很高的。
- **InnoDB:**事务处理,以及并发条件下要求数据的一致性。除了插入和查询外,包括很多的更新和删除。(InnoDB 有效地降低删除和更新导致的锁定)。
- 对于支持事务的 InnoDB类 型的表来说,影响速度的主要原因是 AUTOCOMMIT 默认设置是打开的,而且程序没有显式调用 BEGIN 开始事务,导致每插入一条都自动提交,严重影响了速度。可以在执行 SQL 前调用 begin,多条 SQL 形成一个事物(即使 autocommit 打开也可以),将大大提高性能。
50、优化表的数据类型,选择合适的数据类型:
**原则:**更小通常更好,简单就好,所有字段都得有默认值,尽量避免 NULL。
例如:数据库表设计时候更小的占磁盘空间尽可能使用更小的整数类型。(mediumint 就比 int 更合适)
比如时间字段:datetime 和 timestamp。datetime 占用8个字节,timestamp 占用4个字节,只用了一半。而 timestamp 表示的范围是 1970—2037 适合做更新时间。
MySQL可以很好的支持大数据量的存取,但是一般说来,数据库中的表越小,在它上面执行的查询也就会越快。
因此,在创建表的时候,为了获得更好的性能,我们可以将表中字段的宽度设得尽可能小。
例如:在定义邮政编码这个字段时,如果将其设置为 CHAR(255),显然给数据库增加了不必要的空间。甚至使用VARCHAR 这种类型也是多余的,因为 CHAR(6) 就可以很好的完成任务了。
同样的,如果可以的话,我们应该使用 MEDIUMINT 而不是 BIGIN 来定义整型字段,应该尽量把字段设置为 NOT NULL,这样在将来执行查询的时候,数据库不用去比较 NULL 值。
对于某些文本字段,例如“省份”或者“性别”,我们可以将它们定义为 ENUM 类型。因为在 MySQL 中,ENUM 类型被当作数值型数据来处理,而数值型数据被处理起来的速度要比文本类型快得多。这样,我们又可以提高数据库的性能。
51、字符串数据类型:char, varchar, text 选择区别。
52、任何对列的操作都将导致表扫描,它包括数据库函数、计算表达式等等,查询时要尽可能将操作移至等号右边。