劣质SQl优化

写在开头

这里所说的劣质SQL限定在数据量未到分库分表和使用分布式缓存程度,指那些执行较差的查询、插入、更新、删除SQL。
本文从执行计划分析慢SQL,从表的创建,查询,更新,以及事务,死锁来分析。
分布缓存和分库分表这里不做表述。

执行计划

对于慢SQL可以使用执行计划查看执行情况(DESC、EXPLAIN SQL语句):执行顺序,索引使用情况,预估的数据量,是否使用了filesort等,如:

《劣质SQl优化》 image.png

基于上图中的项,下面一一解释其含义,对SQL分析十分有帮助。

  • id:一组序号,表示查询中执行select子句或操作表的顺序,id越大则优先级越高,越先被执行,如果id相同,则执行顺序从上至下执行,如果是子查询,id的序号会递增。id也有可能为null,表示是一个结果集,且不再使用它来进行查询。

    《劣质SQl优化》 image.png 《劣质SQl优化》 image.png

  • select_type:
    【simple】:表示不需要union操作或者不包含子查询的简单select查询。有连接查询时,外层的查询为simple,且只有一个。
    【primary】:一个有union操作或者含有子查询的select,位于最外层的单位查询的select_type即为primary。且只有一个。
    【subquery】:在SELECT或WHERE列表中包含了子查询,该子查询被标记为subquery。

mysql> desc select id,(select id from class where id=22) as kk from stu;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
|  1 | PRIMARY     | stu   | NULL       | index | NULL          | idx_no  | 8       | NULL  |   24 |   100.00 | Using index |
|  2 | SUBQUERY    | class | NULL       | const | PRIMARY       | PRIMARY | 8       | const |    1 |   100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
mysql> desc select id from stu where exists (select * from class);
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | PRIMARY     | stu   | NULL       | index | NULL          | idx_no  | 8       | NULL |   24 |   100.00 | Using index |
|  2 | SUBQUERY    | class | NULL       | index | NULL          | PRIMARY | 8       | NULL |    6 |   100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

【dependent subquery】:与dependent union类似,表示这个subquery的查询要受到外部表查询的影响。

mysql> desc select id from stu where exists(select id from class where stu.id=id);
+----+--------------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+--------------------------+
| id | select_type        | table | partitions | type   | possible_keys | key     | key_len | ref        | rows | filtered | Extra                    |
+----+--------------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+--------------------------+
|  1 | PRIMARY            | stu   | NULL       | index  | NULL          | idx_no  | 8       | NULL       |   24 |   100.00 | Using where; Using index |
|  2 | DEPENDENT SUBQUERY | class | NULL       | eq_ref | PRIMARY       | PRIMARY | 8       | db1.stu.id |    1 |   100.00 | Using index              |
+----+--------------------+-------+------------+--------+---------------+---------+---------+------------+------+----------+--------------------------+
2 rows in set, 2 warnings (0.00 sec)

【derived】:用来表示包含在from子句中的子查询的select,mysql会递归执行并将结果放到一个临时表中。服务器内部称为”派生表”,因为该临时表是从子查询中派生出来的。
【union】:第二个SELECT出现在UNION之后,则被标记为UNION;
【dependent union】:与union一样,出现在union 或union all语句中,但是这个查询要受到外部查询的影响
【union result】:包含union的结果集,在union和union all语句中,因为它不需要参与查询,所以id字段为null。

  • table

    显示查询表名,如果查询使用了别名,那么这里显示的是别名。
    如果不涉及对数据表的操作,那么这显示为null。
    如果显示为尖括号括起来的<derived N>就表示这个是临时表,后边的N就是执行计划中的id,表示结果来自于这个查询产生。
    如果是尖括号括起来的<union M,N>,与<derived N>类似,也是一个临时表,表示这个结果来自于union查询的id为M,N的结果集。

  • type
    system,const,eq_ref,ref,fulltext,ref_or_null,unique_subquery,index_subquery,range,index_merge,index,ALL。依次从好到差

【system】:表中只有一行数据或者是空表,且只能用于myisam和memory表。如果是Innodb引擎表,type列在这个情况通常都是all或者index
【const】使用唯一索引或者主键,返回记录一定是1行记录的等值where条件时,通常type是const。其他数据库也叫做唯一索引扫描。
【eq_ref】类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件。

mysql> desc select * from stu,class where stu.id=class.id;
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------+------+----------+-------+
| id | select_type | table | partitions | type   | possible_keys | key     | key_len | ref          | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------+------+----------+-------+
|  1 | SIMPLE      | class | NULL       | ALL    | PRIMARY       | NULL    | NULL    | NULL         |    6 |   100.00 | NULL  |
|  1 | SIMPLE      | stu   | NULL       | eq_ref | PRIMARY       | PRIMARY | 8       | db1.class.id |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------+------+----------+-------+
2 rows in set, 1 warning (0.00 sec)

