数据库,是Web应用的核心,也是最常见的性能瓶颈。
一个性能好的数据库,查询速度快,并发能力强,能支撑大量用户的访问;一个性能差的数据库,查询速度慢,并发能力弱,稍微有点流量,就会卡顿,甚至崩溃。数据库的性能,直接决定了Web应用的性能上限。
MySQL,作为最流行的开源关系型数据库,被广泛应用于各种Web应用中。但很多开发者,对MySQL的性能优化,了解不多,只会写简单的SQL,不知道如何优化查询,如何设计表结构,如何配置服务器,导致数据库性能很差,影响了整个应用的性能。
我自己,做了多年Web开发,和MySQL打了很多年交道。从最初的只会写简单的增删改查,到后来关注SQL优化,再到现在能够系统地进行数据库性能优化,一路走来,踩了不少坑,也总结了不少方法。我见过太多的应用,因为数据库设计不合理、SQL写得差、没有索引、配置不当,而导致性能问题,其实,只要稍微优化一下,性能就能提升好几倍,甚至几十倍。
2015年,MySQL 5.6已经非常成熟,MySQL 5.7也已经发布(2015年10月发布GA版),性能有了很大提升。InnoDB存储引擎,已经成为默认的存储引擎,取代了MyISAM。各种性能优化工具和方法,也已经非常成熟。
今天,分享MySQL性能优化,从SQL优化、索引优化、表结构设计、配置优化、架构优化等多个维度,全方位讲解MySQL性能优化的方法和技巧,帮你让你的数据库飞起来。
性能优化的原则
在讲具体的优化方法之前,先讲几个数据库性能优化的基本原则。
1. 先测量,再优化
和PHP性能优化一样,数据库优化,也不能靠猜测,而要靠数据。
在优化之前,先用工具测量,找到性能瓶颈在哪里。是SQL查询慢?还是索引不合理?还是表结构设计有问题?还是服务器配置不当?还是并发太高?
只有找到了真正的瓶颈,才能有针对性地优化,事半功倍。如果靠猜测优化,可能会优化了不该优化的地方,浪费时间和精力,甚至可能引入新的问题。
常用的MySQL性能测量工具:
- 慢查询日志(Slow Query Log):记录执行时间超过指定阈值的SQL,是找到慢查询的最常用工具
- EXPLAIN:分析SQL的执行计划,查看索引使用情况、扫描行数、连接类型等
- SHOW PROFILE:分析SQL执行的各个阶段的耗时
- Performance Schema:MySQL的性能监控架构,可以监控各种性能指标
- MySQL Workbench:MySQL官方的图形化工具,有性能监控和分析功能
- pt-query-digest:Percona Toolkit中的慢查询分析工具,能分析慢查询日志,找出最耗时的SQL
- New Relic、Datadog等APM工具:应用性能监控工具,可以监控数据库查询性能
2. 优化瓶颈,而不是全部
数据库优化,也要遵循"二八定律":80%的性能问题,来自20%的SQL或表。
所以,优化的时候,要聚焦于那20%的瓶颈SQL和表,而不是全部。优化瓶颈,能以最小的成本,获得最大的性能提升。
比如,如果一个网站,80%的数据库时间,都花在几个慢查询上,那么优化这几个慢查询,就能获得最大的性能提升;而如果去优化那些只占5%时间的简单查询,效果就很有限。
3. 权衡优化的成本和收益
数据库优化,也是有成本的。有些优化,需要修改大量SQL和表结构,增加复杂度,但性能提升有限;有些优化,只需要简单修改,就能获得显著的性能提升。
所以,优化的时候,要权衡成本和收益。优先做那些成本低、收益高的优化;对于成本高、收益低的优化,可以考虑不做,或者延后做。
比如,加索引,通常是成本低、收益高的优化,应该优先做;而分库分表,是成本高、复杂度高的优化,应该在确实需要的时候才做。
4. 优化后要验证
优化完成后,要再次测量,验证优化是否有效,是否引入了新的问题。
有些优化,可能在测试环境有效,但在生产环境无效;有些优化,可能提升了查询性能,但降低了写入性能;有些优化,可能引入了新的bug。
所以,优化后,一定要验证,确保优化是有效的、安全的。
5. 不要过度优化
不要为了一点点性能提升,而把数据库设计搞得很复杂,降低可维护性。比如,为了减少一次查询,而把数据冗余到很多表中,导致数据不一致的风险增加;为了提升查询速度,而加太多索引,导致写入性能下降。
性能优化,要权衡成本和收益,在性能和可维护性之间,找到平衡点。
SQL优化
SQL优化,是数据库性能优化中,最常见,也是最有效的优化方式。很多时候,一条写得差的SQL,可能会比写得好的SQL,慢几十倍,甚至几百倍。
1. 避免SELECT *
SELECT 会查询所有字段,即使不需要的字段也会查询,浪费内存和网络带宽。而且,SELECT 无法使用覆盖索引,可能会导致回表查询,性能更差。
只查询需要的字段,能减少数据传输,提升性能,也更容易使用覆盖索引。
不好的写法:
SELECT * FROM users WHERE id = 1;
SELECT * FROM posts WHERE category_id = 1;好的写法:
SELECT id, name, email FROM users WHERE id = 1;
SELECT id, title, created_at FROM posts WHERE category_id = 1;2. 用EXISTS代替IN
在某些情况下,EXISTS比IN性能更好,特别是当子查询的结果集很大时。
EXISTS是找到一条匹配的记录就返回,不需要扫描全部结果;而IN需要先执行子查询,得到全部结果,再进行匹配。
不好的写法:
SELECT * FROM posts WHERE user_id IN (SELECT id FROM users WHERE status = 1);好的写法:
SELECT * FROM posts p WHERE EXISTS (SELECT 1 FROM users u WHERE u.id = p.user_id AND u.status = 1);注意:这不是绝对的,在某些情况下,IN可能比EXISTS好。要根据实际情况,用EXPLAIN分析,选择性能更好的写法。MySQL 5.6及以上版本,对IN子查询有优化,性能已经不错了。
3. 避免在WHERE子句中对字段进行函数操作或运算
在WHERE子句中,对索引字段进行函数操作或运算,会导致索引失效,全表扫描。
不好的写法:
-- 对字段使用函数,索引失效
SELECT * FROM posts WHERE YEAR(created_at) = 2015;
SELECT * FROM users WHERE LOWER(email) = 'test@example.com';
-- 对字段进行运算,索引失效
SELECT * FROM orders WHERE price * 1.1 > 100;好的写法:
-- 用范围查询代替函数
SELECT * FROM posts WHERE created_at >= '2015-01-01' AND created_at < '2016-01-01';
-- 存储时就存小写,查询时用小写
SELECT * FROM users WHERE email = 'test@example.com';
-- 把运算放到常量那边
SELECT * FROM orders WHERE price > 100 / 1.1;4. 避免模糊查询的前置通配符
LIKE查询中,如果通配符%在前面,会导致索引失效,全表扫描。
不好的写法:
-- 前置通配符,索引失效
SELECT * FROM users WHERE name LIKE '%张%';
SELECT * FROM posts WHERE title LIKE '%MySQL%';好的写法:
-- 后置通配符,可以使用索引
SELECT * FROM users WHERE name LIKE '张%';
SELECT * FROM posts WHERE title LIKE 'MySQL%';如果确实需要全文模糊查询,应该使用全文索引(FULLTEXT INDEX),或者使用搜索引擎(如Elasticsearch、Sphinx),而不是用LIKE '%xxx%'。
5. 避免OR连接多个条件
OR连接多个条件时,如果其中有一个条件没有索引,就会导致全表扫描。
不好的写法:
SELECT * FROM posts WHERE category_id = 1 OR user_id = 2;好的写法:
-- 用UNION代替OR
SELECT * FROM posts WHERE category_id = 1
UNION
SELECT * FROM posts WHERE user_id = 2;或者,给所有OR条件的字段都加上索引,MySQL也可能会使用索引合并(index merge)。但用UNION,通常更可靠。
6. 用UNION ALL代替UNION
UNION会对结果集进行去重,需要排序和比较,性能较差;而UNION ALL不去重,直接合并结果集,性能更好。
如果确定两个结果集没有重复数据,应该用UNION ALL代替UNION。
不好的写法:
SELECT id, title FROM posts WHERE category_id = 1
UNION
SELECT id, title FROM posts WHERE user_id = 2;好的写法:
SELECT id, title FROM posts WHERE category_id = 1
UNION ALL
SELECT id, title FROM posts WHERE user_id = 2;7. 避免子查询,尽量用JOIN
在MySQL中,子查询的性能,通常不如JOIN。特别是相关子查询(子查询中引用了外层查询的字段),性能很差。
不好的写法:
-- 相关子查询,性能差
SELECT p.*, (SELECT name FROM users u WHERE u.id = p.user_id) AS user_name
FROM posts p;好的写法:
-- 用JOIN代替子查询
SELECT p.*, u.name AS user_name
FROM posts p
LEFT JOIN users u ON u.id = p.user_id;注意:MySQL 5.6及以上版本,对子查询有优化,会自动把某些子查询转换为JOIN,性能已经不错了。但用JOIN,通常更直观,也更容易优化。
8. 分页优化
大数据量的分页,LIMIT offset, count会越来越慢,因为MySQL需要扫描offset条记录,然后丢弃。
不好的写法:
-- offset越大越慢
SELECT * FROM posts ORDER BY id LIMIT 100000, 10;好的写法:
-- 游标分页(基于上一页的最后一条记录的ID)
SELECT * FROM posts WHERE id < 100000 ORDER BY id DESC LIMIT 10;
-- 延迟关联(先查ID,再关联查询详情)
SELECT p.* FROM posts p
INNER JOIN (SELECT id FROM posts ORDER BY id LIMIT 100000, 10) t ON p.id = t.id;另外,要限制最大页数,不允许翻到太后面。大多数用户,只会看前几页,不需要翻到第10000页。
9. 批量操作
批量插入、批量更新、批量删除,比逐条操作,性能好很多。因为逐条操作,每次都要开启事务、写日志、刷新磁盘,开销很大;而批量操作,只需要一次事务,一次日志,一次磁盘刷新,开销小很多。
不好的写法:
// 逐条插入,性能差
foreach ($users as $user) {
$pdo->query("INSERT INTO users (name, email) VALUES ('$user[name]', '$user[email]')");
}好的写法:
// 批量插入,性能好
$values = [];
foreach ($users as $user) {
$values[] = "('" . $pdo->quote($user['name']) . "', '" . $pdo->quote($user['email']) . "')";
}
$sql = "INSERT INTO users (name, email) VALUES " . implode(',', $values);
$pdo->query($sql);注意:批量操作时,要注意SQL的长度,不要一次插入太多数据,导致SQL过长。可以分批,每批100-1000条。
10. 使用预处理语句
预处理语句(Prepared Statements),不仅能防止SQL注入,还能提升性能。
预处理语句,会把SQL语句发送给MySQL编译,然后可以多次执行,只需要传递参数。对于多次执行的相同SQL,预处理语句能减少MySQL编译的开销,提升性能。
而且,预处理语句,能减少SQL注入的风险,是更安全的写法。
示例:
// 使用PDO预处理
$stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (:name, :email)");
foreach ($users as $user) {
$stmt->execute([':name' => $user['name'], ':email' => $user['email']]);
}索引优化
索引,是数据库查询优化的最重要手段。合适的索引,能让查询速度提升几个数量级;而不合适的索引,不仅不能提升性能,还会降低写入性能,浪费存储空间。
1. 索引的类型
主键索引(PRIMARY KEY):主键自动创建索引,唯一且非空。一张表只能有一个主键索引。
唯一索引(UNIQUE):值唯一,可以为空。一张表可以有多个唯一索引。
普通索引(INDEX/KEY):最基本的索引,没有唯一性限制。
联合索引(复合索引):多个字段组成的索引,遵循"最左前缀原则"。
全文索引(FULLTEXT):用于全文搜索,支持自然语言搜索和布尔搜索。
2. 索引的使用原则
在WHERE、JOIN、ORDER BY、GROUP BY的字段上建索引:这些字段,是查询中经常用到的字段,建索引能显著提升查询性能。
区分度高的字段,适合建索引:区分度,是指不同值的数量占总记录数的比例。区分度越高,索引的效果越好。比如,用户ID、邮箱、手机号等,区分度很高,适合建索引;而性别、状态等,只有几个值,区分度很低,不适合建索引。
联合索引,把区分度高的字段放前面:联合索引,遵循最左前缀原则,查询时,会从最左边的字段开始匹配。把区分度高的字段放前面,能更快地过滤数据。
不要过度建索引:索引,会增加写入的开销(INSERT、UPDATE、DELETE时,需要更新索引),也会占用存储空间。一张表,索引不是越多越好,一般3-5个索引比较合适。
定期检查索引使用情况,删除未使用的索引:随着业务的变化,有些索引可能不再使用了,应该及时删除,减少写入开销和存储空间。
3. 最左前缀原则
联合索引,遵循最左前缀原则。比如,有一个联合索引(a, b, c),那么以下查询,能使用索引:
WHERE a = 1WHERE a = 1 AND b = 2WHERE a = 1 AND b = 2 AND c = 3WHERE a = 1 AND c = 3(只能用到a的索引,c用不到)
而以下查询,不能使用索引:
WHERE b = 2(没有最左边的a)WHERE c = 3(没有最左边的a)WHERE b = 2 AND c = 3(没有最左边的a)
所以,建联合索引时,要把查询中最常用的字段,放最左边。
4. 覆盖索引
如果一个索引,包含了查询需要的所有字段,那么MySQL就不需要回表查询数据,直接从索引中就能获取所有数据,这就是覆盖索引。覆盖索引,能显著提升查询性能。
示例:
-- 有联合索引 (category_id, created_at, title)
SELECT id, title, created_at FROM posts WHERE category_id = 1 ORDER BY created_at DESC;这个查询,需要的字段(id、title、created_at),都在索引中(id是主键,InnoDB中主键会自动包含在二级索引中),所以MySQL可以直接从索引中获取数据,不需要回表,性能很好。
要使用覆盖索引,就要避免SELECT *,只查询需要的字段,并且让这些字段,都包含在索引中。
5. 索引失效的场景
以下场景,会导致索引失效:
- 在索引字段上使用函数或运算(如
YEAR(created_at)、price * 1.1) - 模糊查询前置通配符(如
LIKE '%xxx%') - 隐式类型转换(如字符串字段用数字查询,或数字字段用字符串查询)
- OR连接的条件中,有一个没有索引
- 联合索引不满足最左前缀原则
- 使用
!=、<>、NOT IN等负向查询(不一定失效,但优化器可能选择全表扫描) - MySQL优化器认为全表扫描比索引更快(如小表,或查询返回大部分数据)
6. 查看索引使用情况
用EXPLAIN分析SQL的执行计划,查看索引使用情况:
EXPLAIN SELECT * FROM posts WHERE category_id = 1 ORDER BY created_at DESC;关键字段:
type:连接类型,从好到差:system>const>eq_ref>ref>range>index>ALL。ALL是全表扫描,需要优化。key:实际使用的索引。如果为NULL,说明没有使用索引。rows:扫描的行数。越少越好。Extra:额外信息。Using index表示使用了覆盖索引;Using where表示使用了WHERE过滤;Using filesort表示需要额外排序,需要优化;Using temporary表示使用了临时表,需要优化。
表结构设计优化
表结构设计,是数据库性能优化的基础。一个设计合理的表结构,能从根本上提升性能;而一个设计不合理的表结构,即使SQL和索引优化得再好,性能也有限。
1. 选择合适的数据类型
选择合适的数据类型,能节省存储空间,提升查询性能。
整数类型:
TINYINT:1字节,范围-128到127(或0到255无符号),适合状态、年龄等小整数SMALLINT:2字节,范围-32768到32767,适合较小的整数MEDIUMINT:3字节,范围-8388608到8388607INT:4字节,范围-21亿到21亿,最常用的整数类型BIGINT:8字节,范围很大,适合主键、大整数
选择最小的、能满足需求的数据类型。比如,状态字段,用TINYINT就够了,不要用INT;用户ID,如果不会超过21亿,用INT就够了,不要用BIGINT。
字符串类型:
CHAR(n):定长字符串,最多255字符。查询速度快,但浪费空间。适合长度固定的字段,如MD5值、邮编。VARCHAR(n):变长字符串,最多65535字节。节省空间,但查询稍慢。适合长度变化的字段,如标题、名称。TEXT:大文本,最多65535字节。不能有默认值,不能全部索引。适合长文本,如文章内容。MEDIUMTEXT:中等文本,最多16MB。LONGTEXT:长文本,最多4GB。
VARCHAR的长度,要根据实际需求设置,不要设置得太大。比如,标题字段,用VARCHAR(200)就够了,不要用VARCHAR(1000)。
日期时间类型:
DATE:日期,3字节,格式'YYYY-MM-DD'TIME:时间,3字节,格式'HH:MM:SS'DATETIME:日期时间,8字节,格式'YYYY-MM-DD HH:MM:SS',范围'1000-01-01'到'9999-12-31'TIMESTAMP:时间戳,4字节,格式'YYYY-MM-DD HH:MM:SS',范围'1970-01-01'到'2038-01-19',会自动转换时区
2015年,推荐用DATETIME,因为范围更大,不受2038年限制。TIMESTAMP因为有2038年问题,不推荐使用(虽然MySQL 5.6之后有改进,但还是有隐患)。
其他类型:
DECIMAL(m, d):精确小数,适合金额、价格等需要精确计算的字段。不要用FLOAT或DOUBLE存金额,因为会有精度问题。ENUM:枚举类型,适合状态、类型等只有几个固定值的字段。但要注意,修改ENUM的值,需要ALTER TABLE,在大数据量表上会锁表。BOOLEAN:布尔类型,实际上是TINYINT(1),0表示false,非0表示true。
2. 尽量使用NOT NULL
尽量把字段设置为NOT NULL,并设置默认值。
NULL字段,会占用更多的存储空间(InnoDB中,NULL需要额外的1位来标记),也会让索引、比较、排序更复杂,影响性能。而且,NULL在查询时,容易出问题(如WHERE field = NULL永远不会匹配,要用WHERE field IS NULL)。
所以,除非确实需要NULL(如可选字段),否则,尽量用NOT NULL,并设置默认值。比如,状态字段,默认0;计数字段,默认0;字符串字段,默认空字符串。
3. 合理使用范式和反范式
范式化设计,能减少数据冗余,保证数据一致性,但可能会导致多表JOIN,影响查询性能。
反范式化设计,通过冗余数据,减少JOIN,提升查询性能,但会增加数据冗余,可能导致数据不一致,也会增加写入的开销。
在实际应用中,要权衡范式和反范式,根据业务需求,选择合适的设计。
- 经常需要JOIN查询的字段,可以考虑冗余到主表中,减少JOIN。比如,文章表中,冗余用户名和头像,不需要每次都JOIN用户表。
- 但冗余字段,要注意数据一致性。如果用户修改了用户名,需要同步更新所有文章表中的冗余用户名。可以用触发器、应用层同步、或定时任务来保证一致性。
- 对于经常变化的字段,不适合冗余,因为同步更新的成本太高。
4. 大表拆分
当一张表的数据量很大(如超过1000万条),查询和写入性能,都会下降。这时候,可以考虑拆分表。
水平拆分(分表):把一张表的数据,按照某个维度(如ID范围、时间、哈希),拆分到多张表中。每张表的结构相同,数据不同。
比如,文章表,可以按ID范围拆分:posts01000万、posts1000万2000万;也可以按时间拆分:posts2015、posts2016;也可以按用户ID哈希拆分:posts0到posts63(64张表)。
水平拆分,能减少单表的数据量,提升查询和写入性能。但会增加应用层的复杂度(需要路由到正确的表),跨表查询和聚合也比较麻烦。
垂直拆分:把一张表的字段,拆分到多张表中。把不常用的、大的字段,拆分到单独的表中。
比如,文章表,可以把文章内容(content,大字段)拆分到postcontents表中,文章主表只存id、title、excerpt、categoryid等常用字段。这样,主表更小,查询更快;只有在查看文章详情时,才去查询内容表。
垂直拆分,能减少单表的字段数和数据量,提升常用查询的性能。但会增加JOIN的需求。
5. 避免在数据库中存储大文件
不要在数据库中存储大文件(如图片、视频、文档)。大文件,会让表变得很大,查询很慢,备份和恢复也很麻烦。
应该把大文件,存储在文件系统或对象存储(如阿里云OSS、腾讯云COS、七牛云)中,数据库中只存储文件的路径或URL。
配置优化
MySQL的配置,对性能影响很大。合理的配置,能充分利用服务器资源,提升性能。
1. InnoDB缓冲池(innodbbufferpool_size)
这是InnoDB最重要的配置参数。缓冲池,是InnoDB用来缓存表数据和索引的内存区域。缓冲池越大,能缓存的数据就越多,磁盘IO就越少,性能就越好。
建议:设置为物理内存的50%-70%。如果服务器只跑MySQL,可以设置为70%-80%。
比如,服务器有8G内存,可以设置innodbbufferpoolsize = 5G;有16G内存,可以设置innodbbufferpoolsize = 10G。
注意:不要设置得太大,要给操作系统和其他进程,留出足够的内存。
2. InnoDB日志文件(innodblogfile_size)
InnoDB日志文件(redo log),用于崩溃恢复。日志文件越大,崩溃恢复的时间越长,但需要刷盘的次数越少,写入性能越好。
建议:设置为256M-1G。MySQL 5.6默认是48M,太小了,建议调大。
比如,innodblogfile_size = 256M或512M。
注意:修改这个参数,需要先停止MySQL,删除旧的日志文件(iblogfile0、iblogfile1),再启动MySQL,否则会报错。
3. InnoDB日志缓冲(innodblogbuffer_size)
日志缓冲,是InnoDB用来缓存redo log的内存区域。缓冲越大,在事务提交前,需要刷盘的次数越少,写入性能越好。
建议:设置为8M-64M。默认是8M,对于大多数应用足够了。如果有很多大事务,可以调大到16M或32M。
4. 事务提交刷盘策略(innodbflushlogattrx_commit)
这个参数,控制事务提交时,redo log的刷盘策略,对写入性能和数据安全,影响很大。
0:每秒刷一次盘,事务提交时不刷盘。性能最好,但崩溃时可能丢失1秒的数据。1:每次事务提交都刷盘。最安全,不会丢失数据,但性能最差。默认值。2:每次事务提交都写到操作系统缓冲区,每秒刷一次盘。性能较好,崩溃时(MySQL崩溃但操作系统不崩溃)不会丢失数据,但操作系统崩溃可能丢失1秒数据。
建议:
- 如果对数据安全要求很高(如金融、支付),用
1。 - 如果对性能要求高,能接受少量数据丢失(如博客、论坛),用
2。 0不推荐,因为MySQL崩溃就可能丢数据。
5. 每个表独立表空间(innodbfileper_table)
这个参数,控制InnoDB是否为每张表创建独立的表空间文件(.ibd文件)。
ON:每张表有独立的.ibd文件。可以回收空间(TRUNCATE或DROP表时,空间会还给操作系统),也方便迁移和备份。OFF:所有表的数据,都存在共享表空间(ibdata1)中。表删除后,空间不会还给操作系统,ibdata1会越来越大。
建议:设置为ON。MySQL 5.6及以上版本,默认就是ON。
6. 连接数(max_connections)
最大连接数,控制MySQL同时接受的最大客户端连接数。
建议:根据服务器配置和应用需求设置。一般设置为100-1000。
不要设置得太大,因为每个连接,都会占用一定的内存(每个连接约占1M-几M内存),连接太多,可能会导致内存不足。
可以用SHOW STATUS LIKE 'Maxusedconnections'查看历史最大连接数,根据这个值,合理设置max_connections。
7. 排序缓冲(sortbuffersize)
排序缓冲,用于ORDER BY和GROUP BY的排序。如果排序的数据,能放在排序缓冲中,就可以在内存中排序,不需要使用临时文件(filesort),性能更好。
建议:设置为256K-4M。默认是256K。不要设置得太大,因为每个连接,都会分配独立的sort_buffer,连接多时,会占用大量内存。
8. 临时表大小(tmptablesize和maxheaptable_size)
这两个参数,控制内存临时表的大小。如果临时表的大小,超过了这个值,就会把临时表,写到磁盘上,性能下降。
建议:设置为64M-256M。两个参数,要设置成一样的值。
比如,tmptablesize = 128M,maxheaptable_size = 128M。
9. 查询缓存(querycachetype和querycachesize)
查询缓存,会缓存SELECT查询的结果,下次相同的查询,直接返回缓存结果,不需要执行SQL。
但是,查询缓存,有很多问题:
- 只要表有任何更新(INSERT、UPDATE、DELETE),这个表的所有查询缓存,都会失效。对于更新频繁的表,查询缓存命中率很低,反而会影响性能。
- 查询缓存,需要额外的内存和CPU开销。
- MySQL 8.0已经移除了查询缓存。
建议:2015年,MySQL 5.6/5.7,可以开启查询缓存,但要评估命中率。如果命中率低,就关闭。对于大多数Web应用(更新频繁),建议关闭查询缓存。
query_cache_type = 0
query_cache_size = 010. 字符集
推荐使用utf8mb4字符集,支持完整的Unicode(包括emoji表情)。
[client]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4
[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init_connect = 'SET NAMES utf8mb4'注意:不要用utf8,因为MySQL的utf8不是真正的UTF-8,最多只支持3字节,不支持emoji等4字节字符。要用utf8mb4。
架构优化
当单台MySQL服务器,无法满足性能需求时,就需要进行架构优化。
1. 主从复制
主从复制,是把一台MySQL服务器(主库)的数据,复制到另一台或多台MySQL服务器(从库)。
主库,负责写操作(INSERT、UPDATE、DELETE);从库,负责读操作(SELECT)。通过读写分离,提升读性能。
优点:
- 读写分离,提升读性能
- 数据备份,从库可以作为备份
- 高可用,主库挂了,可以提升从库为主库
缺点:
- 主从延迟,从库的数据,可能比主库晚一点
- 增加架构复杂度
- 需要应用层支持读写分离
2. 读写分离
在主从复制的基础上,应用层把写操作,路由到主库;把读操作,路由到从库。
可以用中间件(如MySQL Proxy、Atlas、MyCat、ProxySQL)来实现读写分离,也可以在应用层自己实现。
注意:对于实时性要求高的读操作(如刚写完就需要读),应该走主库,避免主从延迟的问题。
3. 分库分表
当单库单表的数据量太大,无法满足性能需求时,就需要分库分表。
分库:把一个库的数据,拆分到多个库中。可以按业务模块分库(如用户库、订单库、商品库),也可以按某个维度分库(如按用户ID哈希)。
分表:把一张表的数据,拆分到多张表中。前面表结构设计部分,已经讲过水平拆分和垂直拆分。
分库分表,能显著提升性能和容量,但会大大增加架构复杂度和应用层复杂度。跨库JOIN、分布式事务、全局ID、跨库查询聚合等,都是需要解决的问题。
只有在单库单表确实无法满足需求时,才考虑分库分表。不要过早分库分表,因为复杂度太高。
4. 引入缓存
在数据库前面,加一层缓存(如Redis、Memcached),把热点数据,缓存到内存中,减少数据库查询。
缓存,是提升Web应用性能,最有效的手段之一。大多数读多写少的应用,都适合用缓存。
注意:缓存,会带来缓存穿透、缓存雪崩、缓存击穿等问题,需要处理。前面PHP性能优化部分,已经讲过缓存的问题和解决方案。
5. 使用搜索引擎
对于全文搜索、复杂搜索、聚合搜索等场景,MySQL的性能,可能不够。这时候,可以引入搜索引擎(如Elasticsearch、Sphinx、Solr),专门处理搜索。
把数据,同步到搜索引擎中,搜索请求,走搜索引擎,而不是MySQL。这样,能提升搜索性能,也能减轻MySQL的压力。
其他优化技巧
1. 定期优化表
用OPTIMIZE TABLE,整理表碎片,回收空间,提升性能。特别是对于经常有DELETE和UPDATE的表,容易产生碎片。
OPTIMIZE TABLE posts;注意:OPTIMIZE TABLE会锁表,在大数据量表上,可能需要很长时间。应该在低峰期执行。
2. 定期分析表
用ANALYZE TABLE,更新表的索引统计信息,让优化器能选择更优的执行计划。
ANALYZE TABLE posts;3. 慢查询日志分析
开启慢查询日志,定期分析,找出慢查询,进行优化。
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1 # 执行时间超过1秒的SQL,记录到慢查询日志
log_queries_not_using_indexes = 1 # 记录没有使用索引的查询用pt-query-digest分析慢查询日志:
pt-query-digest /var/log/mysql/slow.log4. 合理使用事务
事务,能保证数据一致性,但也会锁定数据,影响并发性能。
- 事务要尽量短,不要在事务中做耗时的操作(如发送邮件、调用API、循环处理大量数据)
- 选择合适的事务隔离级别,隔离级别越高,并发性能越差
- 避免长事务,长事务会导致undo log膨胀,锁等待时间长
- 批量操作,使用事务,能提升性能(因为每次INSERT都会提交事务,批量操作只提交一次)
5. 避免锁等待
InnoDB使用行级锁,但在某些情况下,会升级为表锁,或者导致锁等待。
- 避免在事务中,更新大量行,会锁定很多行,影响并发
- 避免长时间不提交事务,会导致锁不释放
- 合理使用索引,避免因为没有索引,而导致全表扫描,锁定很多行
- 对于热点行(如库存计数),可以考虑用缓存,减少数据库锁竞争
总结
MySQL性能优化要点:
- 优化原则:先测量再优化(慢查询日志EXPLAIN SHOW PROFILE Performance Schema pt-query-digest APM工具)、优化瓶颈而不是全部(二八定律80%问题来自20%SQL或表)、权衡成本收益(优先成本低收益高的如加索引,分库分表成本高延后做)、优化后验证(确保有效安全不引入新问题)、不过度优化(性能和可维护性找平衡)
- SQL优化:避免SELECT (只查需要字段减少传输易使用覆盖索引)、用EXISTS代替IN(EXISTS找到匹配就返回不需要扫描全部,MySQL5.6对IN有优化需用EXPLAIN分析)、避免WHERE中对字段函数操作或运算(YEAR(created_at) LOWER(email) price1.1导致索引失效,用范围查询存储小写运算放常量端)、避免模糊查询前置通配符(LIKE '%xxx%'索引失效,用后置通配符或全文索引搜索引擎)、避免OR连接多条件(一个没索引就全表扫描,用UNION代替或给所有OR字段加索引用索引合并)、用UNION ALL代替UNION(UNION去重需排序比较性能差,确定无重复用UNION ALL)、避免子查询尽量用JOIN(相关子查询性能差,MySQL5.6有优化但JOIN更直观易优化)、分页优化(LIMIT offset大了慢用游标分页基于ID或延迟关联先查ID再关联,限制最大页数)、批量操作(批量插入更新删除比逐条性能好,一次事务一次日志一次磁盘刷新,注意SQL长度分批100-1000条)、使用预处理语句(防注入+性能,多次执行相同SQL减少编译开销)
- 索引优化:索引类型(主键PRIMARY KEY唯一UNIQUE普通INDEX联合索引全文FULLTEXT)、使用原则(WHERE JOIN ORDER BY GROUP BY字段建索引,区分度高的字段适合如用户ID邮箱手机号,性别状态区分度低不适合,联合索引区分度高放前面,不要过度建索引3-5个合适,定期检查删除未使用索引)、最左前缀原则(联合索引(a,b,c)查询从最左开始匹配,a=1、a=1 AND b=2、a=1 AND b=2 AND c=3能用,b=2、c=3、b=2 AND c=3不能用,常用字段放最左)、覆盖索引(索引包含查询需要的所有字段不需要回表,避免SELECT *只查需要字段让字段包含在索引中)、索引失效场景(字段函数运算、前置通配符、隐式类型转换、OR一个没索引、不满足最左前缀、负向查询!= <> NOT IN不一定失效、优化器认为全表扫描更快如小表或返回大部分数据)、查看索引使用(EXPLAIN分析type从好到差system>const>eq_ref>ref>range>index>ALL,ALL全表扫描需优化;key实际使用索引NULL没使用;rows扫描行数越少越好;Extra中Using index覆盖索引Using filesort额外排序需优化Using temporary临时表需优化)
- 表结构设计优化:选择合适数据类型(整数TINYINT/SMALLINT/MEDIUMINT/INT/BIGINT选最小满足需求的;字符串CHAR定长快但浪费适合固定长度,VARCHAR变长节省空间适合变化长度,TEXT大文本不能默认值不能全部索引;日期DATETIME 8字节范围大推荐,TIMESTAMP 4字节有2038问题不推荐;DECIMAL精确小数适合金额不要用FLOAT DOUBLE;ENUM适合固定值但修改需ALTER;BOOLEAN=TINYINT(1))、尽量使用NOT NULL(NULL占更多空间索引比较排序复杂,查询容易出问题=NULL永远不匹配要用IS NULL,除非确实需要否则用NOT NULL设默认值)、合理使用范式和反范式(范式减少冗余保证一致但多JOIN影响性能,反范式冗余减少JOIN提升查询但冗余可能不一致增加写入开销,权衡选择,经常JOIN的字段可冗余如文章表冗余用户名头像,经常变化的字段不适合冗余,用触发器应用层同步定时任务保证一致性)、大表拆分(水平拆分按ID范围时间哈希分表减少单表数据量但增加应用复杂度跨表查询麻烦;垂直拆分把不常用大字段如文章content拆到单独表,主表更小查询更快,查看详情才查内容表但增加JOIN)、避免在数据库存大文件(图片视频文档存文件系统或对象存储OSS COS七牛,数据库只存路径URL)
- 配置优化:InnoDB缓冲池innodbbufferpoolsize(最重要,物理内存50-70%,只跑MySQL可70-80%,不要太大留内存给OS)、日志文件innodblogfilesize(256M-1G,默认48M太小,修改需停MySQL删旧日志文件)、日志缓冲innodblogbuffersize(8M-64M,默认8M够,大事务可调大)、事务提交刷盘innodbflushlogattrxcommit(0每秒刷性能最好崩溃丢1秒数据不推荐;1每次提交刷最安全性能差默认;2每次提交写OS缓冲每秒刷性能较好MySQL崩溃不丢OS崩溃可能丢,博客论坛用2金融支付用1)、独立表空间innodbfilepertable(ON每张表独立.ibd可回收空间方便迁移备份,MySQL5.6默认ON)、连接数maxconnections(100-1000根据服务器和需求,不要太大每个连接占内存,用Maxusedconnections查看历史最大)、排序缓冲sortbuffersize(256K-4M默认256K,不要太大每个连接独立分配)、临时表大小tmptablesize和maxheaptablesize(64M-256M设一样,超过就写磁盘性能下降)、查询缓存(更新频繁的表命中率低反而影响性能,MySQL8.0已移除,大多数Web应用建议关闭querycachetype=0 querycache_size=0)、字符集用utf8mb4(支持完整Unicode包括emoji,不要用utf8最多3字节不支持4字节)
- 架构优化:主从复制(主库写从库读,读写分离提升读性能,数据备份高可用,但主从延迟增加复杂度)、读写分离(写路由主库读路由从库,用中间件Atlas MyCat ProxySQL或应用层实现,实时性要求高的读走主库避免延迟)、分库分表(单库单表无法满足时才做,分库按业务模块或维度,分表水平垂直,提升性能容量但大大增加复杂度,跨库JOIN分布式事务全局ID跨库查询都是问题,不要过早分库分表)、引入缓存(Redis Memcached缓存热点数据减少数据库查询,读多写少适合,处理缓存穿透雪崩击穿)、使用搜索引擎(全文复杂聚合搜索用Elasticsearch Sphinx Solr,数据同步到搜索引擎搜索请求走搜索引擎,提升搜索性能减轻MySQL压力)
- 其他优化:定期优化表OPTIMIZE TABLE(整理碎片回收空间,会锁表低峰期执行)、定期分析表ANALYZE TABLE(更新索引统计信息让优化器选更优执行计划)、慢查询日志分析(开启slowquerylog longquerytime=1 logqueriesnotusingindexes,用pt-query-digest分析找出慢查询优化)、合理使用事务(事务尽量短不在事务中做耗时操作,选合适隔离级别,避免长事务,批量操作用事务提升性能)、避免锁等待(避免事务更新大量行,避免长时间不提交,合理用索引避免全表扫描锁很多行,热点行用缓存减少锁竞争)
MySQL性能优化,是一个系统工程,涉及SQL、索引、表结构、配置、架构等多个方面。没有银弹,需要根据实际情况,测量、分析、优化、验证,持续迭代。
但大多数时候,我们不需要做很复杂的优化,只需要把基础做好:写好SQL、建好索引、设计好表结构、配置好服务器,就能解决80%的性能问题。
"数据库性能,决定了Web应用的性能上限。"希望这篇文章,能帮你掌握MySQL性能优化的方法和技巧,让你的数据库飞起来。
最后,记住:先测量,再优化;优化瓶颈,而不是全部;权衡成本和收益;优化后验证。 这是MySQL性能优化的金科玉律,也是所有性能优化的通用原则。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录