☰
MySQL ROW_COUNT 与 FOUND_ROWS 分页计数避坑指南
2026/11/3 12:59:20 网站建设 项目流程

先说一个在代码评审里反复见到的小插曲:有人贴了一段分页代码问“为什么我这边FOUND_ROWS()一直返回 0”。代码本身没问题,问题出在他把函数名写成了found_row()——少了个s。MySQL 里压根不存在这个函数,执行时直接报错,他大概是从旧笔记或者别的数据库迁移过来时顺手写错了。这个细节看着不起眼,但“found_row()”这个拼写在搜索里出现的频次一点都不低,说明被绕进去的人不在少数。

顺着这个口子往下,就是 MySQL 里两个经常被混为一谈的计数函数:ROW_COUNT()和FOUND_ROWS()。前者负责告诉你“上一条 DML 真正动了多少行”,后者配合SQL_CALC_FOUND_ROWS告诉你“这条分页查询在不加 LIMIT 的情况下会命中多少行”。两个函数都跟“行数”有关,但数的东西完全不是一回事,取值时机、作用域、受客户端标志影响的程度也全都不一样。分页、批量更新、数据同步、存储过程计数,只要涉及“执行完一条语句后确认影响面”,基本都绕不开它们。

这篇内容适合三类人看:正在写分页逻辑、想搞清楚SQL_CALC_FOUND_ROWS到底还该不该用的后端同学;做数据同步、批量刷数、需要精确拿到影响行数的数据侧同学;以及被连接池、ORM、读写分离搞出过“计数串味”问题、想弄明白根因的排查者。下面从函数命名、返回值语义、典型组合、真实场景对照、性能代价到替代方案,一条线捋下来。

1. 先把名字掰正:不存在 found_row(),只有 FOUND_ROWS()

1.1 一个拼写偏差带来的连锁误解

found_row()这个写法在 MySQL 里是无效的,正确的函数名是FOUND_ROWS(),带s。这个偏差之所以顽固,是因为它经常和另一个真函数ROW_COUNT()混在一起被讨论,两个名字一左一右摆在标题里,很容易让人以为它们是同一族的“单数/复数”配对。实际上它们不是配对关系,来源和用途都不一样。

ROW_COUNT()是个通用函数,INSERT、UPDATE、DELETE、REPLACE、ALTER TABLE等语句执行完之后都能读;FOUND_ROWS()则几乎只服务于一个特定场景——配合SELECT SQL_CALC_FOUND_ROWS拿总条数。前者关心“改动”,后者关心“匹配”。

还有一层容易踩的坑:如果你在 SQL 里写了found_row(),MySQL 报的是FUNCTION db.found_row does not exist,而不是语法错误。这个报错信息会让人误以为是库名或者权限问题,实际只是拼错了。我见过有人在存储过程里排查了半小时权限,最后发现是少了个字母。

提示:函数名不区分大小写,found_rows()、FOUND_ROWS()、Found_Rows()都能执行,但拼写必须完整。

1.2 两个函数各自站在哪一侧

把这两个函数放到一条完整的数据链路里看,位置其实很清楚。写入链路关心的是“我改了没有、改了几行”,这是ROW_COUNT()的地盘;读取链路关心的是“这个条件下总共有多少条、我这一页翻到哪儿了”,这是FOUND_ROWS()的原始设计目标。

对比维度ROW_COUNT()FOUND_ROWS()
主要用途获取上一条语句影响的行数获取上一条SQL_CALC_FOUND_ROWS查询去掉 LIMIT 后的行数
是否需要特殊语法不需要,直接读需要前置语句带SQL_CALC_FOUND_ROWS
典型搭配语句INSERT / UPDATE / DELETE / REPLACESELECT
受客户端标志影响是(CLIENT_FOUND_ROWS)否
当前状态正常可用自 8.0.17 起被标记为废弃

这个表格值得先记住,后面所有讨论都是在这两行基础上展开的。很多人出问题,本质是把这两列的语义记混了——比如拿ROW_COUNT()去数SELECT的行数,或者以为FOUND_ROWS()能反映 UPDATE 的结果。

2. ROW_COUNT() 数的是“被改动的行”,不是“被读到的行”

2.1 INSERT、UPDATE、DELETE 下的返回值拆解

