Django 分页为什么每次都多一条 COUNT(*)?大表上翻到最后一页会慢多少?有什么替代?这篇在 100 万行的表上量每一部分。
SELECT COUNT(*) AS "__count" FROM "lab_item" SELECT "lab_item"."id", "lab_item"."name", "lab_item"."created" FROM "lab_item" ORDER BY "lab_item"."id" ASC LIMIT 25 OF 同一个 Paginator 再取第 4 页时,新增的 COUNT 查询: 0 (count 是缓存的) p.count = 1000000 p.num_pages = 40000
Paginator.count 是 cached_property,每个 Paginator 对象只查一次。但每个请求通常新建一个 Paginator,所以每个请求都会 COUNT(*)。
SELECT COUNT(*): 14.1 ms 第 1 页(LIMIT 25): 0.26 ms 第 20000 页(OFFSET 499975): 21.4 ms 第 40000 页(OFFSET 999975): 42.5 ms Paginator(qs, 25).page(1) 整体(含 COUNT): 14.4 ms Paginator(qs, 25).page(40000) 整体: 57.5 ms 用 keyset(WHERE created > 999975 ORDER BY 索引列 LIMIT 25): 0.32 ms 过滤后的 COUNT(created < 500000 条件): 18.8 ms
PostgreSQL 的 COUNT(*) 需要扫描(索引或表)满足条件的所有行,行数越多越慢。OFFSET 需要先取出并丢弃前面的行。都是本机单次测量,数字只反映这台机器上这份数据的量级。
未排序的 QuerySet: ["Pagination may yield inconsistent results with an unordered object_list: <class 'lab.models.Item'> QuerySet."] 越界页 → EmptyPage: That page contains no results 子类里把 count 写死之后,COUNT 查询数: 0
要「上一页 / 下一页」的场景用 keyset;需要页码时可以缓存 count、或提供近似值(子类化 Paginator 重写 count)。分页永远要 order_by,并且排序键要唯一(否则页与页之间可能重复或遗漏)。