【ref】使用非唯一索引扫描或者唯一索引的前缀扫描,使用相等条件检索时就可能出现,常见与辅助索引的等值查找。
【range】索引范围扫描,对索引的扫描开始于某一点,返回匹配值域的行。显而易见的索引范围扫描是带有between或者where子句里带有<, >查询。当mysql使用索引去查找一系列值时,例如IN()和OR列表,也会显示range(范围扫描),当然性能上面是有差异的。
【index】索引全表扫描,把索引从头到尾扫一遍,index与ALL区别为index类型只遍历索引树。

mysql> desc select id from stu;
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key    | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | stu   | NULL       | index | NULL          | idx_no | 8       | NULL |   27 |   100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

【all】全表查询

  • possible_keys:指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用
  • key:查询真正使用到的索引
  • key_len:用于处理查询的索引长度,如果是单列索引,那就整个索引长度算进去,如果是多列索引,那么查询不一定都能使用到所有的列,具体使用到了多少个列的索引,这里就会计算进去,没有使用到的列,这里不会计算进去。
  • rows:表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数。
  • filtered:返回结果占所读行的百分比。
  • Extra:包含不适合在其他列中显示但十分重要的额外信息。
    Using filesort,Using temporary,Using where。

根据id和select_type项可以分析出SQL执行步骤;type可以分析出使用何种索引类型,要警惕ALL和INDEX,他们查询最差,可以适当增加索引或优化SQL;key和key_len可以分析出哪些索引使用到了,尤其是组合索引,可以进一步分析那一项使用到了;rows和filtered,一个可以估算出一共查询了多少数据,一个可以看到查询结果和总查询数据的百分比;Extra,如Using filesort,使用order by做了文件排序,sing temporary,使用了临时表等,进而做出优化。

SQL优化

分析执行计划,可以确定慢SQL问题所在,但是对SQL的优化,应该自上而下优化,从表创建开始。

表的创建

  • 表设计尽量小而精,适用最小使用原则:能用5个字段就不要用6个,尽量少的使用扩展字段。

  • 以将表中字段的宽度设得尽可能小,尽量避免text,blob等这些大家伙。

  • 设计表字段能用数字类型就千万别用字符类型。

  • 禁止使用小数存储货币,推荐使用整数。

  • 推荐字段定义为NOT NULL并且提供默认值
    1).null的列使索引/索引统计/值比较都更加复杂,对MySQL来说更难优化
    2). null 这种类型MySQL内部需要进行特殊处理,增加数据库处理记录的复杂性;同等条件下,表中有较多空字段的时候,数据库的处理性能会降低很多
    3). null值需要更多的存储空,无论是表还是索引中每行中的null的列都需要额外的空间来标识
    4). 对null 的处理时候,只能采用is null或is not null,而不能采用=、in、<、<>、!=、not in这些操作符号。

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

  • 禁止使用外键,如果有外键完整性约束,需要应用程序控制。

  • 索引应建立在那些将用于JOIN,WHERE判断和ORDER BY排序的字段上。

  • 禁止在更新十分频繁、区分度不高的属性上建立索引。
    更新会变更B+树,更新频繁的字段建立索引会大大降低数据库性能
    尽量不要对数据库中某个含有大量重复的值的字段建立索引:“性别”这种区分度不大的属性,建立索引是没有什么意义的,不能有效过滤数据,性能与全表扫描类似.对于一个ENUM类型的字段来说,出现大量重复值是很有可能的情况

  • 建立组合索引,必须把区分度高的字段放在前面

查询相关

    1. 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及到且区分度高的列上建立索引。
    1. 不要使用select *,不使用的字段尽量不要查询
      使用select *的话会增加解析的时间,另外会把不需要的数据也给查询出来(尤其在天猫淘宝这种有亿级客户的平台上,数据传输费时费带宽),如果增加字段,程序很有可能没有对字段做对应映射出现错误。
    1. 尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,优先使用UNION ALL替代,避免使用UNION
      UNION 会将各查询子集的记录做比较,对比UNION ALL ,速度都会慢上许多。如果使用UNION ALL能满足要求的话,优先使用UNION ALL。
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
  • 4.慎用not in,会导致全表扫描,可以考虑使用not exist代替
假定order和crm_user上user_name 有索引,不考虑大表小表,数据量一致。

1. select * from order where user_name not in (select user_name from crm_user)
这里crm_user进行全表查询

2. select * from order  o where  not exist (select user_name from crm_user u where o.user_name = u.user_name )
这里crm_user使用了索引

not extsts 的子查询可以用到外表上的索引。

    1. 要注意exist和in的使用
      in 是把外表和内表作hash 连接,而exists是对外表作loop循环,每次loop循环再对内表进行查询。因此,in用到的是外表的索引, exists用到的是内表的索引。
      如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in:
      例如:表A(小表),表B(大表)
