MySQL 性能调优——SQL 查询优化

如何设计最优的数据库表结构,如何创建最好的索引,以及如何扩展数据库的查询,这些对于高性能来讲都是必不可少的。可是只有这些还不够,要得到良好的数据库性能,咱们还要设计合理的数据库查询,若是查询设计的很糟糕,即便增长再多的只读从库,表结构设计的再合理,索引再合适,只要查询不能使用到这些东西,也没法实现高性能的查询。因此说查询优化,索引优化,库表结构优化须要齐头并进。mysql

在进行库表结构设计时,咱们要考虑到之后的查询要如何的使用这些表,一样,编写 SQL 语句的时候也要考虑到如何使用到目前已经存在的索引,或是如何增长新的索引才能提升查询的性能。sql

想要对存在性能问题的查询进行优化,须要可以找到这些查询,下面先看下如何获取有性能问题的 SQL。数据库

1.获取有性能问题的SQL

获取有性能问题的 SQL 的三种方法:缓存

经过用户反馈获取存在性能问题的 SQL;
经过慢查日志获取存在性能问题的 SQL;
实时获取存在性能问题的 SQL;服务器

1.慢查询日志获取性能问题SQL

MySQL 慢查询日志是一种性能开销比较低的获取存在性能问题 SQL 的解决方案,其主要的性能开销在磁盘 IO 和存储日志所须要的磁盘空间。对于磁盘 IO 来讲,因为写日志是顺序存储,开销基本上忽略不计,因此主要须要关注的仍是磁盘空间。并发

slow_query_log:是否启动慢查询日志,默认不启动,on 启动;
slow_query_log_file:指定慢查询日志的存储路径及文件,默认状况下保存在 MySQL 的数据目录中;
long_query_time:指定记录慢查询日志 SQL 执行时间的阈值,单位秒,默认 10 秒,一般对于一个繁忙的系统来讲,改成0.001秒比较合适;
log_queries_not_using_indexes:是否记录未使用索引的 SQL;

和二进制日志不一样,慢查询日志会记录全部符合条件的 SQL,包括查询语句、数据修改语句、已经回滚的 SQL。函数

慢查询日志中记录的内容:工具

# Query_time: 0.000220             //执行时间,能够精确到毫秒,220毫秒
# Lock_time: 0.000120              //所使用锁的时间,能够精确到毫秒
# Rows_sent: 1                     //返回的数据行数
# Rows_examined: 1                 //扫描的数据行数
SET timestamp=1538323200;          //执行sql的时间戳
SELECT c FROM test1 WHERE id =100;    //sql

一般状况下,在一个繁忙的系统中,短期内就能够产生几个 G 的慢查询日志,人工检查几乎是不可能的,为了快速分析慢查询日志,必须借助相关的工具。性能

经常使用的慢查询日志工具:优化

一、mysqldumpslow:一个经常使用的,MySQL 官方提供的慢查询日志分析工具,随着 MySQL 服务器的安装而被安装。能够汇总除查询条件外其余彻底相同的 SQL,并将分析结果按照参数中所指定的顺序输出。

二、pt-query-digest:用于分析 MySQL 慢查询的一个工具。

2.实时获取性能问题SQL

为了更加及时的发现当前的性能问题,咱们还能够经过实时的方法来获取有性能问题的 SQL。最方便的一种方法就是利用 MySQL information_schema 数据库下的 PROCESSLIST 表来实现实时的发现性能问题 SQL。例以下面这条 SQL 表示查询出当前服务器中执行时间超过 1 秒的 SQL:

SELECT id,user,host,db,command,time,state,info FROM information_schema.PROCESSLIST WHERE TIME>=1

而后咱们能够经过脚本周期性的来执行这条 SQL,实时的发现哪些 SQL 执行的是比较慢的。

2.SQL的解析预处理及生成执行计划

找到了那些查询存在性能问题的 SQL,那么下面咱们就看下,为何这些 SQL 会存在性能问题?

为了搞清楚这个问题,咱们先来看下 MySQL 服务器处理一条 SQL 请求所须要经历的步骤都有哪些:

  1. 客户端经过 MySQL 的接口发送 SQL 请求给服务器,这一步一般不会影响查询性能;
  2. MySQL 服务器检查是否能够在查询缓存中命中该 SQL,若是命中,则当即返回存储在缓存中的结果,不然进入下一阶段;
  3. MySQL 服务器进行 SQL 解析,预处理,再由 SQL 优化器生成对应的执行计划;
  4. 根据执行计划,调用存储引擎 API 来查询数据;
  5. 将结果返回给客户端。
  6. 这就是 MySQL 服务器处理查询请求的整个过程。在第二到第五步,都有可能对查询的响应速度形成影响,下面来分别看下这些过程可能对查询的响应速度有影响的因素都有些什么:

在解析查询语句前,若是查询缓存是打开的,那么 MySQL 优先检查这个查询是否命中查询缓存中的数据,这个检查是经过一个对大小写敏感的 Hash 查找实现的。因为 Hash 查找只能进行全值匹配,因此请求的查询和缓存中的查询就算只有一个字节的不一样,那么也不会匹配到缓存中的结果,这种状况下,查询就会进入到下一阶段处理。若是正好命中查询缓存,在返回查询结果以前,MySQL 就会检查用户权限,也是无需解析 SQL 语句的,由于在查询缓存中,已经存放了当前查询所须要访问的表的信息,若是权限没有问题,MySQL 会跳过全部的其余阶段,直接从缓存中拿到结果,并返回给客户端,这种状况下查询是不会被解析的,也不会生成查询计划,不会被执行。

能够发现,从查询缓存中直接返回结果并不容易。

查询缓存对 SQL 性能的影响:

若是查询缓存,一旦数据更新,都要对缓存中数据进行刷新,影响性能;
每次在查询缓存中检查 SQL 是否被命中,都要对缓存加锁,影响性能;
对于一个读写频繁的系统来讲,查询缓存极可能会下降查询处理的效率。因此在这种状况下建议你们不要使用查询缓存。

对查询缓存影响的一些系统参数:

query_cache_type: 设置查询缓存是否可用,能够设置为ON、OFF、DEMAND,DEMAND表示只有在查询语句中使用 SQL_CACHE 和 SQL_NO_CACHE 来控制是否须要缓存。
query_cache_size: 设置查询缓存的内存大小,必须是1024字节的整数倍。 
query_cache_limit: 设置查询缓存可用存储的最大值,若是知道很大不会被缓存,能够在查询上加上 SQL_NO_CACHE 提升效率。
query_cache_wlock_invalidate: 设置数据表被锁后是否返回缓存中的数据,默认关闭。
query_cache_min_res_unit: 设置查询缓存分配的内存块最小单位。

对于一个读写频繁的系统来讲,能够把 query_cache_type 设置为 OFF,而且把 query_cache_size 设置为 0。

当查询缓存未启用或者未命中则会进入下一阶段,也就是须要将一个 SQL 转换成一个执行计划,MySQL 再依据这个执行计划和存储引擎进行交互,这个阶段包括了多个子过程:解析 SQL,预处理,优化 SQL 执行计划。在这个过程当中,出现任何错误,好比语法错误等,都有可能停止查询的过程。

在语法解析阶段,主要是经过关键字对 MySQL 语句进行解析,并生成一棵对应的 “解析树”。这一阶段,MySQL 解析器将使用 MySQL 语法规则验证和解析查询,包括检查语法是否使用了正确的关键字、关键字的顺序是否正确等。

预处理阶段则是根据 MySQL 规则进一步检查解析树是否合法,好比检查查询中所涉及的表和数据列是否存在、检查名字或别名是否存在歧义等。

若是语法检查所有都经过了,查询优化器就能够生成查询计划了。

  • 会形成 MySQL 生成错误的执行计划的缘由:
  • 统计信息不许确;
  • 执行计划中的成本估算不等同于实际的执行计划的成本;
  • MySQL 查询优化器所认为的最优可能与你所认为的最优不同;
  • MySQL 从不考虑其余并发的查询,这可能会影响当前查询的速度;
  • MySQL 有时候也会基于一些固定的规则来生成执行计划;
  • MySQL 不会考虑不受其控制的成本,例如存储过程、用户自定义的函数等。
    MySQL 的查询优化器能够优化的 SQL 类型:

从新定义表的关联顺序,优化器会根据统计信息来决定表的关联顺序;

  • 将外链接转化为内链接,好比 where 条件和库表结构均可能让一个外链接等价于内链接;
  • 使用等价变换规则,好比 (5=5 and a>5) 将被改写为 a>5;
  • 利用索引和列是否为空来优化 count()、min() 和 max() 等聚合函数;
  • 将一个表达式转换为常数表达式;
  • 使用等价变换规则,好比覆盖索引,当 MySQL 查询优化器发现索引中的列包含全部查询中所须要的信息的时候,MySQL 就能使用索引返回须要的数据;
  • 子查询优化,好比把子查询转换为关联查询,减小表的查询次数;
  • 提早终止查询;
  • 对 in() 条件进行优化。
    以上这些就是 MySQL 查询优化器能够自动对查询所作的一些优化。通过查询优化器改写后的 SQL,查询优化器会对其生成一个 SQL 执行计划,而后 MySQL 服务器就能够根据执行计划调用存储引擎的 API,经过存储引擎获取数据了。

    3.肯定查询处理各个阶段的耗时

    SQL 查询优化的主要目的就是减小查询所消耗的时间,加快查询的响应速度。下面来介绍如何度量查询处理各个阶段所消耗的时间。