先看最直白的部分。执行一条普通的INSERT,ROW_COUNT()返回插入的行数;INSERT ... SELECT插了 100 行就返回 100;批量INSERT里有一行因为唯一键冲突失败(没有用IGNORE),整条语句报错回滚,此时ROW_COUNT()的值就不该被信任了。

UPDATE的行为稍微复杂一点。默认情况下,ROW_COUNT()返回的是真正发生改变的行数。假设一张表里有 5 行满足WHERE status = 0,但你把status更新成status = 0(值没变),那么返回的是 0,不是 5。这个设计在业务上经常引发误解,尤其是在做“更新成功条数”上报的时候——用户点了保存但没改任何字段,接口返回“更新 0 条”,前端一脸问号。

DELETE相对干净,删了几行就返回几行。REPLACE INTO要注意语义:它内部是“删除 + 插入”的组合,如果发生了替换,ROW_COUNT()返回 2;如果是纯插入,返回 1;如果是删除后又插入但被其他约束挡掉,情况会更绕。所以用REPLACE的时候,别拿ROW_COUNT()的值当业务判断依据,容易误判。

2.2 SELECT 为什么常常返回 -1

这是被问得最多的一个问题。在 MySQL 的规范行为里,对于返回结果集的SELECT语句,ROW_COUNT()返回-1。原因是服务端把结果集流式推给客户端之后,并不会为这条语句维护一个精准的“已发送行数”计数器,用一个明确的-1表示“这条语句的行数统计不适用”,比返回一个可能不准的数字要诚实得多。

但这里有个重要的例外:不返回结果集的SELECT是可以正常计数的。比如SELECT ... INTO OUTFILE、SELECT ... INTO @var这类,返回值就是实际处理的行数。所以判断标准不是“语句是不是 SELECT”,而是“这条语句会不会给客户端返回结果集”。

我在项目里见过有人写:

SELECT COUNT(*) FROM orders WHERE user_id = 123; SELECT ROW_COUNT();

期待第二个语句返回订单数,结果拿到-1。正确做法是直接把COUNT(*)的结果取出来用,ROW_COUNT()在这个场景里没有任何意义。这一点务必在团队里讲清楚,否则新人很容易照着某篇老博客写成这样。

2.3 CLIENT_FOUND_ROWS 标志:同一条 UPDATE 能给出两个数字

真正让ROW_COUNT()变得“不靠谱”的,是连接层的CLIENT_FOUND_ROWS标志。连接建立时如果带上这个标志,UPDATE返回的就是匹配行数(matched rows)而不是改变行数(changed rows)。同一句UPDATE ... SET status = 0 WHERE status = 0,在两种配置下分别返回 5 和 0,差了整整 5。

不同客户端的默认行为不一样,这才是最容易踩坑的地方:

客户端 / 驱动默认是否发送CLIENT_FOUND_ROWS默认拿到的是
命令行客户端不发送改变行数
Java 侧常见 JDBC 驱动发送(useAffectedRows=false)匹配行数
PHP 的 PDO_MySQL不发送改变行数
PHP 的 mysqli不发送改变行数
部分 Go / Python 驱动可通过参数控制视配置而定

所以同一个业务逻辑,用 Java 写和用 PHP 写,ROW_COUNT()出来的数字可能对不上。这不是数据库的问题,是驱动层的默认值差异。要统一行为,就得显式把标志设成一致:JDBC 那边把useAffectedRows设为true,PHP PDO 那边把PDO::MYSQL_ATTR_FOUND_ROWS设为false(两边都取“改变行数”语义)。

注意:这个标志是在连接建立时协商的,运行期改不了。如果项目里混用了多种语言的微服务,最好在规范文档里明确写死“统一使用改变行数语义”,否则做数据核对时会出现莫名其妙的对不上。

3. FOUND_ROWS() 与 SQL_CALC_FOUND_ROWS:分页场景的经典组合

3.1 这个组合想解决的是“一条语句拿两样东西”

传统分页要拿两样信息:当前页的数据、满足条件的总条数。最朴素的做法是发两条 SQL,一条SELECT ... LIMIT拿数据,一条SELECT COUNT(*)拿总数。两条语句意味着两次解析、两次优化、两次执行,在网络往返和扫描开销上都有重复。

SQL_CALC_FOUND_ROWS的设计初衷就是把这俩合并:在查询上加个标志,MySQL 在返回LIMIT结果的同时,顺手把“如果没有 LIMIT 会返回多少行”记在一个会话级的位置,随后用FOUND_ROWS()读出来。

