尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
ROW_COUNT与FOUND_ROWS辨析及MySQL分页替换
1. 两个函数到底在数什么先分清改了和有多少做后台列表的人几乎都绕不开这两个东西ROW_COUNT()和FOUND_ROWS()。名字看着像亲戚实际是两条完全不同的赛道。ROW_COUNT()回答的是上一条语句动了几行FOUND_ROWS()回答的是上一条带SQL_CALC_FOUND_ROWS的查询在不加 LIMIT 时一共能返回多少行。前者是写操作的体检报告后者是分页场景的历史补丁。搞混这两个写出来的代码就会在更新了但提示失败和翻页总数永远是 10之间反复摇摆。我见过太多这类问题有人用ROW_COUNT()去取列表总数结果每次都是 -1有人用FOUND_ROWS()去判断 UPDATE 是否命中结果拿到的是上一条 SELECT 的残留值。这些都不是玄学纯粹是没弄清两个函数的取值时机和依赖条件。这篇内容把这两者的语义边界、实测行为、驱动层映射以及分页到底该怎么做一次性讲透。刚接触数据库的能看懂怎么用写过几年业务的也能从中找到几个自己踩过但没深究的坑。ROW_COUNT()的返回值只看上一条语句这个时间点会话级别、语句级别没有任何持久性。你在它前面插一条SELECT 1前面的结果就没了。FOUND_ROWS()更严格它只认上一条SELECT SQL_CALC_FOUND_ROWS中间夹任何语句都会让结果变成上一次的遗留值或者 0。这两个函数的共同特点是它们在会话里存了一个隐式状态而绝大多数线上事故都是这个隐式状态被别的东西覆盖了。1.1 ROW_COUNT() 的语义边界它只认上一条ROW_COUNT()的规则很直白返回上一条语句影响的行数返回值类型是BIGINT。INSERT返回新插入的行数UPDATE返回实际发生改变的行数DELETE返回删除的行数。如果上一条语句是SELECT它返回 -1。对于 DDL 这类不涉及行操作的语句返回值没有实际意义实测常见 0 或 -1不要依赖。这里有个特别容易忽略的点UPDATE默认返回的是真正被改了的行数不是被匹配到的行数。如果一条记录匹配上了但所有字段的新值和旧值完全一样MySQL 认为这行没有发生变化不计入ROW_COUNT()。这就是我明明更新成功了程序却提示没找到数据的根源。想要拿到匹配行数得在连接上打开CLIENT_FOUND_ROWS标志这个标志是会话建立时确定的改不了运行时的行为。另一个高频误区是INSERT ... ON DUPLICATE KEY UPDATE。这条语句的返回值和普通 INSERT 完全不同插入新行算 1更新已有行算 2命中已有行但值没变化算 0。如果你用一个if (rowCount 0)判断是否插入成功那更新场景下返回 2 也会走进插入分支值没变返回 0 又会走进失败分支。这个语义是 MySQL 特有的写代码时必须显式拆开判断。1.2 FOUND_ROWS() 存在的理由一次往返拿到两个数FOUND_ROWS()必须和SQL_CALC_FOUND_ROWS配套使用。典型写法是两条语句连着发第一条SELECT SQL_CALC_FOUND_ROWS * FROM article WHERE status 1 ORDER BY id DESC LIMIT 20 OFFSET 0第二条SELECT FOUND_ROWS()。第一条返回当前页的 20 条数据第二条返回如果没有 LIMIT 会返回多少条。它的设计初衷很朴素——早期网络往返开销大应用层希望用一次查询拿到数据 总数省掉一次独立的COUNT(*)。这个设计在数据量小的年代确实好用。问题在于它的实现方式服务器必须把符合条件的所有行都扫出来、装配好然后再丢掉超出 LIMIT 的部分。也就是说你只要了 20 行服务器却干了 20 万行的活。而且这条路径对优化器的限制很多ORDER BY加LIMIT的提前终止优化基本用不上。数据量一大这条语句就会从几毫秒涨到几百毫秒涨幅不是线性的。MySQL 8.0.17 开始SQL_CALC_FOUND_ROWS和FOUND_ROWS()被正式标记为废弃。这个决定不是拍脑袋是官方也承认了性能问题与其维护一个看起来方便、实际很慢的语法糖不如让开发者老老实实写两条语句或者用其他方案。新项目里再看到这个写法基本可以判定是历史遗留代码。1.3 一张表看清分水岭维度ROW_COUNT()FOUND_ROWS()依赖的前置语句任意 DML 语句必须紧跟SELECT SQL_CALC_FOUND_ROWS返回含义上一条语句实际影响的行数上一条查询去掉 LIMIT 后的总行数SELECT 之后取值-1需要前置带SQL_CALC_FOUND_ROWS否则为 0 或残留值会话状态是否可被覆盖是任何后续语句都会覆盖是任何后续语句都会覆盖官方状态正常可用MySQL 8.0.17 起废弃典型误用拿它判断是否查到数据拿它判断更新是否成功这张表建议直接贴在团队文档里。我见过的最离谱的用法是用ROW_COUNT()去接分页总数然后前端显示共 -1 条。改起来只要一行但排查花了两个小时因为没人往函数用错这个方向想。2. 分页场景的真实动机与代价为什么FOUND_ROWS()曾经这么流行因为早期 Web 框架的分页组件几乎都内置了这个写法开发者照着文档抄抄完能跑就不管了。这套写法的核心卖点是少一次数据库往返在应用服务器和数据库分离部署、单次往返要几毫秒的年代这个优化是有价值的。但代价被严重低估了。真正的问题在于分页的总数查询本身就是一个全量统计操作。无论你用SQL_CALC_FOUND_ROWS还是独立COUNT(*)数据库都得把符合条件的行数点一遍。区别只在于独立COUNT(*)有机会走覆盖索引只扫索引不读数据行而SQL_CALC_FOUND_ROWS必须把整行数据组装出来因为它不确定你后面要不要这些字段。前者是轻量计数后者是完整的行组装加丢弃两者的资源消耗完全不是一个量级。更麻烦的是SQL_CALC_FOUND_ROWS会让优化器放弃一些本来可用的策略。比如带ORDER BY的分页正常情况下 MySQL 可以用优先队列只保留前 N 行一旦加上SQL_CALC_FOUND_ROWS就必须把全部匹配行排序完因为总数和顺序都需要完整结果集。数据量到几十万行这个级别延迟就会肉眼可见地变长。2.1 传统两条语句的完整流程先看这套经典写法在实际业务里长什么样。假设有个文章列表要按状态筛选、按时间倒序、每页 20 条-- 第一条拿当前页数据 总数 SELECT SQL_CALC_FOUND_ROWS id, title, created_at FROM article WHERE status 1 ORDER BY created_at DESC LIMIT 20 OFFSET 40; -- 第二条拿总数 SELECT FOUND_ROWS();这两条语句必须在同一个连接上连续执行中间不能插入任何其他语句。应用层拿到第一份结果集渲染列表拿第二份结果集渲染分页控件。流程本身没问题问题全都藏在必须连续、必须同连接、必须先执行这三个约束里。在单体应用、单线程请求、短连接的年代这三个约束很容易满足。但在连接池普及之后情况就变了连接是复用的一个请求用完归还下一个请求接着用。如果代码里在两条语句之间插了一次日志写入、一次权限校验查询甚至一次框架自带的心跳检测FOUND_ROWS()拿到的就是那条无关语句之后的状态通常表现为总数是 0 或者上一次请求的陈旧值。2.2 省掉一次往返付出的是全量扫描我做过一个粗略的对比在百万级数据表上同样返回 20 条记录独立COUNT(*)走覆盖索引大约在几十毫秒量级而SQL_CALC_FOUND_ROWS在同样的 WHERE 条件下会慢一个数量级因为后者要读取完整的行数据。这个差距随着数据量增长会继续拉大不是常数倍的差距。这笔账很好算一次网络往返在内网环境通常不到 1 毫秒而全量行组装在多大数据量下都要花几十到几百毫秒。为了省 1 毫秒多花 200 毫秒这个交换在任何场景下都不划算。更要命的是这笔开销是加在数据库服务器上的并发一高CPU 和 IO 全被这类查询吃掉慢查询日志里排前面的往往就是它们。还有个隐藏成本SQL_CALC_FOUND_ROWS返回的结果集包含了所有匹配行即使应用层只取 20 行。这些多余的数据在服务器内部要占用临时表空间遇到大结果集还可能落盘。等于说你为了一个总数把整个结果集在数据库里搬了一遍。2.3 废弃信号背后的判断逻辑官方在 8.0.17 把这两个语法标记为废弃给出的理由集中在性能不可预期和存在更好的替代方案。从工程角度看这个判断是对的分页总数本来就不该和分页数据绑在一起算。两者的数据特征完全不同——当前页数据需要精确、需要排序、需要完整字段总数只需要一个近似或精确的计数字段一个都不用。把两者拆开之后各自的优化空间就出来了。计数可以走覆盖索引、可以缓存、可以降级成约 N 条数据查询可以走游标分页、可以做延迟关联。绑在一起的时候两者互相拖累谁也优化不了。所以看到废弃标记正确的反应不是赶紧找替代语法而是这个场景本来就该拆成两件事。3. 动手实验把行为边界一条条钉死光看文档容易记混我习惯把关键行为在本地跑一遍。下面这套实验在 MySQL 5.7 和 8.0 上都验证过行为一致的部分我会说明有差异的会单独标注。实验用的表结构很简单一张用户表几个字段够用就行。CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, status TINYINT NOT NULL DEFAULT 0, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; INSERT INTO t_user (name, status) VALUES (u1, 0), (u2, 0), (u3, 0), (u4, 1), (u5, 1);准备工作就这些。接下来的每组实验都要在同一个会话里连续执行中间不要插入无关语句否则结果会被覆盖这一点在做实验的时候体会特别明显。3.1 DML 三种场景下 ROW_COUNT() 的返回值先看最基础的 INSERT、UPDATE、DELETEINSERT INTO t_user (name, status) VALUES (u6, 0); SELECT ROW_COUNT(); -- 1 UPDATE t_user SET status 1 WHERE status 0; SELECT ROW_COUNT(); -- 5实际改变了 5 行 UPDATE t_user SET status 1 WHERE status 1; SELECT ROW_COUNT(); -- 0匹配到了行但值没变 DELETE FROM t_user WHERE status 0; SELECT ROW_COUNT(); -- 0第三组是关键。WHERE status 1明明匹配到了 6 行但因为新值和旧值完全一样MySQL 判定没有变化返回 0。这个行为在业务代码里会直接导致误判更新失败请重试——其实数据早就是目标状态了。如果你希望返回匹配行数而不是改变行数得在建立连接时打开CLIENT_FOUND_ROWS。这个标志在 C API 里是连接参数在 PHP、Java、Python 的驱动里都有对应的配置项属于连接级设置运行时改不了。打开之后上面第三条语句会返回 6 而不是 0。这个差异在很多幂等更新场景里是必须的否则你无法区分没这条记录和记录已是目标状态。3.2 CLIENT_FOUND_ROWS 打开前后的对比实验同一段 SQL在不同连接参数下结果不同这是最容易让人怀疑人生的地方。下面这张表是我实测的记录场景默认连接开启 CLIENT_FOUND_ROWSUPDATE 命中 5 行全部值改变55UPDATE 命中 5 行值未变05UPDATE 命中 5 行3 行改变 2 行未变35UPDATE 命中 0 行00INSERT 一行11SELECT 查询-1-1这张表说明了一件事ROW_COUNT()的语义是可以通过连接参数改变的它不是常量而是受上下文环境影响的结果。所以团队里如果有多个服务连同一个库一定要统一这个标志的配置否则同样的 SQL 在不同服务里跑出不同的判断结果排查时能让人崩溃。PHP 里mysqli打开这个标志是在连接函数里加参数PDO 是在构造时传选项// mysqli $link mysqli_init(); mysqli_real_connect($link, $host, $user, $pass, $db, $port, $socket, MYSQLI_CLIENT_FOUND_ROWS); // PDO $pdo new PDO($dsn, $user, $pass, [ PDO::MYSQL_ATTR_FOUND_ROWS true, ]);Java 的 JDBC 是在连接串里加useAffectedRowsfalse注意这个参数名有迷惑性设为false反而是让驱动上报匹配行数。这个参数默认值在不同驱动版本里有过变化上线前务必在目标版本上跑一遍验证。3.3 SELECT 之后为什么拿到 -1ROW_COUNT()在 SELECT 之后返回 -1这个 -1 表示语句不影响行无法给出有意义的计数。很多人拿它去做查询结果是否为空的判断逻辑上就错了——SELECT 返回 0 行时ROW_COUNT()也是 -1和返回 100 行没有任何区别。SELECT * FROM t_user WHERE id 999999; -- 返回 0 行 SELECT ROW_COUNT(); -- -1不是 0 SELECT * FROM t_user; -- 返回 6 行 SELECT ROW_COUNT(); -- 还是 -1判断查询结果是否为空只能看结果集本身是否为零行客户端驱动都提供了对应的 API。用ROW_COUNT()去做这件事属于典型的工具用错场景。顺带提一句ROW_COUNT()在REPLACE INTO上的行为也值得一提REPLACE 的语义是先删后插所以如果命中了已存在的行返回值是 2一次删除加一次插入如果没命中返回值是 1。这个 2 和ON DUPLICATE KEY UPDATE的 2 含义还不一样前者是两次操作后者是一次更新别混着理解。3.4 ON DUPLICATE KEY UPDATE 的三种返回值这条语句的返回值语义在业务里用得最多也最容易出错-- 首次插入返回 1 INSERT INTO t_user (id, name, status) VALUES (1, u1, 0) ON DUPLICATE KEY UPDATE name VALUES(name); SELECT ROW_COUNT(); -- 1 -- 命中已有行且 name 变化返回 2 INSERT INTO t_user (id, name, status) VALUES (1, u1_new, 0) ON DUPLICATE KEY UPDATE name VALUES(name); SELECT ROW_COUNT(); -- 2 -- 命中已有行但值没变返回 0 INSERT INTO t_user (id, name, status) VALUES (1, u1_new, 0) ON DUPLICATE KEY UPDATE name VALUES(name); SELECT ROW_COUNT(); -- 0正确写法是把三种情况分开处理返回 1 是新增返回 2 是更新返回 0 是命中但无需变更。如果业务上只关心记录最终是否存在那三种情况都算成功如果关心这次到底插没插新行就必须区分 1 和 2。写代码前先想清楚语义比事后打补丁省事得多。4. 各语言驱动里的 RowCount 到底映射到谁标题里提到mysql RowCount()这个写法其实来自客户端驱动的封装层不同语言、不同驱动对它的实现差异不小。有的映射到mysql_affected_rows()有的映射到结果集行数有的在特定场景下干脆不可靠。搞清楚你手上这个rowCount到底是什么比背文档重要得多。4.1 PHP 三种扩展的写法对照PHP 历史上换过三代 MySQL 扩展每一代的计数方式都不一样// 第一代 mysql_*已废弃 $rows mysql_affected_rows($link); // 第二代 mysqli $rows mysqli_affected_rows($link); // 第三代 PDO $stmt $pdo-query(UPDATE t_user SET status 1 WHERE id 1); $rows $stmt-rowCount();mysqli_affected_rows()的语义和 SQL 层的ROW_COUNT()完全一致-1 表示上一条语句失败0 表示无变化正数表示影响行数。PDOStatement::rowCount()则要小心官方文档明确提示对 SELECT 语句不保证返回正确的行数因为不同驱动实现不一样。有些驱动在缓冲结果集之后能返回准确行数有些返回 -1有些返回 0。所以 PHP 里取 SELECT 结果行数安全做法是count($rows)把结果集取到数组里再数。虽然多一步内存操作但结果是确定的。rowCount()只适合用在 DML 之后判断影响行数而且还要保证没有开启PDO::MYSQL_ATTR_FOUND_ROWS带来的语义偏移。4.2 Java 与 Python 的返回值语义JDBC 的executeUpdate()返回受影响行数语义和ROW_COUNT()一致默认上报实际改变行数。前面提到的useAffectedRows参数就是控制这个行为的设成false时上报匹配行数。这个参数的默认值在不同驱动版本之间变过如果你依赖 0 和 1 的区别做业务判断一定要在项目锁定版本上写个测试用例固定住行为。Python 这边cursor.rowcount在PyMySQL、mysqlclient、mysql-connector-python里行为不完全一致。执行 DML 之后它返回的是受影响行数执行 SELECT 之后有的驱动在结果集缓冲后给出准确行数有的在你没读完结果时返回 -1。下面这段展示了最稳妥的用法import pymysql conn pymysql.connect(host127.0.0.1, userapp, password***, databasedemo) with conn.cursor() as cur: cur.execute(UPDATE t_user SET status 1 WHERE id %s, (1,)) affected cur.rowcount # 安全DML 之后取 conn.commit() rows cur.execute(SELECT * FROM t_user WHERE status 1) total cur.fetchall() # 依赖 rowcount 的话要先 confirm 驱动行为 print(len(total)) # 最稳的写法结论是一样的rowcount在 DML 之后可以放心用在 SELECT 之后不要依赖它取结果集自己数。4.3 ORM 封装层最容易踩的误判ORM 通常会把影响行数包装成布尔值或者实体对象这一层抽象会吞掉 0 和 1 的区别。比如某个封装把update的返回值定义为 0即成功那么在值未变化的场景下就会返回 false业务层就误判为失败。更隐蔽的是批量更新。有些 ORM 把多条 UPDATE 合并成一条WHERE id IN (...)的语句返回的是总影响行数你在循环里逐条判断就会错乱。还有的 ORM 在更新前先查一遍旧值做脏检查如果值没变就直接跳过不发 SQL这种实现反而不受ROW_COUNT()语义影响但会带来额外的查询开销。我的习惯是涉及是否真的写入了这种强语义判断时绕开 ORM 的返回值直接用原生 SQL 拿ROW_COUNT()或者在更新前显式查询一次目标状态。多一次查询换来确定的语义在关键业务路径上是值得的。5. 分页方案替换的完整落地回到分页。既然SQL_CALC_FOUND_ROWS被废弃且性能确实不理想那就得给出能直接替换的方案。不同业务场景适合的方案不一样下面把四种常见做法的适用边界和写法都列出来。5.1 四种分页方案的横向对比方案总数精度数据量适应性实现复杂度适用场景SQL_CALC_FOUND_ROWS精确差低历史代码维护新项目不建议独立COUNT(*) 数据查询精确中低后台管理列表筛选条件不复杂游标分页keyset无总数优中信息流、无限滚动总数缓存 / 估算近似优中高前台展示约 N 条、大数据量列表选择的关键不是哪个快而是你的用户是否需要看到精确总数。后台管理系统要翻到第 500 页那必须给精确值信息流用户从头滚到底压根不看页码那游标分页才是对的。很多项目把所有列表都做成带页码的传统分页其实是没想清楚交互需求。5.2 两次查询方案的写法与索引配合最直接的替换就是把一条语句拆成两条-- 第一条精确总数 SELECT COUNT(*) FROM article WHERE status 1; -- 第二条当前页数据 SELECT id, title, created_at FROM article WHERE status 1 ORDER BY created_at DESC LIMIT 20 OFFSET 40;这两条语句不需要在同一连接上连续执行因为它们之间没有隐式状态依赖。这一下就把连接池和并发场景的所有坑都消掉了是最大的收益。性能上COUNT(*)想快核心是让 WHERE 条件走覆盖索引。比如status 1这个条件建一个(status, created_at)的联合索引计数时只需要扫索引不用回表读数据行。如果筛选条件有多个把区分度高、使用频率高的字段放前面让优化器有索引可选。关于COUNT(*)和COUNT(1)、COUNT(col)的取舍COUNT(*)和COUNT(1)在 InnoDB 上走的是同一条路径都是数行数COUNT(col)会跳过col IS NULL的行而且可能无法使用覆盖索引性能更差。所以统一写COUNT(*)就行不用担心星号效率低这种说法那是很老的误解。5.3 游标分页与深分页的延迟关联LIMIT 100000, 20这种深分页是真慢因为 MySQL 必须扫描前 100020 行然后丢掉前 100000 行。数据量一大页面翻到后面就卡。两种改法第一种是游标分页把第几页换成上一页最后一条记录的排序键SELECT id, title, created_at FROM article WHERE status 1 AND id 100000 ORDER BY id DESC LIMIT 20;这个写法每页的扫描量恒定不随页码增长而变慢。代价是不能跳页只能上一页下一页或者下拉加载。前台信息流用这个方案非常合适。第二种是延迟关联适用于必须保留跳页能力、又要缓解深分页的场景SELECT a.id, a.title, a.created_at FROM article a INNER JOIN ( SELECT id FROM article WHERE status 1 ORDER BY created_at DESC LIMIT 100000, 20 ) b ON a.id b.id;子查询只取主键能走覆盖索引扫描成本远低于取全字段外层再用主键回表拿 20 行完整数据。实测在百万级表上从几秒降到几十毫秒量级。代价是 SQL 变复杂了可读性下降适合在明确的慢查询点上做优化不适合全局套用。5.4 总数缓存与近似值的取舍数据量到千万级即使COUNT(*)走覆盖索引也要几百毫秒。这时候就得考虑不每次都算精确值缓存总数加短 TTL比如 30 秒列表页展示的总数允许轻微滞后只统计前 N 页的最大范围超过就显示更多用EXPLAIN的rows估算值做参考展示约 N 条维护一张计数表在写入时同步增减。这几种方案的共同点是牺牲精确性换取响应速度。是否可接受完全取决于业务——财务报表不行社交内容列表完全可以。判断标准很简单用户看到的总数出错 1%会不会导致客诉或者资金损失不会那就大胆用近似。6. 高频故障与排查速查这两个函数引发的问题症状都很像返回值莫名其妙、时对时错、并发下偶发。下面把最常见的几类整理成排查路径。6.1 FOUND_ROWS() 串号连接池下的经典事故症状分页总数偶尔变成 0或者变成上一次请求的总数刷新几次可能又正常了。根因FOUND_ROWS()读的是当前会话的状态而连接池会复用连接。同一个连接先后服务两个请求第二个请求的FOUND_ROWS()如果在别的语句之后执行读到的就不是预期的值。排查步骤打开通用日志确认两条语句之间是否夹了别的 SQL检查框架是否在同一请求内做了额外的查询权限、配置、埋点检查是否使用了持久连接或者连接池检查是否开启了多语句执行allowMultiQueries多语句下中间状态更容易乱。解决直接换成独立COUNT(*)从根上消除隐式状态依赖。短期内如果改不动至少把两条语句封装成一次多语句执行减少中间插入的机会但这不是长久之计。6.2 存储过程与触发器里的覆盖问题在存储过程里用ROW_COUNT()必须紧跟目标 DML中间不能有任何语句CREATE PROCEDURE p_update_status(IN p_id BIGINT, OUT p_rows INT) BEGIN UPDATE t_user SET status 1 WHERE id p_id; SET p_rows ROW_COUNT(); -- 必须紧挨着中间不能插别的 -- 后面即使有 SELECT、SET、IF也不会影响已经存进变量的值 END;如果在UPDATE之后先写日志表、再取ROW_COUNT()拿到的是写日志的影响行数。这个错误在存储过程里特别隐蔽因为过程体长、语句多肉眼很难看出顺序问题。触发器同理而且更麻烦触发器内部执行的任何语句都会覆盖会话的ROW_COUNT()状态。所以不要在主语句之后依赖ROW_COUNT()如果表上有触发器。这种场景下判断是否写入成功应该用别的信号比如受影响的主键值、或者显式再查一次。6.3 排查速查表症状可能原因快速验证处理方式ROW_COUNT()返回 -1上一条是 SELECT看前一条语句类型改用结果集行数判断UPDATE 返回 0 但数据对新值等于旧值手动执行看是否真变开CLIENT_FOUND_ROWS或改判断逻辑分页总数是 0FOUND_ROWS()被覆盖通用日志看语句顺序换独立COUNT(*)分页总数是旧值连接池复用 未紧邻执行看连接是否持久化同上消除隐式状态批量更新行数比预期多多语句合并执行看实际下发 SQL改为逐条或用主键回查ON DUPLICATE KEY UPDATE判断错1/2/0 三种返回未区分打印实际返回值拆分三种分支处理存储过程内计数不对中间插入了其他语句检查过程体语句顺序紧跟 DML 立即存变量还有一招很实用把ROW_COUNT()和LAST_INSERT_ID()都当成瞬时值来对待取到之后立刻存进应用层变量或者 SQL 变量绝不留到后面再用。这个习惯一旦养成这类问题基本就绝迹了。7. 我在实际项目里踩过的几个坑说几个印象比较深的。有一次做数据同步用UPDATE判断记录是否存在返回值 0 就当作不存在去执行 INSERT结果跑出了大量重复数据。原因就是那些记录本来就存在、字段值也一致UPDATE返回 0。后来改成先 SELECT 再决定插入还是更新虽然多一次查询但语义清楚了重复数据再没出现过。这件事让我彻底记住了ROW_COUNT() 0只代表没有行发生改变不代表没有匹配的行。还有一次是分页总数偶尔为 0。查了半天发现是中间件在两条语句之间加了一次连接有效性检测把会话状态刷掉了。这个问题的恶劣之处在于它是概率性的本地压测复现不了只在线上高并发时偶发。最后是直接废弃SQL_CALC_FOUND_ROWS改成两条独立查询问题当天消失。最后一个建议如果你现在维护的项目里还有SQL_CALC_FOUND_ROWS不用急着全量替换但至少要评估三件事——表的数据量级、这条查询在慢日志里的排名、有没有在两条语句之间插入其他查询。三样里任意一样踩中就该排期改了。改的时候顺手把rowCount的语义判断也过一遍很多更新成功却报错的工单根源就在这一行代码上。
RELATED

