MySQL 性能优化其实是个很大的课题,在优化上存在着一个调优金字塔的说法:
很明显从图上可以看出,越往上走,难度越来越高,收益却是越来越小的。 比如硬件和 OS 调优,需要对硬件和 OS 有着非常深刻的了解,仅仅就磁盘一项来说,一般非 DBA 能想到的调整就是 SSD 盘比用机械硬盘更好,但其实它至少包括 了,使用什么样的磁盘阵列(RAID)级别、是否可以分散磁盘 IO、是否使用裸 设备存放数据,使用哪种文件系统(目前比较推荐的是 XFS),操作系统的磁盘 调度算法(目前比较推荐 deadline,对机械硬盘和 SSD 都比较合适。从内核 2.5 开始,默认的 I/O 调度算法是 Deadline,之后默认 I/O 调度算法为 Anticipatory, 直到内核 2.6.17 为止,从内核 2.6.18 开始,CFQ 成为默认的 IO 调度算法,但 CFQ 并不推荐作为数据库服务器的磁盘调度算法)选择,是否需要调整操作系统文件管理方面比如 atime 属性等等。
所以在进行优化时,首先需要关注和优化的应该是架构,如果架构不合理, 即使是 DBA 能做的事情其实是也是比较有限的。
对于架构调优,在系统设计时首先需要充分考虑业务的实际情况,是否可以 把不适合数据库做的事情放到数据仓库、搜索引擎或者缓存中去做;然后考虑写 的并发量有多大,是否需要采用分布式;最后考虑读的压力是否很大,是否需要 读写分离。对于核心应用或者金融类的应用,需要额外考虑数据安全因素,数据 是否不允许丢失。
作为金字塔的底部的架构调优,采用更适合业务场景的架构能最大程度地 升系统的扩展性和可用性。在设计中进行垂直拆分能尽量解耦应用的依赖,对读 压力比较大的业务进行读写分离能保证读性能线性扩展,而对于读写并发压力比 较大的业务在 MySQL 上也有采用读写分离的大量案例。
作为金字塔的底部,在底层硬件系统、SQL 语句和参数都基本定型的情况下, 单个 MySQL 数据库能 供的性能、扩展性等就基本定型了。但是通过架构设计和 优化,却能承载几倍、几十倍甚至百倍于单个 MySQL 数据库能力的业务请求能力。
对于 MySQL 调优,需要确认业务表结构设计是否合理,SQL 语句优化是否足够,该添加的索引是否都添加了,是否可以剔除多余的索引等等。
最后确定系统、硬件有哪些地方需要优化,系统瓶颈在哪里,哪些系统参数 需要调整优化,进程资源限制是否 到足够高;在硬件方面是否需要更换为具有 更高 I/O 性能的存储硬件,是否需要升级内存、CPU、网络等。
如果在设计之初架构就不合理,比如没有进行读写分离,那么后期的 MySQL 和硬件、系统优化的成本就会很高,并且还不一定能最终解决问题。如果业务性 能的瓶颈是由于索引等 MySQL 层的优化不够导致的,那么即使配置再高性能的 I/O 存储硬件或者 CPU 也无法支撑业务的全表扫 。
所以本章我们重点关注 MySQL 方面的调优,特别是索引。SQL/索引调优要求 对业务和数据流非常清楚。在阿里巴巴内部,有三分之二的 DBA 是业务 DBA,从 业务需求讨论到表结构审核、SQL 语句审核、上线、索引更新、版本迭代升级, 甚至哪些数据应该放到非关系型数据库中,哪些数据放到数据仓库、搜索引擎或 者缓存中,都需要这些 DBA 跟踪和复审。他们甚至可以称为数据架构师(Data Architecher)。
前面的章节我们知道如何设计最优的库表结构、如何建立最好的索引,这些 对于高性能来说是必不可少的。但这些还不够—还需要合理的设计查询。如果查 询写得很糟糕,即使库表结构再合理、索引再合适,也无法实现高性能。
慢查询日志,顾名思义,就是查询花费大量时间的日志,是指 mysql 记录所 有执行超过 long_query_time 参数设定的时间阈值的 SQL 语句的日志。该日志能 为 SQL 语句的优化带来很好的帮助。默认情况下,慢查询日志是关闭的,要使用 慢查询日志功能,首先要开启慢查询日志功能。如何开启,我们稍后再说。
查询性能低下最基本的原因是访问的数据太多。大部分性能低下的查询都可 以通过减少访问的数据量的方式进行优化。对于低效的查询,一般通过下面两个 步骤来分析总是很有效:
有些查询会请求超过实际需要的数据,然后这些多余的数据会被应用程序丢 弃。这会给 MySQL 服务器带来额外的负担,并增加网络开销,另外也会消耗应 用服务器的 CPU 和内存资源。比如:
3. 查询不需要的记录
一个常见的错误是常常会误以为 MySQL 会只返回需要的数据,实际上 MySQL 却是先返回全部结果集再进行计算。我们经常会看到一些了解其他数据库 系统的人会设计出这类应用程序。这些开发者习惯使用这样的技术,先使用 SELECT 语句查询大量的结果,然后获取前面的 N 行后关闭结果集(例如在新闻 网站中取出 100 条记录,但是只是在页面上显示前面 10 条)。他们认为 MySQL 会执行查询,并只返回他们需要的 10 条数据,然后停止查询。实际情况是 MySQL 会查询出全部的结果集,客户端的应用程序会接收全部的结果集数据,然后抛弃 其中大部分数据。最简单有效的解决方法就是在这样的查询后面加上 LIMIT。
4. 总是取出全部列
每次看到 SELECT*的时候都需要用怀疑的眼光审视,是不是真的需要返回全 部的列?很可能不是必需的。取出全部列,会让优化器无法完成索引覆盖扫 这 类优化,还会为服务器带来额外的 I/O、内存和 CPU 的消耗。因此,一些 DBA 是 严格禁止 SELECT *的写法的,这样做有时候还能避免某些列被修改带来的问题。
什么时候应该允许查询返回超过需要的数据?如果这种有点浪费数据库资 源的方式可以简化开发,因为能 高相同代码片段的复用性,如果清楚这样做的 性能影响,那么这种做法也是值得考虑的。如果应用程序使用了某种缓存机制, 或者有其他考虑,获取超过需要的数据也可能有其好处,但不要忘记这样做的代 价是什么。获取并缓存所有的列的查询,相比多个独立的只获取部分列的查询可 能就更有好处。
5. 重复查询相同的数据
不断地重复执行相同的查询,然后每次都返回完全相同的数据。比较好的方 案是,当初次查询的时候将这个数据缓存起来,需要的时候从缓存中取出,这样 性能显然会更好。
在确定查询只返回需要的数据以后,接下来应该看看查询为了返回结果是否 扫 了过多的数据。对于 MySQL,最简单的衡量查询开销的三个指标如下:
响应时间、扫描的行数、返回的行数
没有哪个指标能够完美地衡量查询的开销,但它们大致反映了 MySQL 在内 部执行查询时需要访问多少数据,并可以大概推算出查询运行的时间。这三个指 标都会记录到 MySQL 的慢日志中,所以检查慢日志记录是找出扫 行数过多的 查询的好办法。
6. 响应时间
响应时间是两个部分之和:服务时间和排队时间。
服务时间是指数据库处理这个查询真正花了多长时间。
排队时间是指服务器因为等待某些资源而没有真正执行查询的时间—-可能 是等 I/O 操作完成,也可能是等待行锁,等等。
当你看到一个查询的响应时间的时候,首先需要问问自己,这个响应时间是否是一个合理的值。概括地说,了解这个查询需要哪些索引以及它的执行计划是什么,然后计算大概需要多少个顺序和随机 I/O,再用其乘以在具体硬件条件下一 次 I/O 的消耗时间。最后把这些消耗都加起来,就可以获得一个大概参考值来判断当前响应时间是不是一个合理的值。
7. 扫描的行数和返回的行数
分析查询时,查看该查询扫描的行数是非常有帮助的。这在一定程度上能够说明该查询找到需要的数据的效率高不高。
理想情况下扫描的行数和返回的行数应该是相同的。但实际情况中这种“美事”并不多。例如在做一个关联查询时,服务器必须要扫描多行才能生成结果集中的一行。扫描的行数对返回的行数的比率通常很小,一般在 1:1 和 10:1 之间, 不过有时候这个值也可能非常非常大。
8. 扫描的行数和访问类型
在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。 MySQL 有好几种访问方式可以查找并返回一行结果。有些访问方式可能需要扫描很多行才能返回一行结果,也有些访问方式可能无须扫描就能返回结果。
在 EXPLAIN 语句中的 type 列反应了访问类型。访问类型有很多种,从全表扫描到索引扫描、范围扫描、唯一索引查询、常数引用等。这里列的这些,速度是从慢到快,扫描的行数也是从小到大。你不需要记住这些访问类型,但需要明白扫描表、扫描索引、范围访问和单值访问的概念。
如果查询没有办法找到合适的访问类型,那么解决的最好办法通常就是增加一个合适的索引,为什么索引对于查询优化如此重要了。索引让 MySQL 以最高效、扫描行数最少的方式找到需要的记录。
一般 MySQL 能够使用如下三种方式应用 WHERE 条件,从好到坏依次为:
好的索引可以让查询使用合适的访问类型,尽可能地只扫描需要的数据行。
如果发现查询需要扫描大量的数据但只返回少数的行,那么通常可以尝试下面的技巧去优化它:
在优化有问题的查询时,目标应该是找到一个更优的方法获得实际需要的结果——而不一定总是需要从 MySQL 获取一模一样的结果集。有时候,可以将查询转换一种写法让其返回一样的结果,但是性能更好。但也可以通过修改应用代码,用另一种方式完成查询,最终达到一样的目的。
设计查询的时候一个需要考虑的重要问题是,是否需要将一个复杂的查询分成多个简单的查询。在传统实现中,总是强调需要数据库层完成尽可能多的工作, 这样做的逻辑在于以前总是认为网络通信、查询解析和优化是一件代价很高的事情。但是这样的想法对于 MySQL 并不适用,MySQL 从设计上让连接和断开连接 都很轻量级,在返回一个小的查询结果方面很高效。现代的网络速度比以前要快很多,无论是带宽还是延迟。在某些版本的 MySQL 上,即使在一个通用服务器上,也能够运行每秒超过 10 万的查询,即使是一个千兆网卡也能轻松满足每秒超过 2000 次的查询。所以运行多个小查询现在已经不是大问题了。
MySQL 内部每秒能够扫内存中上百万行数据,相比之下,MySQL 响应数据给客户端就慢得多了。在其他条件都相同的时候,使用尽可能少的查询当然是更好的。但是有时候,将一个大查询分解为多个小查询是很有必要的。
不过,在应用设计的时候,如果一个查询能够胜任时还写成多个独立查询是不明智的。例如,应用对一个数据表做 10 次独立的查询来返回 10 行数据,每个查询返回一条结果,查询 10 次。
有时候对于一个大查询我们需要“分而治之”,将大查询切分成小查询,每个查询功能完全一样,只完成一小部分,每次只返回一小部分查询结果。 删除旧的数据就是一个很好的例子。定期地清除大量数据时,如果用一个大的语句一次性完成的话,则可能需要一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。将一个大的 DELETE 语句切分成多个较小的查询可以尽可能小地影响 MySQL 性能,同时还可以减少 MySQL 复制的延 迟。
一次删除一万行数据一般来说是一个比较高效而且对服务器影响也最小的做法。同时,需要注意的是,如果每次删除数据后,都暂停一会儿再做下一次删除,这样也可以将服务器上原本一次性的压力分散到一个很长的时间段中,就可以大大降低对服务器的影响,还可以大大减少删除时锁的持有时间。
很多高性能的应用都会对关联查询进行分解。简单地,可以对每一个表进行一次单表查询,然后将结果在应用程序中进行关联。到底为什么要这样做? 乍一看,这样做并没有什么好处,原本一条查询,这里却变成多条查询,返回的结果又是一模一样的。事实上,用分解关联查询的方式重构查询有如下的优势:
让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。 将查询分解后,执行单个查询可以减少锁的竞争。
在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。查询本身效率也可能会有所提升。
可以减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需要重复地访问一部分数据。 从这点看,这样的重构还可能会减少网络和内存的消耗。
更进一步,这样做相当于在应用中实现了哈希关联,而不是使用 MySQL 的嵌套循环关联。某些场景哈希关联的效率要高很多。
在很多场景下,通过重构查询将关联放到应用程序中将会更加高效,这样的场景有很多,比如:当应用能够方便地缓存单个查询的结果的时候、当可以将数据分布到不同的MySQL服务器上的时候、当能够使用IN()的方式代替关联查询的时候、当查询中使用同一个数据表的时候。
4.2.4. 慢查询配置
我们已经知道慢查询日志可以帮助定位可能存在问题的 SQL 语句,从而进行 SQL 语句层面的优化。但是默认值为关闭的,需要我们手动开启。
show VARIABLES like ‘slow_query_log’;
mysql> show VARIABLES like 'slow_query_log';
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| slow_query_log | ON |
+----------------+-------+
1 row in set (0.00 sec)
on 是打开 如果是off 执行
set GLOBAL slow_query_log=1;
但是多慢算慢?MySQL 中可以设定一个阈值,将运行时间超过该值的所有 SQL 语句都记录到慢查询日志中。long_query_time 参数就是这个阈值。默认值为 10,代表 10 秒。
mysql> show VARIABLES like '%long_query_time%';
+-----------------+----------+
| Variable_name | Value |
+-----------------+----------+
| long_query_time | 1.000000 |
+-----------------+----------+
1 row in set (0.00 sec)
当然也可以设置
set global long_query_time=1; ---默认 10 秒,这里为了演示方便设置为 0
对于产生的慢查询日志,可以指定输出的位置,通过参数 log_output 来控制, 可以输出到[TABLE][FILE][FILE,TABLE]。比如
set global log_output=‘FILE,TABLE’,缺省是输出到文件,我们的配置把慢查询 输出到表,不过一般不推荐输出到表。
mysql> show VARIABLES like 'log_output';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_output | FILE |
+---------------+-------+
1 row in set (0.00 sec)
小结
开启慢查询功能以后,会根据我们的配置产生慢查询日志
slow_query_log 启动停止慢查询日志
从慢查询日志里面摘选一条慢查询日志,数据组成如下
“Time: 2021-04-05T07:50:53.243703Z”:查询执行时间
“User@Host: root[root] @ localhost [] Id: 3”:用户名 、用户的 IP 信 息、线程 ID 号
“Query_time: 0.000495”:执行花费的时长【单位:毫秒】 “Lock_time: 0.000170”:执行获得锁的时长 “Rows_sent”:获得的结果行数 “Rows_examined”:扫 的数据行数 “SET timestamp”:这 SQL 执行的具体时间
最后一行:执行的 SQL 语句
慢查询的日志记录非常多,要从里面找寻一条查询慢的日志并不是很容易的 事情,一般来说都需要一些工具辅助才能快速定位到需要优化的 SQL 语句,下面 介绍两个慢查询辅助工具
mysqldumpslow
常用的慢查询日志分析工具,汇总除查询条件外其他完全相同的 SQL,并将 分析结果按照参数中所指定的顺序输出。当然它的参数不少,我们常用的也就是 那么几个。
语法:
mysqldumpslow -s r -t 10 slow-mysql.log -s order (c,t,l,r,at,al,ar)
c:总次数
t:总时间
l:锁的时间 r:获得的结果行数 at,al,ar :指 t,l,r 平均数
【例如:at = 总时间/总次数】
-s 对结果进行排序,怎么排,根据后面所带的 (c,t,l,r,at,al,ar),缺省为 at
-t NUM just show the top n queries:仅显示前 n 条查询
-g PATTERN grep: only consider stmts that include this string:通过 grep 来 筛选语句。
./mysqldumpslow -s t -t 10 /home/mysql/mysql57/data/iZwz9j203ithc4gu1uvb2wZ-slow.log
Reading mysql slow query log from /data/mysql/data/VM-16-8-centos-slow.log
Count: 2 Time=1.12s (2s) Lock=0.00s (0s) Rows=10567.0 (21134), root[root]@[127.0.0.1]
select * from naruto.order_exp where order_note like 'S'
Count: 1 Time=1.66s (1s) Lock=0.00s (0s) Rows=10567.0 (10567), naruto[naruto]@[127.0.0.1]
select * from order_exp
Count: 1 Time=1.14s (1s) Lock=0.00s (0s) Rows=10567.0 (10567), naruto[naruto]@[127.0.0.1]
select * from order_exp order by order_status
Count: 1 Time=0.15s (0s) Lock=0.00s (0s) Rows=1.0 (1), root[root]@localhost
接下来介绍重点。mysql执行计划