-- 第一条:查询当前页数据,同时让服务端计算总数 SELECT SQL_CALC_FOUND_ROWS id, order_no, amount, created_at FROM orders WHERE user_id = 123 AND status IN (1, 2, 3) ORDER BY created_at DESC LIMIT 20 OFFSET 0; -- 第二条:读会话里记录的总数 SELECT FOUND_ROWS();

两条语句之间的衔接非常脆弱,这是后面所有坑的根源。

3.2 一个可以直接抄的分页实现

先把完整链路写出来。假设分页参数是page(从 0 开始)和pageSize,服务端要做参数边界处理,防止pageSize过大导致内存压力:

SET @page_size = 20; SET @offset = 0; SELECT SQL_CALC_FOUND_ROWS id, order_no, amount FROM orders WHERE user_id = 123 ORDER BY created_at DESC LIMIT 20 OFFSET 0; SELECT FOUND_ROWS() AS total;

应用层的处理逻辑大致是:先校验pageSize(我一般限制在 1 到 200 之间,超过就截断),算出 offset,发第一条 SQL 拿结果集,紧接着在同一个连接、同一个会话里发第二条 SQL 拿总数,最后拼成{ list, total, page, pageSize }返回。

这里有两个必须强调的前提:同一个连接和紧接着执行。任何一条不满足,FOUND_ROWS()的值就不可信。

3.3 FOUND_ROWS() 的作用域比你想象的窄

FOUND_ROWS()的值是会话级的,不是语句级,也不是全局的。这意味着:

如果两条语句之间插入了别的东西——哪怕只是一条SET或者一条无关的SELECT——结果都可能出问题。MySQL 文档对无效场景的表述是“undefined”,也就是不保证正确,可能返回上一次的旧值,也可能返回 0,取决于版本和优化器路径。

我在项目里见过最典型的翻车现场是这样:业务代码里封了一个query()方法,内部走了连接池。第一条 SQL 拿完数据,连接被还回池里;第二条SELECT FOUND_ROWS()从池里借出来的可能是另一个连接,会话不同,值自然对不上,返回 0。这个 bug 在低并发下很难复现,一上压力测试就大面积出现。

还有一个更隐蔽的变种:ORM 框架在两条 SQL 之间自动插了一条“查询表结构”或者“开启事务”的语句。表面上代码只写了两次查询,实际上中间混了第三条,FOUND_ROWS()对应的可能已经是那条内部语句了。

提示:如果非要用这个组合,务必保证两条语句在同一个连接内、中间不插任何语句。用一个withConnection之类的封装把这段逻辑包起来,是最低成本的保障。

4. 六个真实场景下的行为对照

4.1 存储过程调用之后读到的到底是什么

CALL proc_name()执行完之后,ROW_COUNT()返回的是存储过程内部最后一条语句的影响行数,而不是整个过程的累计。如果过程里最后一条是SELECT,那就是-1。

这个行为很容易误导人。有人在过程里做了 100 条更新,最后加了条SELECT 'done'打印日志,结果调用方拿到的ROW_COUNT()是-1。要让外部拿到有意义的数字,得在过程末尾显式SELECT ROW_COUNT() INTO @out_param或者把计数累加到OUT参数里,别指望调用方从ROW_COUNT()里读。

另外,过程内部的ROW_COUNT()是易失的,任何一条语句执行完都会被覆盖。所以正确的姿势是执行完目标语句之后立刻存下来:

UPDATE inventory SET qty = qty - 1 WHERE sku = 'A001' AND qty >= 1; SET @affected = ROW_COUNT(); -- 后续随便做别的,@affected 保持不变

用用户变量接住,比反复读ROW_COUNT()安全得多。这是我在批量刷数脚本里固定用的写法。

4.2 ON DUPLICATE KEY UPDATE 的 0、1、2

这是 MySQL 里少有的“返回值带业务语义”的场景。INSERT ... ON DUPLICATE KEY UPDATE执行完,ROW_COUNT()的含义是:

返回值含义
1纯插入,没有冲突
2发生冲突并执行了更新,且值确实变了
0发生冲突但更新后值与原来相同,没有实际改动

这个语义很适合做“插入还是更新”的判断,但前提是连接的CLIENT_FOUND_ROWS标志没被打开。一旦打开,返回 2 和 0 的区分就失效了,冲突场景下统一返回匹配行数,业务判断直接崩掉。