select * from A where cc in (select cc from B)
效率低,用到了A表上cc列的索引;
select * from A where exists(select cc from B where cc=A.cc)
效率高,用到了B表上cc列的索引。
select * from B where cc in (select cc from A)
效率高:先查询A表数据,根据查询到的临时表查询B数据(使用了B表cc索引)
select * from B where exists(select cc from A where cc=B.cc)
效率低:循环查询B数据,根据B数据查询A数据(使用到A索引)
  • 6.禁止负向查询,以及%开头的模糊查询
    负向查询条件:NOT、!=、<>、!<、!>、NOT IN、NOT LIKE等,会导致全表扫描
    %开头的模糊查询,会导致全表扫描

  • 7.选择最有效率的表名顺序

    1. 删除重复记录
      DELETE FROM EMP E WHERE E.ROWID > (SELECT MIN(X.ROWID)
      FROM EMP X WHERE X.EMP_NO = E.EMP_NO);
  • 9.避免在where子句中对字段进行函数操作,逻辑运算,类型转换等,会导致索引不生效

  • 10.禁止使用属性隐式转换
    SELECT uid FROM t_user WHERE phone=13812345678 会导致全表扫描

  • 11.使用连接(JOIN)来代替子查询(Sub-Queries)

1. SELECT * FROM customerinfo WHERE CustomerID NOT IN (SELECTC ustomerID FROM salesinfo)
2. SELECT * FROM customerinfo LEFT JOIN salesinfo ON customerinfo.CustomerID=salesinfo.CustomerID WHERE salesinfo.CustomerID ISNULL
    1. 禁止大表使用JOIN查询,禁止大表使用子查询。

会产生临时表,消耗较多内存与CPU,极大影响数据库性能

  • 13 使用OR条件,可以使用IN查询,between进行代替。
    旧版本Mysql的OR查询是不能命中索引的,即使能命中索引,要使用or生成临时表,比较耗费CPU和内存。

  • 14.limit优化(大翻页查询)

 select id,uni_crm_id,phone from film uni_crm_account oder by gmt_create limit 600,20
优化后

select id,uni_crm_id,phone from uni_crm_account where id >12000 oder by gmt_create  limit 1,20;
  • 15.建立组合索引,必须把区分度高的字段放在前面
    索引的最左边前缀(leftmost prefix of the index)来进行查询:
    如索引:key(last_name, first_name, dob)
    (1) 匹配全值(Match the full value):对索引中的所有列都指定具体的值。例如,上图中索引可以帮助你查找出生于1960-01-01的Cuba Allen。
    (2) 匹配最左前缀(Match a leftmost prefix):你可以利用索引查找last name为Allen的人,仅仅使用索引中的第1列。
    (3) 匹配列前缀(Match a column prefix):例如,你可以利用索引查找last name以J开始的人,这仅仅使用索引中的第1列。
    (4) 匹配值的范围查询(Match a range of values):可以利用索引查找last name在Allen和Barrymore之间的人,仅仅使用索引中第1列。
    (5) 匹配部分精确而其它部分进行范围匹配(Match one part exactly and match a range on another part):可以利用索引查找last name为Allen,而first name以字母K开始的人。
    (6)仅对索引进行查询(Index-only queries):如果查询的列都位于索引中,则不需要读取元组的值。

更新相关

批量更新

update时,where语句尽量要走索引,不然会全表扫描,对所有记录锁定,如果数据量较大,更新时间会比较长,长时间锁表是应用程序难以承受;还有最好事先计算更新行数,在update语句后添加limit <行数>,避免处理脏数据的同时再发生二次故障,同时找人review下,项目组报备,。

事务

事务在并发情况下很复杂,存在执行慢,lost update,死锁等。

降低事务的颗粒度

大事务执行非常耗时,可以采取异步任务(MQ,异步线程 )等柔性事务降低事务颗粒度,采取正向、逆向对账机制进行事后补偿。

innodb中锁的类型分为共享锁,排他锁,意向共享锁和意向排他锁。起哄意向锁是为了解决表锁和行锁冲突问题,官方文档:“The main purpose of IX and IS locks is to show that someone is locking a row, or going to lock a row in the table.”。
其中select xxx for update 、insert 、delete、update是排他锁,共享锁需要使用lock in share mode。
根据锁的粒度分分为行锁,表锁。行锁又分为Record Lock、Gap Lock、Next-Key Lock。

InnoDB定义的4种隔离级别事务,降低了并发冲突的可能。

  1. Read Uncommited(RU):可以读取未提交记录。

  2. Read Committed (RC):针对当前读更新,RC隔离级别保证对读取到的记录加锁 (记录锁),存在幻读现象。

  3. Repeatable Read (RR):基于快照读,针对当前读更新,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象(这里指的是非聚镞索引)。

  4. Serializable
    所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。Serializable隔离级别下,写写冲突,读写冲突,并发度急剧下降。

