Django-ORM+喵喵CRM-慢查询的定位和优化(1)
Django-ORM+喵喵CRM-慢查询的定位和优化(1)

Django-ORM+喵喵CRM-慢查询的定位和优化(1)

最近用把简历甩给AI,发现目前连AI提的问题都搞不定……嘚哩,继续学习吧。

其实慢查询这个问题在Mysql学习以及喵喵CRM优化时候都涉及了,但是没能有效整合起来。

Django-ORM+喵喵CRM-N+1优化 – 红茶小馆

MySQL数据库学习-进阶索引(1) – 红茶小馆

这篇文章就尝试整合一下,主要介绍慢查询以及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()。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注