我在一个库存同步的脚本里吃过这个亏:本地测试用的是命令行客户端,逻辑正常;上了生产换成 Java 服务,驱动默认开了标志,返回 0 和 2 分不清,导致“新增”和“更新”的统计全部错位。排查了半天才定位到是驱动参数问题。

4.3 REPLACE INTO 为什么不能用来判断影响行数

REPLACE INTO的ROW_COUNT()返回值会让人更困惑:发生替换时返回 2(删一条 + 插一条),纯插入返回 1,如果删除后插入失败,可能返回 1 或者报错。而且REPLACE会触发DELETE相关的级联操作和外键行为,语义上完全不等价于“upsert”。

所以凡是需要“更新成功了几条”的地方,别用REPLACE。用INSERT ... ON DUPLICATE KEY UPDATE或者显式的“先查后改”更可控。REPLACE留给“整行覆盖、不关心原值”的场景。

4.4 事务回滚之后,计数还在不在

会。ROW_COUNT()和FOUND_ROWS()都是会话级的状态,不参与事务。你把一条UPDATE放进事务,执行完读到ROW_COUNT() = 3,然后ROLLBACK,再读ROW_COUNT(),仍然是 3。因为它记录的是“这条语句执行时影响了多少行”,不是“最终提交了多少行”。

同理会话变量也是。这意味着在长事务里做批量操作,用ROW_COUNT()累加统计“总共改了多少行”是可行的,但统计结果反映的是“尝试改动的行数”,不是“最终落库的行数”。如果业务报表要精确,得在提交成功之后再记。

4.5 连接池、持久连接下的“值串味”

这是线上事故的高发区。典型链路:应用用连接池,连接是复用的,上一个请求留下的FOUND_ROWS()值还躺在会话里。新请求进来,如果只发了SELECT FOUND_ROWS()而没有前置的SQL_CALC_FOUND_ROWS查询,拿到的可能是上一个请求的残留值。

这类 bug 的表现是“偶发性的总数错误”,数据量小时不容易发现,因为残留值恰好也差不多。一旦并发上来,各种诡异的数字都会冒出来,而且很难复现。

排查思路是先确认两点:两条 SQL 是否共享同一个物理连接;两条 SQL 之间是否有其他语句插入。可以临时在代码里打日志,把CONNECTION_ID()一起打出来比对。如果两次查询的CONNECTION_ID()不同,问题就坐实了。

SELECT CONNECTION_ID() AS conn1; SELECT SQL_CALC_FOUND_ROWS * FROM orders WHERE user_id = 123 LIMIT 20; SELECT CONNECTION_ID() AS conn2, FOUND_ROWS() AS total;

conn1和conn2应该是同一个值。不同就说明连接被换掉了。

4.6 主从读写分离下的额外风险

如果项目做了读写分离,第一条带SQL_CALC_FOUND_ROWS的查询和第二条FOUND_ROWS()被路由到不同节点,结果必然错。因为FOUND_ROWS()的值只存在于发起查询的那个会话里,只读从上根本没有这个状态。

有些中间件会强制把这类“无表名的 SELECT”路由到主库,反而导致两条语句分别落在主从,问题更隐蔽。所以在读写分离架构下,这个组合基本可以直接放弃了,换成独立COUNT(*)更省心。

5. SQL_CALC_FOUND_ROWS 为什么被官方挂上“废弃”

5.1 它被废弃不是因为不好用,是因为太贵

从 MySQL 8.0.17 开始,SQL_CALC_FOUND_ROWS和FOUND_ROWS()被正式标记为废弃,官方给出的替代建议是:要么单独执行一次COUNT(*),要么用窗口函数COUNT(*) OVER()一次算出来。

根本原因是性能。SQL_CALC_FOUND_ROWS执行时,优化器必须遍历所有满足条件的行,才能算出“不加 LIMIT 会返回多少”。而普通的分页查询在有合适索引时,扫到 offset + limit 行就可以提前停止。这两者的代价差异,在深分页和大数据量场景下会被放大成数量级。

举个直观的例子。一张有 500 万行的订单表,user_id上有索引,某个用户有 30 万条订单。查询第 1 页:

  • 普通LIMIT 20:扫 20 行索引就返回。
  • 加了SQL_CALC_FOUND_ROWS:必须把 30 万行索引全扫一遍计数。