相关推荐

RAP Fiori开发核心:CDS+Behavior+Projection三位一体架构

RAP Fiori开发核心:CDS+Behavior+Projection三位一体架构

简介:本资源是一份面向ABAP开发者与SAP Fiori初/中级学习者的RAP(ABAP RESTful Application Programming)开发教程合集,聚焦SAP S/4HANA环境下基于ABAP的现代化Fiori应用构建。内容系统覆盖RAP核心概念、CDS视图建模、行为定义与实…

📅 2026/11/3 21:06:16
安卓教师辅助系统开发:考勤、互动与资源管理实践

安卓教师辅助系统开发:考勤、互动与资源管理实践

1. 项目概述:基于安卓的教师上课辅助系统这个项目是我在指导计算机专业学生毕业设计时遇到的一个典型案例。教师上课辅助系统本质上是一个针对课堂教学场景的移动端工具集,它需要解决传统纸质考勤、课堂互动和教学资源管理的低效问题。从技术实现来看&am…

📅 2026/11/3 21:01:14
Web开发中间层:连接前后端的关键技术解析

Web开发中间层:连接前后端的关键技术解析

1. 重新认识Web开发的全貌当大多数人谈论Web开发时,脑海中浮现的往往是两个泾渭分明的角色:前端工程师负责浏览器端的界面和交互,后端工程师处理服务器逻辑和数据库。这种二分法已经根深蒂固,以至于我们常常忽略了连接这两端的广阔…

