后台列表查询分层与分页治理
列表变慢先别急着加索引
后台列表变慢,先看查询模型,不要先加索引。
这类列表一开始通常都很简单:
1、按条件查表
2、分页返回
3、前端展示几列字段
但做着做着就会长出很多额外需求:
1、多条件筛选
2、多种排序
3、列表里直接带关联信息
4、导出和列表共用一套查询
5、详情页也复用同一个查询对象
问题往往不是单条 SQL 慢,而是一个查询承担了太多职责。
先看典型 SQL
这类列表 SQL 往往长这样:
1 | select * |
请求量上来以后,问题基本集中在三块:
1、orderByClause 动态开放过大
2、深分页 limit offset 越翻越慢
3、查询字段越来越多
查询是怎么一步步变重的
下面这个例子只说明问题,不代表固定耗时。
初始版本:
1 | select * from t_partner |
这个写法通常不是单点问题,而是字段过多、排序字段不稳、索引没贴住条件一起叠上来。
第一步:先把过滤条件和排序字段收拢,再补复合索引
1 | create index idx_status_create_update on t_partner(status, create_time, update_time); |
这一步通常能先收掉全表扫描,但还不代表列表已经稳定。
第二步:列表页只查必需列
1 | select id, name, status, create_time from t_partner |
如果列表请求量不大,这一步可能就够了。只有页码翻得很深时,才需要继续改分页模型。
第三步:深分页改游标分页
1 | -- 旧方式(第100页,offset=100000) |
所以只盯索引,很多时候治不了根。
数据分层先拆查询职责
这里说的数据分层,不是大架构概念,就是把查询职责拆开:
1、主列表只查核心字段
2、补充展示信息按需二次查询
3、详情页单独查明细
4、导出单独走一条查询链路
一个 SQL 同时兼顾列表、详情、导出、排序、筛选,最后一定会越来越重。
先收字段和排序
先看两件事:
1、列表到底需要哪些列
2、排序到底开放了哪些字段
很多列表前端只展示几列,但查询时已经把整张表的列都带出来了。再叠加大字段、JSON 字段,数据库和网络开销都会上来。
下面这个例子只说明一件事:列表字段要收。
1 | // 不好:直接返回整个entity,50列都查出来 |
动态排序要做白名单
orderByClause 最大的问题不是难写,而是太容易放开。
如果完全放开给前端传入,通常会出现两个后果:
1、前端开始按没有索引的字段排序
2、排序表达式不可控,维护和安全风险一起上来
更稳的做法是服务端只开放有限几个排序类型:
1 | if ("updateTimeDesc".equals(sortType)) { |
这样不算通用,但线上可控。
深分页要换模型
浅分页用 limit offset 还能接受,页码一深,扫描成本就会快速上去。
这种场景我更倾向换成基于游标或主键翻页:
1 | select id, name, status, update_time |
这不一定适合所有后台页面,但对深分页通常比继续扛 offset 更稳。
导出不要复用列表查询
这是很常见的坑。
导出通常具备两个特点:
1、数据量更大
2、字段更多
如果沿用列表页那套分页、排序、字段装配逻辑,最后两边都会别扭。
处理方式一般是:
1、列表保持轻量查询
2、导出单独做查询链路
3、数据量大时走异步导出
详情页也不要复用列表查询
列表和详情本来就是两种诉求:
1、列表要快,字段少
2、详情可以重一点,但只查单条
如果详情页继续复用列表查询,再在返回对象上不断补字段,最后列表查询很容易变成大而全接口。
列表变慢后的处理顺序
处理顺序直接按这个来:
1、先限制排序字段
2、再裁掉列表非必要列
3、再把详情和导出拆出去
4、最后再评估是否改成游标分页
核心就是先收查询模型,再谈索引和参数优化。
先告警,再决定是否降级
列表接口不是都值得自动降级。很多后台列表一天没多少请求,先把慢日志和分段耗时接起来更实用:
1 |
|
如果这个接口本身是高频后台入口,再考虑缓存或异步预热,不然很容易把问题从数据库挪到缓存一致性。
慢查询触发后先在数据库检查:
1 | SET GLOBAL slow_query_log = 'ON'; |
小结
后台列表慢,很多时候不是单条 SQL 写得差,而是一个接口承担了太多查询职责。
主列表只查核心字段,排序做白名单,深分页及时换模型,导出和详情拆独立链路,最后补监控和告警。这比单纯堆索引更稳。