第 1 页的耗时差距可以是 1 毫秒和 300 毫秒的区别。如果是个“最近订单”这类高频入口,这个代价完全不可接受。

5.2 用 Handler 状态变量实测差异

想知道某条查询到底扫了多少行,最直接的办法是看Handler_read_*的增量。这套方法比EXPLAIN更贴近真实执行,因为它反映的是实际访问了多少个索引条目和行。

FLUSH STATUS; SELECT SQL_CALC_FOUND_ROWS id, order_no FROM orders WHERE user_id = 123 ORDER BY created_at DESC LIMIT 20 OFFSET 0; SHOW SESSION STATUS LIKE 'Handler_read%';

重点看Handler_read_next(顺序扫描索引的次数)和Handler_read_key。对比一下同样条件、去掉SQL_CALC_FOUND_ROWS的那条,Handler_read_next的增量会小非常多。

再对比一组:

FLUSH STATUS; SELECT COUNT(*) FROM orders WHERE user_id = 123; SHOW SESSION STATUS LIKE 'Handler_read%';

COUNT(*)在 InnoDB 上也是全索引扫描(除非命中覆盖索引),但它的优势在于:只扫索引不扫表,不需要回表取order_no、created_at这些字段,也不需要在服务端拼装结果集、走网络传输。这两点加起来,实际耗时通常比SQL_CALC_FOUND_ROWS低一个档次。

还有一个细节:SQL_CALC_FOUND_ROWS遇到ORDER BY时会强制做完整排序,而普通LIMIT在有匹配索引的情况下可以走到“索引有序,读到即停”。这在深分页场景下同样是巨大的差异。

5.3 四种替代路线的成本对照

方案额外开销总数精确性适用场景
独立COUNT(*)多一次查询往返精确大多数常规后台分页
覆盖索引上的COUNT多一次查询,但只走索引精确大表、过滤条件简单
窗口函数COUNT(*) OVER()单次查询但全量计算精确结果集不大、想省往返
游标分页(无总数)几乎没有不需要信息流、无限滚动
TABLE_ROWS估算值几乎为零估算展示“约多少条”

选哪条路,取决于你的页面到底需不需要那个精确的总数。后台管理系统的表格要显示“共 3821 条”并支持跳页,那就老老实实COUNT(*)。信息流、消息列表这类只支持“加载更多”的,根本不需要总数,直接游标分页。

6. 分页重构的四种落地写法

6.1 独立 COUNT(*):最稳但要注意别重复算

最保守的方案,两条 SQL 分开,互不依赖,可以分别路由、分别缓存:

-- 第一条 SELECT COUNT(*) AS total FROM orders WHERE user_id = 123 AND status IN (1, 2, 3); -- 第二条 SELECT id, order_no, amount, created_at FROM orders WHERE user_id = 123 AND status IN (1, 2, 3) ORDER BY created_at DESC LIMIT 20 OFFSET 0;

代价是多一次往返,好处是彻底摆脱了会话状态依赖,连接池、读写分离、ORM 插入语句都不影响结果。

有个优化点值得做:如果总数很大而用户其实翻不了那么深,可以给COUNT加个上限截断,避免全表扫描:

SELECT COUNT(*) AS total FROM ( SELECT 1 FROM orders WHERE user_id = 123 AND status IN (1, 2, 3) LIMIT 10001 ) t;

超过 1 万就显示“10000+”,绝大多数后台场景完全够用。这个思路在某些大型系统里很常见,能把深分页的计数开销压到可控范围。

6.2 覆盖索引上的 COUNT:让计数只走索引

COUNT(*)在 InnoDB 上不像一些人以为的那样“全表扫描”。如果WHERE条件涉及的列都在同一个索引里,优化器会走覆盖索引扫描,跳过回表。所以给计数查询建一个合适的联合索引,收益非常明显。

-- 针对 user_id + status 的过滤条件 ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);

建完之后,SELECT COUNT(*) FROM orders WHERE user_id = 123 AND status IN (1,2,3)可以直接在这个索引上完成,Handler_read_next的增量反映的是索引条目数,而不是回表次数。

要注意索引列的顺序:把选择性高、过滤强的列放前面,把范围查询的列放后面。如果status的取值只有个位数,(user_id, status)和(status, user_id)的效果可能差别很大,具体要看数据分布,用EXPLAIN验证一遍再定。

6.3 窗口函数一次查询搞定

MySQL 8.0 支持窗口函数,可以把总数和数据一次性带回来:

