前言
"网站越来越慢了,数据库 CPU 一直在 100%。"这是很多站长在网站流量增长后都会遇到的困境。数据库性能瓶颈是个人网站从"能用"到"好用"必须跨越的一道坎。本文基于真实案例,从查询优化、索引设计、配置调优、架构演进四个维度,系统性地讲解 MySQL/MariaDB 的性能优化方法。
一、发现慢查询:优化的起点
在动手优化之前,首先要找到问题在哪里。MySQL 提供了慢查询日志功能,可以记录执行时间超过指定阈值的 SQL 语句。
开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';生产环境中建议将 long_query_time 设置为 1 秒。对于内容型网站,绝大多数查询应该在 50 毫秒内完成,超过 1 秒的查询已经属于严重慢查询。
分析慢查询日志
原始慢查询日志可读性较差,推荐使用 pt-query-digest 工具进行分析:
pt-query-digest /var/log/mysql/slow.log > slow_analysis.txt这个工具会自动聚合相似的查询,按总耗时和平均耗时排序,让你一眼看出最需要优化的查询是哪些。
二、索引优化:性价比最高的优化手段
索引是数据库性能优化中投入产出比最高的手段。一个正确的索引可以让查询速度提升几个数量级。
常见索引设计原则
第一,为 WHERE 子句中经常出现的列建立索引。如果一个查询条件 WHERE status = 1 AND create_time > '2025-01-01' 频繁执行,应该在 (status, create_time) 上建立联合索引。
第二,注意索引列的顺序。联合索引遵循"最左前缀"原则,选择性高的列放在前面。比如用户表,性别列的区分度很低(只有男女),而邮箱列的区分度很高,联合索引应该把邮箱放在前面。
第三,避免在索引列上使用函数。WHERE DATE(create_time) = '2025-01-01' 会导致索引失效,应该改写为 WHERE create_time >= '2025-01-01' AND create_time < '2025-01-02'。
实战案例
有一个文章表包含 50 万条数据,原来的查询:
SELECT id, title, views FROM articles WHERE status = 1 AND category_id = 5 ORDER BY views DESC LIMIT 20;执行时间 2.3 秒。通过 EXPLAIN 分析发现,查询使用了全表扫描。优化方案:在 (status, category_id, views) 上建立联合索引。
ALTER TABLE articles ADD INDEX idx_status_cat_views (status, category_id, views DESC);优化后查询时间降至 0.02 秒,速度提升了 100 倍以上。这就是索引的威力。
三、配置调优:让数据库发挥最大性能
很多站长使用的是云服务器的默认 MySQL 配置,这个配置通常只考虑兼容性,完全不为性能优化。以下参数值得重点关注:
InnoDB 缓冲池大小
这是最重要的性能参数。InnoDB 缓冲池用于缓存数据和索引,建议设置为服务器可用内存的 70% 左右。
innodb_buffer_pool_size = 2G如果你的服务器有 4GB 内存,将缓冲池设为 2.8GB 是比较合理的值,但要为操作系统和其他进程预留足够的内存。
查询缓存
MySQL 8.0 已经移除了查询缓存功能,因为在高并发场景下,查询缓存的全局锁会成为性能瓶颈。MariaDB 仍然保留查询缓存,但建议在写入频繁的网站上关闭它:
query_cache_type = 0
query_cache_size = 0连接数配置
max_connections = 500
thread_cache_size = 64连接数不是越大越好。每个连接都会消耗内存,过大的 max_connections 可能导致服务器内存耗尽。配合连接池使用是更好的方案。
日志相关
innodb_log_file_size = 512M
innodb_log_buffer_size = 64M增大日志文件大小可以减少日志刷盘频率,提升写入性能。但重启数据库后才能生效,需要在维护窗口操作。
四、SQL 语句优化技巧
避免 SELECT *
很多框架生成的 SQL 默认会查询所有字段,但实际业务可能只需要其中几个字段。例如文章列表页只需要标题和摘要,不需要文章正文这种大字段。
-- 不推荐
SELECT * FROM articles WHERE status = 1;
-- 推荐
SELECT id, title, summary, create_time FROM articles WHERE status = 1;合理使用 LIMIT
分页查询时,LIMIT 100000, 20 这样的写法会导致 MySQL 扫描 100020 行后丢弃前 100000 行。可以使用延迟关联优化:
-- 不推荐
SELECT * FROM articles ORDER BY id LIMIT 100000, 20;
-- 推荐
SELECT a.* FROM articles a
INNER JOIN (SELECT id FROM articles ORDER BY id LIMIT 100000, 20) tmp
ON a.id = tmp.id;使用批量操作代替逐条操作
-- 不推荐:逐条插入,每次都要建立网络连接
INSERT INTO logs VALUES (1, 'a');
INSERT INTO logs VALUES (2, 'b');
-- 推荐:批量插入
INSERT INTO logs VALUES (1, 'a'), (2, 'b');批量操作能将写入性能提升 5 到 10 倍。
五、架构演进:从单库到读写分离
当网站流量进一步增长,单库已经无法满足性能需求时,需要考虑读写分离架构。
主从复制搭建
# 主库配置
server-id = 1
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = ROW
# 从库配置
server-id = 2
relay_log = /var/log/mysql/mysql-relay-bin.log
read_only = 1主库处理写入操作,从库分担读取操作。对于内容型网站,读操作通常占 90% 以上,一主一从的架构就能将写入性能提升接近一倍。
使用中间件
对于小型网站,在应用层手动实现读写分离即可。对于更复杂的场景,推荐使用 ProxySQL 或 MaxScale 这类数据库中间件,它们可以自动路由读写请求、实现负载均衡和故障切换。
六、定期维护不可忽视
表优化
OPTIMIZE TABLE articles;定期执行表优化可以回收碎片空间、重建索引。建议在低峰期每周执行一次。
统计信息更新
ANALYZE TABLE articles;MySQL 的查询优化器依赖表的统计信息来生成执行计划。当数据大量变更后,统计信息可能过时,导致优化器选择了劣质的执行计划。建议在大量数据变更后执行 ANALYZE。
结语
数据库性能优化是一个系统工程,从最简单的索引优化到复杂的架构调整,每一步都能带来实实在在的性能提升。对于个人站长来说,不必一步到位追求最完美的架构。建议按照"发现慢查询、优化索引、调整配置、重构 SQL"的顺序逐步推进,每做完一步就验证效果。通常仅索引优化和配置调整就能解决 80% 以上的性能问题。记住一个原则:先优化后扩容,永远不要在没有做优化的情况下盲目增加硬件资源。