资讯动态

SQL UPDATE从底层原理到生产实践:锁、索引与批量更新全解析

发布时间:2026/10/8 9:06:52 来源:尧图企业网站定制
做数据库的同行应该都接过这种电话凌晨两点生产环境某个表的数据不对了一查是有人跑了一条没有带WHERE条件的UPDATE。这不是段子是真实事故。我见过太多开发把SQL UPDATE当成Word里的查找替换来用结果替换范围从选中区域变成了全文。语法上UPDATE table SET column value WHERE condition三分钟就能学会但一条UPDATE在数据库内部到底做了什么、为什么有的UPDATE会锁表、为什么批量更新越跑越慢、为什么并发一高就死锁这些没有几年实战根本积累不起来。这篇文章就围绕UPDATE操作从底层执行链路、多种实战写法、大表更新优化、并发事务安全、窗口函数联动、以及我踩过的高频事故六个角度系统拆一遍。适合刚入行的开发、想补全知识盲区的DBA以及准备数据库面试的同学。1. UPDATE的底层执行链路先搞清楚一次更新到底做了什么1.1 一条UPDATE在数据库内部的流转过程先说一个我经常问候选人的问题UPDATE和SELECT在数据库里最大的区别是什么很多人的答案是UPDATE会改数据SELECT不会。对但更本质的区别是UPDATE除了要像SELECT一样找到目标行之外还要对目标行加锁、记录修改前的旧值、修改数据、记录修改日志最后才返回结果。也就是说一条UPDATE的代价天然比SELECT高一个量级。具体走一遍MySQL InnoDB下的流程客户端发来的UPDATE语句先经过解析器做语法解析生成语法树然后优化器决定执行计划也就是决定走哪个索引、扫描多少行接着执行器调用存储引擎接口存储引擎根据执行计划定位到第一条满足条件的记录给它加上排他锁加锁成功后先把修改前的旧值写入undo log再把新值写入当前记录同时把变更写入redo log buffer如果表上有二级索引被修改了还要同步维护对应的索引记录最后引擎层返回这条记录更新成功执行器继续处理下一条直到所有满足条件的行都处理完事务提交时redo log落盘binlog也记录这次变更。为什么要先写undo log因为事务可能回滚。万一你UPDATE到一半发现条件写错了或者程序报错需要回滚数据库要能从undo log里把旧值恢复出来。redo log则是为了崩溃恢复——数据库突然宕机时内存里已修改但没落盘的数据页要靠redo log重做。所以一条UPDATE不是简单的改一个值它是在一套完整的先保护现场、再修改现场的机制下运行的。讲个生活化类比UPDATE就像去图书馆改一本书。SELECT只需要找到那本书、看内容UPDATE则是找到书之后把书从书架上抽出来加锁防止别人同时改抄一份旧版本存档undo log在书上改写内容再把这个动作记到馆长的日志里redo log。你想想这套流程比只看一眼要重多少。1.2 影响行数返回值里那些容易被忽略的细节很多程序员的UPDATE代码是这样写的执行UPDATE语句然后判断返回的影响行数是否大于0来决定业务是否成功。这个逻辑在大多数时候没问题但有几个细节很容易埋雷。第一个细节匹配行数和变更行数不是一回事。在MySQL命令行执行UPDATE你会看到三行输出Rows matched、Rows changed、Warnings。Rows matched是WHERE条件匹配到的行数Rows changed是实际被修改的行数。如果UPDATE把某行的值改成和原来一样MySQL默认会显示matched但不changed具体行为和版本、参数有关。而JDBC或者MyBatis拿到的返回值通常对应的是changed行数而不是matched行数。这就可能导致一个场景明明数据是对的只是值没变程序却因为返回值是0而报错更新失败。第二个细节不同数据库的返回机制不一样。SQL Server里用ROWCOUNTOracle里用SQL%ROWCOUNT在Oracle中如果SET赋的值和原值相同也会算作更新成功且影响行数1这和MySQL的默认行为不同。如果你写过跨数据库兼容的业务代码这种差异值得专门留意。第三个细节MyBatis的update方法返回int很多人拿它判断是否更新了记录。但如果传入的参数本身和库里的值一致返回值就是0业务层如果拿0当失败处理就会产生假失败。我的习惯是需要严格判断这行数据是否存在的场景更新前先查一次如果只是想让这行数据变成目标状态就不要把返回值当成唯一判据改成判断是否抛异常或者结合查询结果一起判断。1.3 为什么不带WHERE的UPDATE那么危险不带WHERE的UPDATE等价于全表更新。在InnoDB里它会逐行加锁、逐行修改行数越多持锁时间越长期间所有对该表的写入操作都会堵在锁等待上。如果你在一个几千万行的大表上跑无WHERE的UPDATE带来的后果往往不是这一条语句跑得慢而是整个业务的写入链路被拖垮。更麻烦的是这样的语句通常不是故意的而是写WHERE的时候出了问题。比如条件写反、少了一个字段的过滤、或者用了某个本身没索引的字段导致实际上扫了全表。我自己见过最典型的案例某同学要更新今天创建且状态为0的订单写出来的WHERE是 status 0 OR create_date 2024-01-01由于OR的存在优化器干脆放弃索引走了全表扫描几百万行订单被锁住线上支付回调直接超时。所以我对团队的要求是三条铁律第一生产环境的UPDATE必须带WHERE没有WHERE的UPDATE要经过DBA审批和双人复核第二UPDATE之前先跑一条等价的SELECT COUNT(*)估算影响行数第三必要的时候开启MySQL的sql_safe_updates参数让不带WHERE或者不带LIMIT的UPDATE直接报错。这个参数在测试环境极其好用它能强制拦截掉大量手滑操作。2. 多表更新与批量更新JOIN、CASE WHEN和MERGE的实战选择2.1 关联表更新的三种写法及各数据库的差异实际开发里经常要做这种操作根据A表的信息去更新B表的字段。不同数据库的写法差异很大我直接给对照表数据库推荐写法示例MySQLUPDATE ... JOINUPDATE orders o JOIN users u ON o.user_id u.id SET o.user_name u.name WHERE u.status 1SQL ServerUPDATE ... FROMUPDATE o SET o.user_name u.name FROM orders o INNER JOIN users u ON o.user_id u.id WHERE u.status 1PostgreSQLUPDATE ... FROMUPDATE orders o SET user_name u.name FROM users u WHERE o.user_id u.id AND u.status 1OracleMERGE INTOMERGE INTO orders o USING users u ON (o.user_id u.id) WHEN MATCHED THEN UPDATE SET o.user_name u.nameMySQL的UPDATE JOIN写法是平时用得最多的它本质上就是先把两张表做连接筛选出目标行再对orders表执行更新。有一点要注意如果JOIN之后一张orders订单匹配到了多条users记录比如users表存在重复数据MySQL会用其中某一条来更新具体是哪一条不受控制结果可能变成随机更新。所以在多表更新前必须确认关联字段在驱动表里是唯一的或者先对关联表做去重。我习惯的做法是先把关联表去重成临时表再执行UPDATE JOIN宁可多写一层子查询也不要赌数据没有重复。PostgreSQL的UPDATE FROM写法要特别小心如果FROM子句里有多条记录匹配同一行目标记录PostgreSQL不会报错而是随机选择一条来更新。这个行为比MySQL更隐蔽。我用PG更新数据量大的表时一定会先跑一遍SQL验证关联字段的唯一性。2.2 CASE WHEN用一条语句更新同一张表的多行不同值需求把订单表里ID为1、2、3的订单状态分别改成10、20、30。新手最常见的写法是执行三条UPDATE这当然没错但如果你有几百上千个不同值要更新一条一条UPDATE的代价就很明显了——网络往返多、binlog日志量大、锁获取次数多。这时候CASE WHEN就派上用场了。UPDATE orders SET status CASE id WHEN 1 THEN 10 WHEN 2 THEN 20 WHEN 3 THEN 30 ELSE status END WHERE id IN (1, 2, 3);这个写法的关键是ELSE status它在分支没覆盖到的时候保持原值防止误更新。WHERE条件必须和CASE覆盖的键集合完全一致如果WHERE只写了IN (1,2)CASE里却有WHEN 3那么ID为3的行会因为WHERE没匹配到而不被更新这算安全反过来如果WHERE写了IN (1,2,3,4)CASE里没有4的分支ID为4的行就会走ELSE logic。ELSE status保证了即使多匹配了也不会改出问题。实际性能对比我曾经在一个千万级表上对比过500条单行UPDATE和一条500分支的CASE WHEN UPDATE。单行UPDATE在索引命中的情况下每条1-2毫秒总耗时1秒左右但500次锁获取和日志写入会产生大量小的redo/binlog记录CASE WHEN版本只需要一次索引范围扫描、一次加锁集合总耗时更低尤其在MySQL主从复制架构下单条大UPDATE产生的一条大binlog比500条小binlog在从库上的回放效率高很多。当然CASE WHEN也有代价SQL语句本身会变得很长如果超过数据库的max_allowed_packet限制就要注意拆批另外一旦CASE分支里的值写错改错的也是一大批数据所以执行前务必把分支列表导出来人工核对一遍。2.3 INSERT ... ON DUPLICATE KEY UPDATE同步数据的实用细节做数据同步时不存在则插入存在则更新是特别常见的需求。MySQL里最直接的写法就是ON DUPLICATE KEY UPDATEINSERT INTO user_stat (user_id, order_cnt, update_time) VALUES (1001, 5, NOW()) ON DUPLICATE KEY UPDATE order_cnt VALUES(order_cnt), update_time NOW();这个语句依赖主键或唯一键来判断是否重复。但有两个坑我必须提醒。第一在MySQL 8.0.20之后VALUES()函数被官方标记为废弃推荐改成别名写法INSERT INTO user_stat (user_id, order_cnt, update_time) VALUES (1001, 5, NOW()) AS new ON DUPLICATE KEY UPDATE order_cnt new.order_cnt, update_time new.update_time;第二这个写法在并发插入同一行时死锁概率明显高于纯INSERT。原因是两个事务同时对不存在的记录加插入意向锁又同时尝试插入其中一个需要等待另一个回滚或提交很容易形成锁等待环路。如果业务对延迟敏感建议在代码里做前置查询来判断走插入还是更新或者用分布式锁控制相同key的并发。还有一点容易被忽略即使走了UPDATE分支MySQL的自增主键值也可能被消耗掉。因为插入尝试本身就分配了自增ID如果重复键走上更新分支这个ID不会被回滚。对业务来说最直观的影响是自增ID出现跳号比如从100跳到102。如果你有ID必须连续的需求这个方案就不合适如果没有只是ID跳号那就无所谓。3. 大表UPDATE的慢SQL排查为什么越更越慢、怎么分批才安全3.1 大表更新慢的三个核心原因很多人在大表上执行UPDATE跑了几分钟还没结束第一反应是数据库变慢了。其实大多数时候不是数据库慢而是你的UPDATE方式有问题。我把大表UPDATE变慢的原因归纳成三类。第一类扫描行数过多。WHERE条件没用上索引导致存储引擎只能全表扫描然后逐行判断、逐行更新。这在执行计划里会体现为typeALL、rows几百万。最典型的是在状态字段上做条件更新而状态字段本身区分度极低优化器评估下来觉得用索引还不如全表扫干脆不走索引。第二类锁范围过大导致锁等待。InnoDB在可重复读隔离级别下范围UPDATE不仅会给匹配到的行加锁还会给扫描范围内的间隙加锁间隙锁防止其他事务插入新记录。行数越多间隙锁范围越大其他事务的插入和更新全部被阻塞。业务端的表现就是一条UPDATE把订单表写堵了。第三类日志写入量过大。每修改一行既要在undo log记旧值又要在redo log记新值事务提交后还要写binlog。一千万行的大更新产生的日志量可能是几个GB甚至更大写入磁盘的IO开销就成了瓶颈。这还没算上如果修改了二级索引列每个索引都要同步维护写入放大更明显。3.2 分批UPDATE的完整实践思路处理大表更新业界最通用的方案就是分批提交。每批只处理一小部分行提交一个事务释放锁然后再处理下一批。这样每批锁定的行数有限不会长时间霸占资源其他业务也能趁间隙继续写入。我最常用的分批手法是按主键范围切-- 第一批更新前5000行 UPDATE big_table SET status 1 WHERE id 0 ORDER BY id LIMIT 5000; -- 每跑完一批提交一次记录当前最大id继续下一批 UPDATE big_table SET status 1 WHERE id 上一次的最大id ORDER BY id LIMIT 5000;为什么强调按主键顺序分批因为主键通常对应聚簇索引的物理顺序按主键范围扫描是顺序IO比随机IO快得多同时每次LIMIT都能精准确认本次处理的行数方便控制进度。不要用随机抽取5000行的方式那种方式每批都要重新扫描大量无关行效率和稳定性都差。执行分批更新时我习惯写一个存储过程或者脚本循环执行每批之间sleep几百毫秒给其他业务留出喘息空间。批大小要根据表的行宽和机器IO能力调整我一般的起点是2000到5000行如果单批耗时超过几秒就调小如果IO压力不大可以适当调大。还有两个细节要提醒。第一如果分批条件里用了非索引字段比如status那么即使LIMIT了5000MySQL也可能先扫全表找到满足条件的记录再截断这时候分批只能控制提交粒度控制不了扫描量。解决思路是先把满足条件的主键查出来放进临时表再按临时表的主键去关联更新。第二如果要更新的数据量实在太大比如上亿行先和业务方确认能不能接受在维护窗口执行并且提前把binlog、undo表空间、磁盘空间检查一遍避免更新到一半磁盘写满。3.3 EXPLAIN解读与UPDATE索引优化策略MySQL里EXPLAIN默认不支持直接解析UPDATE语句我通常这样做先把UPDATE改写成等价的SELECT用EXPLAIN看执行计划确认走了哪个索引、预估扫描多少行。比如-- 原UPDATE UPDATE orders SET status 1 WHERE create_time 2024-01-01 AND channel app; -- 改写后EXPLAIN EXPLAIN SELECT * FROM orders WHERE create_time 2024-01-01 AND channel app;看关键字段type是否从ALL变成了range或refkey是否用了预期索引rows的预估值是否在可接受范围内。如果rows有几十万甚至上百万就要警惕这条UPDATE会带来的锁范围和日志量赶紧改成上面说的分批方案。索引策略上有一点经常被忽略UPDATE修改的列如果是二级索引的一部分那么修改该列时MySQL不仅要更新聚簇索引里的记录还要删除旧的二级索引记录、插入新的二级索引记录。索引越多写入放大约明显。所以那种给表建了五六个索引然后天天UPDATE索引列的表更新慢是必然的。在索引设计阶段就要考虑频繁更新的列尽量少建索引索引列要尽量选择更新不频繁的字段。4. 并发事务下的UPDATE安全网锁等待、更新丢失与死锁4.1 行锁、间隙锁和next-key lock对UPDATE的影响InnoDB默认的行锁机制让UPDATE一条记录时不会锁住整张表这大大提升了并发度。但行锁有时也会升级成范围锁关键看WHERE条件能不能用上唯一索引或主键。我举一个实战场景订单表在可重复读隔离级别下执行UPDATE orders SET status 1 WHERE channel app AND create_time BETWEEN 2024-01-01 AND 2024-01-31;如果channel和create_time的联合索引不够优化或者create_time即使有索引但范围太大InnoDB在扫描过程中会对扫描到的索引记录加锁同时对记录之间的间隙加间隙锁防止其他事务插入新记录导致幻读。这就是next-key lock的典型形态锁定的不只是你正在更新的这五行、十行还包括这些行前后的空档。表面上是行锁实际效果接近范围锁。这就是为什么有些UPDATE在测试环境一条语句秒回到了生产环境一执行整个表的插入就全部等待——不是数据库崩溃而是间隙锁覆盖了大部分插入范围。怎么破第一把隔离级别从REPEATABLE READ降到READ COMMITTED间隙锁会大幅减少这在很多互联网公司已经是标配。第二UPDATE的条件尽量用主键或唯一索引精确定位让锁落到具体的行而不是范围。第三实在需要范围更新就用上面的分批方案让单批语句的扫描范围足够小。4.2 乐观锁版本号改一行不被覆盖的最简单手段并发更新时最经典的问题就是丢失更新两个事务同时读到同一行数据各自修改后提交的覆盖先提交的。比如库存表剩余100件A事务扣了10件变成90B事务也扣了10件但B读到的是旧值100最终写回90等于只扣了一次。这种事故在财务、库存系统里非常致命。最简单的方案是版本号机制也叫乐观锁。表里加一个version字段每次更新时带上当前的版本号UPDATE inventory SET stock stock - 10, version version 1 WHERE product_id 123 AND version 2;执行后检查影响行数如果返回1说明版本号没变更新成功如果返回0说明有其他人抢先更新了这行version已经不是2了程序就需要重新读取最新数据、重新计算或者直接提示用户操作冲突请重试。这个方案看起来简单但它隐含一个前提UPDATE本身是原子的也就是版本比对修改版本号自增这三个动作在数据库层面是一条语句完成的不会出现中间态。所以乐观锁的实现一定要把version条件写进WHERE而不是先查出来再在SET里赋值。我在实际项目里发现很多团队用乐观锁失败的原因是他们把version条件写在SET里比如SET version version 1 WHERE id 123但没有在WHERE里带原始version这样两个并发请求都会成功执行版本号变成了4和5数据也被覆盖了。正确写法只有一个WHERE必须带version 旧值。乐观锁适合并发冲突概率不高的场景比如用户修改自己的资料如果冲突概率很高比如热点商品抢购乐观锁会导致大量请求在重试上消耗这时候反而要用SELECT ... FOR UPDATE的悲观锁直接把行锁住让后来的请求排队。4.3 一次教科书式的死锁案例拆解死锁是并发UPDATE绕不开的话题。我拿一个真实复现过的场景来讲两台应用服务器同时处理两笔订单正好这两笔订单的用户要做积分结算事务A先UPDATE orders SET status 1 WHERE order_id 1001再UPDATE users SET points points 50 WHERE user_id 666 事务B先UPDATE users SET points points 50 WHERE user_id 666再UPDATE orders SET status 1 WHERE order_id 1001。如果A拿到了order_id1001的行锁B拿到了user_id666的行锁然后A请求user_id666的行锁发现被B持有只能等B请求order_id1001的行锁发现被A持有也在等。两个事务互相等待谁也释放不了数据库死锁检测器介入后回滚其中一个事务。排查死锁的步骤我一般这样做发生死锁第一时间执行SHOW ENGINE INNODB STATUS在输出的LATEST DETECTED DEADLOCK部分能看到两个事务各自持有什么锁、等待什么锁、执行到哪条SQL。结合应用日志的时间戳几乎都能定位到具体的代码位置。怎么避免最实用的一条让所有事务按照相同的顺序访问资源。上面例子中如果两个事务都约定先更新users再更新orders就不会形成环路。另外把事务尽量缩短减少持锁时间把大事务拆小在程序里对高并发更新做限流或排队都能显著降低死锁概率。MySQL 8.0还支持NOWAIT和SKIP LOCKED语法在某些排队场景下可以直接跳过被锁的行但这个要谨慎使用得确认业务上跳过是可以接受的。5. 窗口函数与UPDATE联动组内排名、累计值等复杂赋值场景5.1 用ROW_NUMBER()给分组内的行写排名业务上经常遇到每个部门按薪资排名把排名写回员工表这种需求。很多人第一反应是写存储过程循环其实窗口函数配合UPDATE一条语句就能搞定。PostgreSQL和SQL Server可以直接用CTEWITH ranked AS ( SELECT emp_id, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn FROM employee ) UPDATE employee e SET rank_no r.rn FROM ranked r WHERE e.emp_id r.emp_id;MySQL 8.0也可以用类似思路写法上需要把CTE包一层再JOINWITH ranked AS ( SELECT emp_id, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn FROM employee ) UPDATE employee e JOIN ranked r ON e.emp_id r.emp_id SET e.rank_no r.rn;这个写法的价值在于窗口函数在数据库内部完成分组排序不需要你在应用层逐组处理也不会因为并发导致排名错乱。注意一点UPDATE之后如果数据发生变化排名不会自动重算需要重新执行这条语句或者对排名字段建立定时刷新任务。5.2 用窗口函数完成累计值回填另一个常见需求是累计值回填。比如有一张用户日消费流水表每天一行消费金额现在要把截至当天的累计消费金额回填到当月汇总表的某个字段里。窗口函数的写法是SUM() OVER (PARTITION BY user_id ORDER BY day)配合UPDATE就能把计算结果写回去WITH daily_sum AS ( SELECT user_id, day, SUM(amount) OVER (PARTITION BY user_id ORDER BY day) AS cum_amount FROM user_daily_spend ) UPDATE user_daily_spend uds JOIN daily_sum ds ON uds.user_id ds.user_id AND uds.day ds.day SET uds.cum_amount ds.cum_amount;这里有个关键细节窗口函数在UPDATE语句里的使用MySQL要求必须通过派生表或者CTE先计算不能直接写UPDATE ... SET col SUM(...) OVER (...)。MySQL 5.7以及更早的版本根本不支持窗口函数需要先把计算结果查出来存到临时表再UPDATE或者用用户变量模拟。如果你还在维护5.7的老项目遇到这种需求建议直接升级到8.0用户变量模拟窗口函数的写法又绕又容易错我踩过不少坑其中变量在并发下结果错乱是最难排查的。5.3 各数据库对UPDATE使用窗口函数的支持差异数据库窗口函数支持情况UPDATE联动写法注意事项MySQL 8.0完整支持UPDATE JOIN 派生表/CTESET中不能直接调用窗口函数必须包一层PostgreSQL完整支持WITH ... UPDATE FROM多版本特性稳定几乎无坑SQL Server完整支持WITH ... UPDATE支持较老版本语法兼容性好Oracle完整支持MERGE 子查询窗口函数在子查询中计算再MERGEMySQL 5.7及以下不支持只能用临时表/用户变量高并发下用户变量结果可能错乱如果你的项目刚好落在最后一行我建议在SQL层面不要硬扛直接在应用层循环计算后逐条UPDATE数据量小的时候反而清晰可靠数据量大了优先考虑迁移到8.0。6. UPDATE高频事故复盘这些写法我踩过你也别踩6.1 忘记WHERE条件的连锁反应与补救这应该是UPDATE界的头号事故。我自己也犯过一次当时是在测试库执行一条UPDATE想改一行测试数据结果忘记带WHERE整个表几百行全被改成了同一个值。好在是测试库重刷数据就行。但生产库的同类事故我在帮助客户排查时见过好几次后果轻则影响一批业务数据重则触发链路异常需要全量回滚。如果不幸真的在生产库执行了无WHERE的UPDATE能做的是第一立刻停止所有相关业务写入避免脏数据扩散第二确认该表是否有备份或者是否开启了binlog如果开启了binlog且是ROW格式可以利用binlog解析出每一行的旧值逆向生成恢复语句第三如果用了云数据库查看是否有按时间点的闪回功能。这些都是事后补救成本极高。真正的防线在事前。我现在对团队的要求是任何UPDATE开发环境也必须先执行SELECT确认WHERE命中的行数和预期一致生产环境执行UPDATE前把语句放进一个显式事务里先不COMMIT执行完后用SELECT检查关键字段确认无误再提交。我不厌其烦地强调这一点因为它真的能拦住绝大多数手滑事故。另外MySQL有个参数叫sql_safe_updates开启后不带WHERE且不带LIMIT的UPDATE和DELETE会被拒绝执行相当于一道强制保险。我强烈建议所有开发、测试环境都开启它生产环境如果担心操作效率至少高危库和核心表要开。6.2 同一张表不能既更新又子查询的经典报错解决很多人在写复杂的UPDATE时都遇到过这个报错You cant specify target table t for update in FROM clause。意思是MySQL不允许在UPDATE语句的FROM或子查询中直接引用正在被更新的目标表。举个例子你想把每行记录的score更新为当前表最高score的两倍实际上这需求有点拧巴但类似的场景经常出现-- 错误写法 UPDATE t SET score score * 2 WHERE score (SELECT MAX(score) FROM t);MySQL会直接报错。解决办法是先把子查询的结果套一层派生表让MySQL感觉不到直接引用UPDATE t JOIN (SELECT MAX(score) AS max_score FROM t) tmp SET t.score t.score * 2 WHERE t.score tmp.max_score;为什么MySQL要禁止直接引用主要是为了防止语义歧义和实现复杂度——同一张表在执行更新时如果还允许并发查询它自身结果可能不稳定。记住一个原则UPDATE的里层子查询如果引用了目标表就包一层派生表或者CREATE TEMPORARY TABLE把它固化下来。6.3 金额字段的精度陷阱与隐式转换隐患第三个高频事故是数据类型的坑。我见过一个库存系统金额字段用的是FLOAT每次UPDATE累加10.2结果库里出现了10.199999999999999这样的值。原因很简单FLOAT/DOUBLE是二进制浮点很多十进制小数无法精确表示。金额这种对精度敏感的数据在MySQL里应该用DECIMAL比如DECIMAL(10,2)PostgreSQL里对应NUMERICOracle里是NUMBER。另一个坑是隐式转换导致索引失效。最常见的案例订单号字段是VARCHAR但WHERE条件里写成了数字-- 错误示范order_no是varchar传入数字 UPDATE orders SET status 1 WHERE order_no 202401010001;MySQL会把VARCHAR字段隐式转换成数字去比较导致order_no上的索引失效执行计划变成全表扫描。更严重的是如果order_no存储的是类似100和100abc这种值数字比较时可能匹配出多条UPDATE会误伤到不相干的行。所以字符类型的条件必须加引号UPDATE orders SET status 1 WHERE order_no 202401010001;这类问题很难通过报错发现因为SQL能正常执行只是性能变差或者数据被多改了几行。我在团队里养成的习惯是UPDATE语句写完先EXPLAIN看执行计划如果type不是预期的const、eq_ref或range在提交执行前先打一个问号是不是类型写错了、索引没建对、或者隐式转换在作怪。写到这里回头再看UPDATE这条语句语法上确实简单但围绕它展开的执行原理、锁机制、索引策略、事务隔离、数据精度每一块都能单独写出一篇长文。我个人在实际操作中的体会是真正拉开数据库工程师差距的不是谁SQL语法背得熟而是谁在写UPDATE之前想清楚这条语句会扫描多少行、锁住多大范围、产生多少日志、在并发下是否安全。保持先查后改、先小后大、先备份后执行的习惯比记住任何高级写法都重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