SELECT id, order_no, amount, created_at, COUNT(*) OVER() AS total FROM orders WHERE user_id = 123 AND status IN (1, 2, 3) ORDER BY created_at DESC LIMIT 20 OFFSET 0;

优点是不用管会话状态、不用管连接、不用管读写分离,一条 SQL 出结果。缺点是窗口函数需要对整个结果集计算,本质上和SQL_CALC_FOUND_ROWS一样要扫全量,性能上没有本质改善,只是少了一次往返。

所以它适合“结果集本身不大”的场景,比如带强过滤条件的查询(时间范围限定在最近 7 天、某个用户的数据),而不是全表分页。另外每行都带上total会让结果集变大一点点,数据量小时无感。

6.4 基于游标的分页:彻底不要总数

如果页面形态允许(下拉加载更多、无限滚动、时间线),最好的方案是完全不算总数,用“上一页最后一个排序键”作为游标:

-- 第一页 SELECT id, order_no, created_at FROM orders WHERE user_id = 123 ORDER BY created_at DESC, id DESC LIMIT 20; -- 后续页:传上一页最后一条的 created_at 和 id SELECT id, order_no, created_at FROM orders WHERE user_id = 123 AND (created_at < '2024-05-01 10:00:00' OR (created_at = '2024-05-01 10:00:00' AND id < 8891)) ORDER BY created_at DESC, id DESC LIMIT 20;

这个写法在深分页时优势巨大:无论翻到第几页,都只扫 20 行索引,耗时基本恒定。代价是没有总数、不能随意跳页,而且排序键必须唯一(或者像上面这样用id做 tiebreaker)。

我个人的判断标准是:后台管理系统用COUNT(*),前端信息流用游标分页,两边不冲突。硬要用一套逻辑覆盖所有场景,反而两头不讨好。

7. 几个踩过之后才长记性的细节

ROW_COUNT()在 DDL 语句上返回的值不稳定。ALTER TABLE、CREATE INDEX这类语句执行完,ROW_COUNT()返回 0 是常态,不同版本还可能返回别的数。所以千万别拿它去统计“这条 DDL 处理了多少行”,这个数据本身没有保证。

SELECT ... INTO @var是少数几个SELECT能被ROW_COUNT()正确计数的场景。如果你的场景正好是“把查询结果写进变量然后想知道读了多少行”,这个写法可用。但只要带了INTO OUTFILE之外的结果集返回,就退化成-1。

再一个是mysql_affected_rows()和ROW_COUNT()的关系。PHP 里mysqli_affected_rows()内部读的就是ROW_COUNT(),行为完全一致,同样受CLIENT_FOUND_ROWS影响。有人在 PHP 里用mysqli_affected_rows()发现问题,去数据库里单独跑SELECT ROW_COUNT()验证,结果因为客户端不同,两边数字对不上,白折腾了半天。验证的时候一定要用同一个客户端、同一套连接参数。

还有FOUND_ROWS()和UNION的兼容性。当查询里带UNION时,SQL_CALC_FOUND_ROWS的行为在历史版本里有过变动,有些版本会对每个子查询分别计数,结果不符合直觉。如果分页查询涉及UNION,我的建议是直接把整个UNION结果包成子查询,在外面套COUNT(*),别用这个组合。

最后提一个容易被忽略的点:FOUND_ROWS()在GROUP BY场景下返回的是分组后的组数,不是原始行数。这个行为本身没错,但和部分人的预期不一致——他们以为拿到的是“参与分组的原始行数”。如果业务需要的是原始行数,得单独COUNT(*)去数,不能用这个。

提示:做数据核对脚本时,凡是依赖ROW_COUNT()的地方,第一件事就是把连接的CLIENT_FOUND_ROWS状态确认清楚,并写进脚本注释。这个信息不写在代码里,半年后接手的人一定会再踩一遍。

我在实际项目里现在基本不再用SQL_CALC_FOUND_ROWS了。后台管理系统的分页统一走独立COUNT(*)加覆盖索引,信息流类页面走游标分页,两边都能稳定扛住流量。唯一还会用到ROW_COUNT()的地方是批量刷数脚本,而且一定是用用户变量立刻接住,绝不依赖后续读到的值。这套组合用了两年多,没再出过“总数对不上”或者“影响行数为 0”的诡异问题,省下来的排查时间比当初重构的投入多得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询