对于一个存在性能问题的 SQL 来讲,必须知道在查询的哪一阶段消耗的时间最多,而后才能有针对性的进行优化。度量查询处理各个阶段所消耗的时间,经常使用的方法有两种:

  • 使用 profile;
  • 使用 performance_schema;

    4.特定SQL的查询优化

    前面介绍的方法,已经能够获取一个存在性能问题的 SQL 和获取一个 SQL 在执行的各个阶段所消耗的时间了。获得这些信息后,咱们就能够针对性的对 SQL 进行优化了,下面举几个对特定 SQL 优化的案例:

1.大表的更新和删除

对于大表的数据修改最好要分批处理,好比咱们要在一个 1000 万行记录的表中删除/更新 100 万行记录,那么咱们最好分多个批次进行删除/更新,一次只删除/更新 5000 行记录,避免长时间的阻塞,而且为了减小对主从复制带来的压力,每次删除/修改数据后须要暂停几秒。这里提供一个能够完成这样工做的 MySQL 存储过程的实例:

DELIMITER $$
USE 'db_name'$$
DROP PROCEDURE IF EXISTS 'p_delete_rows'$$
CREATE DEFINER='mysql'@'127.0.0.1' PROCEDURE 'p_delete_rows'()
BEGIN
        DECLARE v_rows INT;
        SET v_rows = 1;
        WHERE v_rows > 0
        DO
                DELETE FROM table_name WHERE id >= 9000 AND id <= 290000 LIMIT 5000;
                SELECT ROW_COUNT() INTO v_rows;
                SELECT SLEEP(5);
        END WHERE;
END$$
DELIMITER;

你们能够根据本身的状况来修改这个存储过程,或者使用本身熟悉的开发语言实现这个处理过程,使用这个存储过程只须要修改 DELETE FROM table_name WHERE id >= 9000 AND id <= 290000 LIMIT 5000; 部分的内容便可。

2.如何修改大表的表结构

对于 InnoDB 存储引擎来讲,对表中的列的字段类型进行修改或者改变字段的宽度时仍是会锁表,同时也没法解决主从数据库延迟的问题。

解决方案:

在主服务器上创建新表,新表的结构就是修改以后的结构,再把老表的数据导入到新表中,而且在老表上创建一系列的触发器,把老表数据的修改同步更新到新表中,当老表和新表的数据同步后,再对老表加一个排它锁,而后从新命名新表为老表的名字,最好删除重命名的老表,这样就完成了大表表结构修改的工做。这样处理的好处是能够尽可能减小主从延迟,以及在重命名以前不须要加任何的锁,只须要在重命名的时候加一个短暂的锁,这对应用一般是无影响的,缺点就是操做比较复杂。好在有工具能够帮咱们实行这个过程,这个工具一样是 percona 公司 MySQL 工具集中的一个,叫作 pt-online-schema-change:

pt-online-schema-change \
--alter="MODIFY c VARCHAR(150) NOT NULL DEFAULT ''" \
--user=root --password=password D=db_name,t=table_name \
--charset=utf8 --execute

这个命令就是把 db_name 数据库下的 table_name 表中 c 列的宽度改成 VARCHAR(150)。

3.如何优化not in和<>查询

MySQL 查询优化器能够自动的把一些子查询优化为关联查询,可是对于存在not in和<>这样的子查询语句来讲,就没法进行自动优化了,这就形成了会循环屡次来查找子表来确认是否知足过滤条件,若是子查询刚好是一个很大的表的话,这样作的效率会很是低,因此咱们在进行 SQL 开发时,最好把这类查询自行改写成关联查询。

SELECT id,name,email 
FROM customer 
WHERE id 
NOT IN(SELECT id FROM payment)

优化改写后:

SELECT a.id,a.name,a.email 
FROM customer a 
LEFT JOIN payment b ON a.id=b.id 
WHERE b.id IS NULL

使用 LEFT JOIN 关联替代了 NOT IN 过滤,这样避免了对 payment 表进行屡次查询,这是一种很是经常使用的对 NOT IN 的优化方式。

4.使用汇总表优化查询

最多见的就是商品的评论数,若是咱们在用户访问页面时,实时的访问商品的评论数,一般来讲,查询的 SQL 会相似于下面这个样子:

SELECT COUNT(*) FROM product_comment WHERE product_id = 10001;

这个 SQL 就是统计出全部 product_id = 10001 的评论,假设评论表中有上亿条记录,那么这个 SQL 执行起来是很是的慢的,若是有大量的并发访问,则会对数据库带来很大的压力。对于这么状况,咱们一般使用汇总表的方式进行优化。所谓的汇总表就是提早把要统计的数据进行汇总并记录到表中已备后续的查询使用。针对这个查询,咱们可使用下面的方式进行优化:

CREATE TABLE product_comment_cnt(product_id INT, cnt INT);   //创建汇总表

//查询评论数
SELECT SUM(cnt) FROM(
    SELECT cnt FROM product_comment_cnt WHERE product_id = 10001
    UNION ALL
    SELECT COUNT(*) FROM product_comment WHERE product_id = 10001
    AND timestr > DATE(NOW())
);