热线电话:13121318867

登录
首页大数据时代MySQL 同时获取分页数据与总行数方案
MySQL 同时获取分页数据与总行数方案
2026-11-09
收藏

一、业务背景

在后台列表、管理系统、报表页面的分页需求里,前端通常需要两份数据:当前页的分页记录、符合筛选条件的数据总条数,用来渲染分页组件(总页数、页码、跳页)。

常规写法分开执行两条 SQL:一条 limit 拿分页数据,一条 count(*) 统计总数。两次查询不仅多一次数据库 IO,还会存在数据快照不一致的问题——查询间隙新增或删除数据,就会出现总数和分页对不上。因此需要掌握 MySQL 中同时拿到分页结果与总行数的实现方式。

二、主流方案对比

方案 1:SQL_CALC_FOUND_ROWS + FOUND_ROWS()(旧方案,不推荐)

-- 查询分页,开启计算满足条件总行数
SELECT SQL_CALC_FOUND_ROWS id, name, create_time
FROM `order`
WHERE status = 1
LIMIT 0, 10;

-- 紧接着获取刚才统计的总行数
SELECT FOUND_ROWS();

原理:SQL_CALC_FOUND_ROWS 会让 MySQL 忽略 limit 限制,统计满足 where 条件的全部记录数量,FOUND_ROWS() 读取上一条语句计算出的总数。

缺点

  1. MySQL 8.0.17 版本之后官方已标记废弃,不再推荐,未来版本会移除;
  2. 优化器无法优化统计逻辑,大数据量表性能差;
  3. 必须两条 SQL 顺序执行,中间不能插入其他 SQL,连接断开、事务切换都会失效;
  4. 索引利用弱,数据量大时开销高于单独 count。

适用场景:仅老旧 5.x 版本临时兼容,新项目不要使用。

”

方案 2:子查询 count(每行附带总数字段,简单易上手)

SELECT
    t.*,
    (SELECT COUNT(*) FROM `order` WHERE status = 1) AS total_rows
FROM `order` t
WHERE status = 1
ORDER BY create_time DESC
LIMIT 0, 10;

原理:标量子查询,每一行结果都附带同一个总行数字段 total_rows。

✅ 优点:只一条 SQL,写法简单,兼容所有 MySQL 版本,结果一致性强;

❌ 缺点:子查询 count 存在重复执行风险;返回的每一行都携带 total,存在少量带宽冗余;适合小数据量、筛选条件简单的场景。

方案 3:JOIN + 聚合统计(推荐,性能均衡)

SELECT t.*, c.total_rows
FROM `order` t
CROSS JOIN (SELECT COUNT(*) AS total_rows FROM `order` WHERE status = 1) c
WHERE t.status = 1
ORDER BY create_time DESC
LIMIT 0, 10;

原理:先单独用子查询算出总数,生成只有一行的统计表,交叉连接到分页结果,所有分页行共用同一个 total 值。

✅ 优点:count 只会执行一次,稳定,兼容性好,8.0、5.7 都支持;业务代码只接收一次结果集,解析取出第一条的 total 即可;

❌ 缺点:count 本身在无索引大表上查询较慢,属于 count 固有瓶颈。

方案 4:窗口函数 ROW_NUMBER() + COUNT() OVER()(MySQL 8.0 专属)

SELECT *
FROM (
    SELECT
        id, name, create_time,
        COUNT(*) OVER() AS total_rows,
        ROW_NUMBER() OVER (ORDER BY create_time DESC) AS rn
    FROM `order`
    WHERE status = 1
) AS temp
WHERE rn BETWEEN 1 AND 10;

原理:COUNT(*) OVER() 窗口函数,在不分组的情况下统计整个结果集行数,附加到每一行;ROW_NUMBER() 实现分页行号。

✅ 优点:语义清晰,适合复杂排序、多维度分页场景;可以和其他窗口指标一起计算;

❌ 缺点:MySQL 5.7 及以下不支持窗口函数;窗口计算会先扫描全部符合条件的数据再过滤行号,大表性能风险高,不适合千万级大表分页。

三、最优实践方案推荐

  1. MySQL 5.7 项目:优先选择 CROSS JOIN + count 子查询,稳定可控;
  2. MySQL 8.0 中小型数据:窗口函数写法可读性更强;
  3. 千万级大表、高并发分页:不强行一条 SQL,使用两条独立 SQL——一条 limit 拿数据,一条 count 统计总数,给 count 查询加上合适索引。

补充:大表 count 优化的重点——给 where 条件字段建立联合索引,避免全表扫描;如果允许近似总数,可以用 information_schema 快速估算行数,牺牲精度换速度。

”

四、常见踩坑说明

  1. 排序与条件不一致:统计总数的 where 条件、排序规则,必须和分页 SQL 完全一致,否则总数和分页数据不匹配;
  2. count(*) / count(主键) / count(字段) 的区别:统计总行优先 count(*),MySQL 优化器对 count(*) 优化最好;count(字段) 会自动忽略 NULL 值,统计口径会变小;
  3. LIMIT offset 过大:offset 很大(比如 offset 100000)时,无论哪种写法都会变慢,建议使用主键书签分页(id > last_id)替代 offset 分页;
  4. 新项目不要依赖 SQL_CALC_FOUND_ROWS,官方废弃特性存在迁移风险;
  5. 多表联查分页时,子查询统计总数的逻辑要同步关联条件,避免统计范围扩大。

五、业务代码示例(Python)

以 CROSS JOIN 方案为例,取出分页列表与总条数:

import pymysql

sql = """
SELECT t.*, c.total_rows
FROM `order` t
CROSS JOIN (SELECT COUNT(*) AS total_rows FROM `order` WHERE status = 1) c
WHERE t.status = 1
ORDER BY create_time DESC
LIMIT %s, %s
"""


# page=1, size=10
offset = (1 - 1) * 10
size = 10

cursor.execute(sql, [offset, size])
res = cursor.fetchall()

if not res:
    data_list = []
    total = 0
else:
    total = res[0]["total_rows"]
    # 删除多余的 total 字段,只保留业务字段返回前端
    for row in res:
        del row["total_rows"]
    data_list = res

result = {
    "list": data_list,
    "total": total,
    "page": 1,
    "size": 10
}

六、总结

同时拿到分页数据和总行数,不存在万能写法,需要根据 MySQL 版本、数据量级选型:

  • 小数据、追求单 SQL 简化编码:交叉 JOIN 子查询;
  • MySQL 8.0、复杂分析场景:窗口函数;
  • 超大表高并发:拆成两条 SQL,依靠索引优化 count;
  • 新项目禁止使用 SQL_CALC_FOUND_ROWS。

分页的性能瓶颈往往不在"一条还是两条 SQL",而在于where 筛选条件缺少索引、超大 offset 偏移。优化时应优先从索引和分页方式入手。

(注:部分内容由豆包 AI 生成)

”
数据分析师资讯
更多

OK
客服在线
立即咨询
客服在线
立即咨询