本篇文章主要是通过数据库锁机制和索引来分析SQL语句执行速度慢的原因。
如果一条SQL语句绝大多数时候执行速度正常,偶尔执行慢。那么可能是因为产生了锁竞争,也可能是数据库为了保持数据一致性,在将数据从日志中刷新到磁盘上。
因为事务并发会带来脏读、修改丢失、不可重复读、幻读等问题,所以数据库需要使用锁机制保证数据一致性。数据库常用的锁级别有行级锁、页级锁、表级锁,因为Mysql默认的存储引擎InnoDB使用的是行级锁和表级锁,因此这里只对行级锁和表级锁做个简单介绍。行级锁是加到每一行的记录的索引之上或者加到记录索引的间隙里中(使得被锁包围的行记录可以改,但不能在这个范围里面增加行记录,可以防止新增幻影项)。表级锁是加到数据库表上。一个是对表加锁,一个是对某些行记录加锁,二者的竞争级别不可同日而语。
当数据库使用的默认存储引擎使用的是表级锁,由于锁的颗粒太大,虽然加锁很快,且不会出现死锁,但很容易产生锁冲突。当你查询表user,有并发事务正在对user表进行更新,会导致阻塞等待,增加查询时间。
当数据库使用的存储引擎使用的是行级锁,锁的颗粒较小,加锁慢,使用资源多,且可能出现死锁,但不容易产生锁冲突。当你查询表user的id>15,id<1500时。若有别的并发事务在修改这些行记录,则需要阻塞等待,查询时间增长。
当数据被修改或者有新数据插入表中时,是先在内存中进行插入或者修改数据,这些更新不会立马持久化到磁盘中,而是将这些更新的数据放入日志中,然后在空闲时,通过日志将数据持久化到磁盘中。但当频繁的对数据库表进行修改或者插入数据,而日志的容量又有一定的限度,因此容量快满时,数据库就需要停止其它操作,来进行数据持久化,这就会导致此时来数据库中进行查询的操作速度很慢。
其它进行数据持久化的情况:
1、数据库认为系统空闲的时候。
2、数据库正常关闭时,将数据同步回磁盘,保持数据一致性。
3、内存不够用时,需要替换内存中的某些数据页,此时就需要将某些被替换的脏数据页(数据不一致的数据页)持久化到磁盘中。
经常查询速度慢那肯定就是SQL语句本身需要优化。或者需要建立索引,或者调整SQL语句以命中索引,或者需要防止回表等。这部分文章会涉及覆盖索引、冗余索引、回表操作、最左前缀原则等热点索引知识,若不是很了解可以翻阅我的其它相关博文,例如《全面解析数据库索引(数据库索引种类大盘点)》
对于经常需要查询的字段或者是经常用来排序以及作为查询条件的字段需要在上面建立索引,通过索引进行查找速度会比全表扫描快的不要太多。这里没有进行详述索引的作用,若是有需要,可以翻阅我的其它相关博文。
1、若是使用联合索引,但没有注意联合索引的最左前缀原则,很容易导致索引未命中。(最左前缀原则不了解的话,可以看我的另外一篇博文:《全面解析数据库索引(数据库索引种类大盘点)》)
2、在索引字段上面使用了函数或者对等式左边的索引字段进行了运算,导致没有使用索引。
错误写法:
select name from user where id+500=1000;
select name from user where pow(id,4)=256;
正确写法:
select name from user where id=1000-500;
select name from user where id=4;
3、数据类型的隐式转换导致索引失效
select name,phone from customer where id = '111';
4、虽然字段上有索引,但数据库优化器选择错误,经过优化后选择不使用索引,而使用全表扫描,使得执行速度慢。
数据库优化器通过索引的区分度来确定索引是否值得被使用。一个索引上不同的值越多,意味着索引的区分度越高,索引也就越值得使用。数据库不可能通过遍历全表来进行区分度高低的计算,所以数据库采用的是采样确定区分度的方法,当采样无意中采样的大多是重复索引值数据,导致数据库误认为区分度很低,导致不选择索引,而是去走全表扫描,就会导致执行速度很低。
5、索引字段没有设置为 NOT NULL ,当引擎发现字段列的值为NULL,引擎放弃使用索引而进行全表扫描。
1、没有使用覆盖索引,导致需要回表操作以及随机I/O,使得执行速度变慢。
2、索引是冗余索引或者重复索引,导致增加了查询优化器生成执行计划的时间。
3、查询所有字段或者查询数据量过大,导致需要从磁盘读取大量数据到内存中,导致内存不足,需要进行数据页替换等操作,带来了额外的开销。
1、对于经常被查询或者用于条件语句、用来排序或者连表查询的字段,需要建立索引。
2、多建立联合索引而不是单列索引,多个单列搜索会比一个联合索引更浪费存储空间,而且联合索引在一定程度上可避免回表操作。
3、建立联合索引时,把区分度高的字段放在前边。将经常用来做范围查询的字段,放在索引的后面,因为联合索引在进行索引匹配时,一碰到像类似于where high>173 and high <183的范围查询就会导致索引停止命中,这就是最左匹配原则,不了解最左匹配原则的可以看我的另外一篇博文《全面解析数据库索引(数据库索引种类大盘点)》
。
4、充分使用表中的索引,避免使用双%号的查询条件,双%号的查询条件会导致索引失效,如 name like “%xiaohua%”,如果无前置%,只有后置%,是可以用到列上的索引的。
5、SQL语句尽量避免使用select * from 表名语句,而是需要哪些字段就查询哪些字段,避免消耗更多的 CPU 和 IO 以网络带宽资源,而且避免了无法使用覆盖索引。
6、使用联表查询而不是嵌套子查询,嵌套子查询性能低于联表查询,嵌套子查询的子查询生成的临时表不能使用索引,速度会很慢。
7、不要在SQL语句的 where 子句中对字段施加函数或者在等号左侧对字段进行运算,这会造成无法命中索引。
8、SQL语句应避免数据类型的隐式转换导致索引失效。
9、禁止使用不含字段列表的 INSERT 语句。
如:insert into t values (‘a’,‘b’,‘c’);
应使用:insert into t(c1,c2,c3) values (‘a’,‘b’,‘c’);
10、对同一列进行 or 判断时,使用 in 代替 or。in 操作可以更有效的利用索引,or 大多数情况下很少能利用到索引。
11、禁止使用 order by rand() 进行随机排序。order by rand() 会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值,如果满足条件的数据集非常大,就会消耗大量的 CPU 和 IO 及内存资源。推荐在程序中获取一个随机值,然后从数据库中获取数据的方式。
12、拆分复杂的大 SQL 为多个小 SQL。大 SQL 逻辑上比较复杂,需要占用大量 CPU 进行计算的 SQL,MySQL 中,一个 SQL 只能使用一个 CPU 进行计算
,SQL 拆分后可以通过并行执行来提高处理效率。
13、不使用NOT IN和<>操作。NOT IN和<>操作都不会使用索引而是进行全表扫描。NOT IN可以NOT EXISTS代替,id<>3则可使用id>3 or id<3来代替。