📅 2026/11/3 21:01:14
MORE NEWS

更多资讯

📰

LC选频网络与谐振回路:Q值、通频带及阻抗匹配解析

简介:这份PPT面向电子工程、通信类课程学习者及需要复习高频电路基础的读者,围绕选频网络中的串联谐振回路展开,适合课堂讲授、考前梳理与知识点查漏。内容从单振荡回路的谐振特性切入,依次讲解谐振频率、阻抗与电流相位关系、品质…

📰

工控机无网不用愁:三步搞定网络共享,附共享与网桥对比

车间里的工控机大多是"孤岛"状态——设备本身跑着采集程序、视觉算法或者本地数据库,但一到要拉取更新包、同步日志、远程看个界面,就卡在"没有网络"这一步。很多人第一反应是给每台机器插一张无线网卡,或者干脆拉一根网…

📰

AI工程体系构建:Python/Rust/TS/Julia多语言协同实践

1. 为什么“从零构建AI工程体系”不是写个Python脚本那么简单 很多人看到“AI Engineering from Scratch”这个标题,第一反应是:不就是用PyTorch搭个模型、调调超参、跑个train.py吗?我上周刚用Hugging Face的Trainer类三行代码训完一个文本…

📰

LeetCode 27题:双指针法原地移除数组元素

1. 题目解析与需求理解LeetCode第27题"移除元素"是一个经典的数组操作问题,题目要求我们原地移除数组中所有等于给定值的元素,并返回新数组的长度。这里的"原地"意味着不能使用额外的数组空间,必须通过修改输入数组来实现…

📰

STM32入门必看:Proteus 8.6安装汉化与仿真全攻略

嵌入式入门这条路上,STM32几乎是绕不开的一站,而仿真工具Proteus则是很多人在没有实物板子时最先接触的验证手段。我见过太多新手卡在"软件装好了但界面全是英文""汉化包放进去没反应""仿真跑不起来提示缺少模型"这些环节…

📰

Node.js私厨服务小程序开发实战与架构设计

1. 项目背景与需求分析私厨服务系统小程序是近年来O2O餐饮领域的一个创新方向,它连接了有烹饪技能的个人厨师和追求个性化餐饮服务的消费者。这个毕业设计项目选择基于Node.js技术栈开发,主要考虑了以下几个市场需求:餐饮个性化趋势&#xff…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