你的网站是不是越跑越慢?
后台点开一个订单列表,转圈转得你去泡了杯咖啡才加载出来。
用户刚点“提交订单”,页面就卡住,最后弹个“请求超时”——可服务器监控里CPU和内存都闲着,只有数据库连接数在疯狂跳红。
问题八成出在SQL上。不是代码写得烂,是查询没被好好“伺候”。
为什么你的SQL查询会慢如蜗牛?
慢,从来不是偶然。它背后就那几件事在捣鬼。
第一,没索引,或者索引没用上。
就像查字典不翻页码,直接从第一页往后翻——数据库对大表做全表扫描时,就是这么干的。
第二,SQL本身写得费劲。
比如 WHERE DATE(create_time) = '2023-10-01':函数一裹,索引当场失效,只能扫全表。
再比如 SELECT *:明明只要显示订单号和状态,却把商品详情、用户地址、日志备注这些几MB的字段全捞一遍,网络拖、内存占、解析也慢。
第三,JOIN用得太莽。
连五个表不加限制,或者把本该写在 ON 里的条件塞进 WHERE,数据库可能先拼出上百万行的中间结果,再筛——内存爆、磁盘狂转、响应直接挂。
真实案例:一个本地生活平台的团购订单页,原查询 SELECT * 关联了用户、商户、商品、门店、优惠券、支付记录、物流单……七八张表全拉进来。页面平均加载时间明显变长。我们砍掉不用的字段,把 JOIN 顺序按数据量从小到大排,关联条件全挪到 ON 里。改完当天,用户反馈“终于不卡了”。
如何精准定位拖后腿的慢查询?
别猜。先让数据库自己“打小报告”。
MySQL 自带慢查询日志,打开就能记下所有跑得慢的 SQL。
你只需要在配置文件(比如 my.cnf)里加三行:
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
重启 MySQL,等半天或一天,让它跑一跑真实流量。
然后打开终端,执行这句:
mysqldumpslow -s t /var/log/mysql/slow.log | head -20
你会看到一份“最耗时 SQL 排行榜”。排在前五的,就是你今晚该盯上的目标。
但光知道“谁慢”不够,还得知道“为啥慢”。这时候就轮到 EXPLAIN 出场。
复制排行榜第一条 SQL,在前面加上 EXPLAIN,回车执行。看返回结果里的几个关键字段:
type:要是ALL,说明正在全表扫描,必须处理;key:显示实际用了哪个索引,空着就等于没用上;rows:预估扫描行数,越大越危险;Extra:出现Using filesort或Using temporary?基本可以判定排序或分组逻辑需要重构。
索引真的是万灵药吗?怎么用才对?
索引不是贴膏药,贴多了反而碍事。
它像书签——帮你跳过无关章节,但每加一页书签,写新内容时就得同步更新它。频繁写的表,乱建索引会让插入、更新变慢。
建索引,盯住两件事:
1. 哪些字段值得建?
看你的 WHERE 条件、ORDER BY 字段、GROUP BY 字段、JOIN 的 ON 字段。这些地方反复出现的字段,优先考虑。
2. 字段值够不够“独特”?
用户手机号、订单号这种几乎不重复的字段,建索引效果立竿见影;
性别、状态(“待审核”“已完成”)这种只有两三个值的字段,建了也白建——数据库懒得走索引,直接扫表更快。
联合索引要注意顺序。比如 (category_id, publish_time) 这个组合:
✅ WHERE category_id = 5 可以用
✅ WHERE category_id = 5 AND publish_time > '2024-01-01' 可以用
❌ WHERE publish_time > '2024-01-01' 就用不上
所以,把筛选力最强、最常单独出现的字段放左边。
真实案例:一个政务服务平台的办事指南列表页,按“所属部门”筛选 + 按“更新时间”倒序。最初只在 dept_id 上建了单列索引,排序时总触发 Using filesort。换成 (dept_id, updated_at) 联合索引后,列表打开速度大幅增长,因为数据库能直接从索引里按顺序取出数据,不用额外排序。
除了加索引,还有哪些优化奇招?
高手不只靠索引,更靠“少做事”。
SELECT *是头号懒人写法。改成明确列出要用的字段,比如SELECT id, title, status, updated_at。WHERE里别对字段做计算:WHERE create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)比WHERE DATE(create_time) = ...友好得多。- JOIN 多张表时,把数据量最小的表放在
FROM最左边,让它当“驱动表”,减少中间结果集大小。
复杂报表别硬算。
比如每月统计各区域销量,与其每次查千万条订单实时聚合,不如每天凌晨跑个定时任务,把结果存进一张 monthly_region_sales 表。前端查这张小表,快得飞起。
分页别用 LIMIT 100000, 20。
数据库得先读100020行,再扔掉前10万——纯属浪费。换成游标式分页:记住上一页最后一条记录的 id,下一页查 WHERE id > 123456 LIMIT 20,走索引直接定位,效率提升非常明显。
缓存能用就用,但别迷信。
Redis 里缓存用户基本信息、站点配置、热门政策原文这类“读多写少”的数据很划算;
但订单状态、库存余量这种分钟级就变的数据,缓存反而容易出错,不如老老实实查库。
架构层面如何为数据库减负?
单机调优到极限还扛不住?那就别死磕一台机器了。
读写分离是第一步。
主库只管增删改,从库专门扛查询。Feed 流、搜索列表、个人中心这些纯读场景,全切到从库。压力立马分走一大半。
数据量真上亿了,再考虑拆。
垂直拆:用户相关表放 user_db,订单放 order_db,商品放 product_db——按业务边界切,运维和开发都清爽。
水平拆(分片):比如用户表按 user_id % 256 拆成 256 张子表,查用户时直接路由到对应分片,避免扫全表。
缓存要前置。
把 Redis 当“第一道门”:用户登录后先查缓存有没有他的基础信息;没命中再去查库,查完顺手塞进缓存,TTL 设 30 分钟。高频访问的页面配置、城市列表、常见问答,全扔进去。
真实案例:一个长三角区域的招聘平台,职位表半年涨到 8000 万条,首页推荐、搜索、公司详情页全都变慢。我们先切读写分离,把推荐流和搜索查询导到从库;接着按 company_id % 64 对职位表做水平分片;最后把热门公司简介、行业分类树、常用城市编码这些静态数据全缓存在 Redis。上线后 DB CPU 使用率缩短了不少,用户刷职位列表再没卡顿过。
今天下班前,你能立刻做哪一件事?
别等明天。现在打开你常用的数据库管理工具(phpMyAdmin / Navicat / DBeaver / 或直接连终端),就做这一件事:
打开 MySQL 配置,开启慢查询日志,并跑出前 10 条最慢 SQL。
具体步骤:
- 找到你的 MySQL 配置文件(Linux 通常是
/etc/my.cnf或/etc/mysql/my.cnf;Windows 是my.ini) - 在
[mysqld]区块下添加:slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 2 - 保存,然后在终端执行
sudo systemctl restart mysql(或sudo service mysql restart) - 让网站跑 3–4 小时(或等下班前)
- 回到终端,执行:
mysqldumpslow -s t /var/log/mysql/slow.log | head -10
你会看到一份真实、带时间戳、带执行次数的“慢 SQL 名单”。
挑第一条,复制它的 SQL,前面加上 EXPLAIN,执行。
看看 type 是不是 ALL,key 是不是空,rows 是不是高得离谱。
如果答案是“是”,今晚就给它加个索引,或者改改写法——明早刷新页面,你会明显感觉到不一样。