MYSQL采用了两阶段锁协议,含义是:第一个阶段是获得锁(nsert/update/delete/select … for update),第二个阶段是释放锁(commit/rollback);保证并发调度的正确性,两阶段封锁相对于一阶段锁(一次性获得事务需要的所有锁),提高了并发度,但同时也带来了死锁的可能。

具体加锁机制请看P9大神博文:加锁机制分析

lost update

无论是Read Commited还是Repeable Read级别,都有可能发生lost upate(即覆写),详见a-beginners-guide-to-database-locking-and-the-lost-update-phenomena,文中分析了lost update原因,提到解决方案:悲观锁和乐观锁。

个人觉得选择悲观锁或乐观锁应依具体场景而定。

  1. 悲观锁:并发冲突大的情况,会导致很多任务阻塞,高并发修改的场景就不太适合了;但好处也是有的,如有其他客户端(如msyql console)也在并发更新情况下,程序是基于当前读(官方文档的术语叫locking read,也就是insert,update,delete,select..in share mode和select..for update)做的更新,同一时间只允许一个操作或事务提交,不会产生lost update。

  2. 乐观锁: 一般使用版本号或时间戳做比较,可以结合分布式锁+轮询来降低对数据库的访问次数,并行度较高;缺点是,如果其他客户端也在并行上修改数据且未对乐观锁字段进行判断和更新,存在乐观锁被使用过一次情况,应用程序基于此再次更新数据,出现lost update情况。

死锁

死锁,指两个或多个事务,各自占有对方的期望获得的资源,形成的循环等待,彼此无法继续执行的一种状态。

这里例举几个常简死锁情况。

  1. 如下,session1和session2中2张表更新顺序不同,产生死锁。
session1:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update STU set name='小杭' where id=2;
Query OK, 1 row affected (43.05 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> update teacher set name='老师2' where id=1;
Query OK, 1 row affected (5.98 sec)
Rows matched: 1  Changed: 1  Warnings: 0
session2:
mysql> begin;
Query OK, 0 rows affected (0.14 sec)

mysql> update teacher set name='老师1' where id=1;
Query OK, 1 row affected (0.09 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update STU set name='小明' where id=2;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

解决:调整成一致的更新顺序,死锁解决。

2.一个事务中,在同一张表中对不同记录进行更新,如果更新顺序不一致,也有可能发生死锁。

session1:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update STU set name='刚' where id=13;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update STU set name='行'where id=2;
Query OK, 1 row affected (2.48 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.22 sec)
session2:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update STU set name='航'where id=2;
Query OK, 1 row affected (0.06 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update STU set name='好' where id=13;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

解决:如上问题中,对ID进行排序,在根据排序顺序进行依次更新。

  1. RR级别下,插入死锁
    三个事务同时执行,session1最先获得锁,session2和session3检测到了主键冲突错误但session1未提交所以hold,随后session1上执行回滚,session2顺利获得锁,session3死锁了。
session1:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into STU(id,stu_no,name,idy,version) values(21,21,'21',21,21);
Query OK, 1 row affected (0.07 sec)

mysql> rollback;
Query OK, 0 rows affected (0.09 sec)
session2:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql>  insert into STU(id,stu_no,name,idy,version) values(21,21,'21',21,21);
Query OK, 1 row affected (11.54 sec)
session3:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql>  insert into STU(id,stu_no,name,idy,version) values(21,21,'21',21,21);
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
  1. 同时执行session1和session2,session1和session2加锁顺序相反:session1从name索引出发,读到的[hdc, 1],[hdc, 6]均满足条件,不仅会加name索引上的记录X锁,而且会加聚簇索引上的记录X锁,加锁顺序为先[1,hdc,100],后[6,hdc,10]。而Session 2,从pubtime索引出发,[10,6],[100,1]均满足过滤条件,同样也会加聚簇索引上的记录X锁,加锁顺序为[6,hdc,10],后[1,hdc,100]。如果两个Session恰好都持有了第一把锁,请求加第二把锁,死锁就发生了。

《劣质SQl优化》 image.png

其他(待完善)

  1. show processlist –可以看到长时间执行的SQL,死锁等信息。

    《劣质SQl优化》 image.png

2.慢sql开启
开启慢查询日志:set global slow_query_log=on;
修改慢查询时间:set global long_query_time=1;
查询慢查询下sql存放目录: show global variables like “%slow%”;

3.调用链日志
阿里鹰眼,google Dapper,到家守望者等等。

参考

    原文作者:alivs
    原文地址: https://www.jianshu.com/p/af287c17f342
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