COUNT(*) OVER() 可在单次查询中同时返回分页数据和满足条件的总记录数,无需子查询或二次查询;需确保 WHERE 过滤后计算总数,并避免在 OVER() 中误加 ORDER BY 导致累计计数。

用 COUNT(*) OVER() 一次性拿到总数和分页数据
不用两次查询,也不用子查询套一层,COUNT(*) OVER() 是最直接的解法。它在不改变结果行数的前提下,给每一行都附上全表总记录数,配合 LIMIT/OFFSET 或 ROW_NUMBER() 就能同时输出总数和当前页数据。
常见错误是写成 COUNT(*) 聚合后丢掉明细——那只能拿到一个数字;或者用子查询先算总数再 JOIN,性能差且语义绕。
- PostgreSQL、SQL Server、Oracle、Snowflake、DuckDB 都支持;MySQL 8.0+ 也支持
- 避免在
WHERE条件里漏掉对总数的影响:总数应基于过滤后的结果集(即加WHERE后再OVER()),不是原始表 - 如果用
ROW_NUMBER() OVER(ORDER BY ...)做分页,注意排序字段必须有确定性(比如加主键兜底),否则同序号可能跨页漂移
PostgreSQL 分页示例:带总数的 LIMIT/OFFSET
这是最贴近应用层分页逻辑的写法,兼容 ORM 和手动拼 SQL 场景。
SELECT id, name, email, COUNT(*) OVER() AS total_count FROM users WHERE status = 'active' ORDER BY created_at DESC LIMIT 20 OFFSET 40;
返回 20 行用户数据,每行都带 total_count 字段,值等于满足 status = 'active' 的总人数。注意:OVER() 里没写 PARTITION BY 或 ORDER BY,所以是“全窗口”计数。
-
total_count对每行都一样,前端取任意一行的值即可 - 如果加了
ORDER BY在OVER()里(比如COUNT(*) OVER(ORDER BY created_at)),那就变成累计计数,不是你要的总数 - 别在
WHERE外再套一层查询去算总数——那样多一次全表扫描
需要精确跳转(如第 N 页)?用 ROW_NUMBER() + COUNT(*) OVER()
当分页控件支持“输入页码”或需校验页码是否越界时,光靠 LIMIT/OFFSET 不够直观,用行号更可控。
WITH numbered AS (
SELECT
id, name, email,
ROW_NUMBER() OVER (ORDER BY id) AS rn,
COUNT(*) OVER() AS total_count
FROM users
WHERE deleted = false
)
SELECT id, name, email, total_count
FROM numbered
WHERE rn BETWEEN 101 AND 120;
这样能明确知道第 6 页(每页 20 条)对应哪些行,且 total_count 仍可用作前端计算总页数(CEIL(total_count / 20.0))。
-
ROW_NUMBER()和COUNT(*) OVER()必须在同一层查询或 CTE 中计算,不能分开在嵌套层级里——否则优化器可能重复执行 - 如果
ORDER BY字段有重复值,ROW_NUMBER()结果不稳定,建议补上主键如ORDER BY created_at DESC, id DESC - 某些旧版 MySQL(SELECT COUNT(*),再
SELECT ... LIMIT
为什么不用 FOUND_ROWS() 或 pg_total_relation_size()?
这些是伪解:前者(MySQL)依赖 SQL_CALC_FOUND_ROWS,已弃用且不准(忽略 HAVING、不反映实际 WHERE 过滤后数量);后者(PostgreSQL)查的是磁盘大小,不是行数。
-
SELECT COUNT(*)全表扫太慢?那是索引或统计信息问题,不是换方案的理由 - 缓存总数?风险高:并发写入时极易过期,不如每次按需算——现代数据库对
COUNT(*) OVER()有优化,常复用同一扫描路径 - 真大数据量(亿级)且总数几乎不变?才考虑应用层缓存,但必须配版本号或时间戳校验,不能无条件信任
窗口函数这一步省不掉,但只要写对 OVER() 的括号内容,就几乎不会出错。最容易被忽略的是:总数必须和分页数据用完全一致的 WHERE 条件,少一个等号或 NULL 判断,前后就不匹配。

















