最近用把简历甩给AI,发现目前连AI提的问题都搞不定……嘚哩,继续学习吧。
其实慢查询这个问题在Mysql学习以及喵喵CRM优化时候都涉及了,但是没能有效整合起来。
这篇文章就尝试整合一下,主要介绍慢查询以及Django里面对于慢查询的定位和解决方案。
慢查询介绍
慢查询(Slow Query)= 数据库执行时间超过预期或超过阈值的 SQL,一般会导致:
- 接口响应变慢(用户感觉卡顿)
- 数据库连接被长时间占用(影响并发)
- 导致应用服务器阻塞(Gunicorn/Uvicorn worker 卡死)
- 拖垮整个系统
常规来说,慢查询通常来源于如下情况:
- 无索引扫描(全表扫描)
- 这个还是经常遇到的,没有索引就会这样
- JOIN 过多或 JOIN 字段无索引
- 这个也算常见,多个表组合起来查询
- N+1 查询
- 简单来说就是:一个客户,多个订单,然后因为分表,查一个客户没能把所有订单信息拉出来,导致需要每次一个客户再要进去单独查N个订单信息。
- 喵喵CRM优化就有这个,欢迎查看上一篇文章。
- 模糊搜索 like ‘%xxx’
- 这个没有注意过,简单介绍下,就是前置通配符会破坏左匹配原则,必须全表扫描,性能下降。
- 前置
%会让 BTREE 索引失效,MySQL 必须从第一条遍历到最后一条,挨个匹配 → 全表扫描,数据少还好,数据一多接口直接卡死,CPU 飙高。 - 不过说实话,Django里怎么处理模糊搜索我还真不知道。【待补】
- ORDER BY 无索引字段
- 如果排序的字段没有索引,也会全表扫描,然后把扫描结果写入临时表,再排序,再返回给应用。
- 大分页 offset N limit M
- `SELECT * FROM customer ORDER BY id LIMIT 100000, 20;
- 这个操作就会导致MySQL 扫描完前 10 万条,但只返回最后 20 条。
- 大量写操作导致锁表
- 这儿最初我有点懵,就是写操作不就是前端一次性提供给后端,数据库处理一下不就行了吗?怎么会导致大规模锁呢?下面具体解释下:
- 表面上我们点击提交 -> django save -> 成功。实际上内部往往是:开启事务 -> 定位要修改的行 -> 锁定这行或者范围 -> 执行更新/插入 -> 写入undo log/redo log -> 提交事务 -> 释放锁。
- 我们更新没有索引的字段,Mysql就需要全表扫描定位,因此需要锁住整个表避免更新插队。
- 我们要是更新一个有外键的字段,就会锁住相关索引,锁就会扩大。
- 当有多个写操作,那么数据库内部可能就会被堵死。
- 不过写操作不是问题,问题是写操作到底锁住了多少行以及锁了多久。
- 这儿最初我有点懵,就是写操作不就是前端一次性提供给后端,数据库处理一下不就行了吗?怎么会导致大规模锁呢?下面具体解释下:
慢查询定位
(1)实际上之前也有篇文章做了个中间件,那个皆可以当半个工具。不过实际上django有特有的debug toolbar/logging【待补】
(2)线上定位:数据库有专门的慢查询日志;
– SET GLOBAL slow_query_log = 1; -SET GLOBAL long_query_time = 1;`
(3)EXPLAIN分析执行计划,这个就很常见了,Mysql学习时候有接触。
– `EXPLAIN SELECT * FROM customer WHERE phone=’18012345678′;
– type字段:访问类型,all最差,range/index好,eq_ref/const最好;
– key字段:实际走的索引;
– rows字段:扫描的行数,越少越好;
– Extra字段:是否出现Using temporary / Using filesort(很差)
优化手段
(1)最基础的方法——建索引!
什么WHERE\JOIN\ORDER BY统统加上索引,嘎嘎。
不过不要给更新太频繁的字段加入索引哈。
(2)使用select_related / prefetch_related,避免N+1
见上一篇文章。
(3)Redis缓存热点数据。
配置类数据、读多写少的对象、数据统计结果啥的。喵喵CRM的权限缓存也算是一种慢查询优化吧~
(4)分页优化
- `SELECT * FROM customer ORDER BY id LIMIT 100000, 20;
这个问题解释一下,10万条数据,但是我们点到了最后一页,实际上我们只要最后20条,但是前面的9万多条数据还是会扫。
解决方案就是Keyset Pagination(游标分页)(不再跳过前面的数据,而是利用“上一次的最后一条记录”作为下一次查询的起点。)# 第一次查询 SELECT * FROM customer ORDER BY id LIMIT 10; # 第二次查询 SELECT * FROM customer WHERE id > 10 ORDER BY id LIMIT 10;- 但是有个问题……就是游标分页不支持跨页,比如我正在看第一页,突然想跳到第二十页,游标分页就不支持。所以游标分页支持的知乎那种下滑更新的列表。
- django里可以这样实现:【待测试】
last_id = request.query_params.get("last_id", 0) queryset = Customer.objects.filter(id__gt=last_id).order_by("id")[:10]
(5)模糊搜索优化
前缀匹配改成xxx%。
这个不多赘述,后续看看Django内部怎么实现模糊搜索的。
Django 特有的优化点【待补】
1. values() / only() / defer() 减少字段User.objects.only("id", "username")
减少字段 = 更少 IO = 更快
2. annotate() / aggregate() 在 DB 执行统计
比Python for 循环快 1000 倍。
3. 使用 Q 对象组合查询,减少多次查询User.objects.filter(Q(name='xx') | Q(phone='xx'))
4. 原生 SQL + 索引,解决复杂性能问题
复杂统计类 SQL 使用 raw() 或 connection.cursor()。