ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL 1093错误详解:解决UPDATE子查询同表限制的5种方案

MySQL 1093错误详解:解决UPDATE子查询同表限制的5种方案 折腾 MySQL 的人谁没跟“错误代码 1093”打过照面呢。刚入行那会儿我在一个订单表上跑 UPDATE子查询里顺手就写了从同一张表取数结果 Workbench 直接甩给我一句You can‘t specify target table ‘tb‘ for update in FROM clause翻译过来就是你不能在更新某张表tb的同时在 FROM 子句里又去查这张表tb。乍一看挺反直觉的——我先 SELECT 条件再 UPDATE 对应行逻辑上完全说得通啊凭什么不让写如果你也卡在这里过或者你只是听说过这个报错但还没踩过这篇文章就把这件事讲透报错的根本原因是什么、官方为什么这么限制、实际业务里有哪几种绕过去的标准写法以及我这些年用下来的经验和坑。内容不多但足够让你从“搜一下复制粘贴”变成“真正明白它在干嘛”。不管你是学生、后端开发还是天天跟数据库打交道的 DBA都可以按自己的基础选着看。1. 报错机制剖析为什么 MySQL 拒绝这种“自己查自己”的写法1.1 报错的触发场景与字面意思先定义一下什么是 1093。它的完整名字是“ER_UPDATE_TABLE_USED”英文原文是SQLSTATE: HY000 (ER_UPDATE_TABLE_USED) Message: You can‘t specify target table ‘%s‘ for update in FROM clause这句话的重点是target table和FROM clause。意思是MySQL 不允许一条语句在 FROM 子句中读取目标表也就是你正在 UPDATE 或 DELETE 的表。最常见的触发写法是UPDATE emp SET salary salary * 1.1 WHERE dept_id IN (SELECT dept_id FROM emp WHERE salary 5000);还有 DELETE 场景也一样DELETE FROM emp WHERE id IN (SELECT id FROM emp WHERE salary 3000);这两条语句从业务逻辑上看都很顺先把条件查出来再更新或删除。但 MySQL 直接拒绝编译。原因不在于“条件算不对”而在于 MySQL 在执行 UPDATE/DELETE 时的内部机制不允许同一张表在正在被写入的同时又被读取。1.2 为什么标准 SQL 和 MySQL 都禁止“一边写一边读同一张表”用一个生活化的类比理解一下。想象你是一个老师正在批改一份试卷更新表数据同时你又拿出同一份试卷来抄写答案查询表数据。批改的过程中试卷上的答案可能已经被你改掉了那你参考答案的时候看到的到底是原始答案还是改过的答案这就不确定了。数据库也一样当一条语句同时对同一张表进行读写执行结果会变得不可预期。从数据库实现层面看有几个真实原因第一执行顺序带来的不确定性。理论上SQL 的优化器会想办法先执行子查询再用结果去更新外层表。MySQL 确实也是尝试这样做的但在某些情况下优化器没法保证子查询是在所有更新动作之前一次性完成的。一旦更新和查询交错执行结果就取决于数据读取到的中间状态而不是语句开始时的状态。第二半一致性读semi-consistent read带来的影响。MySQL 在 UPDATE 语句中会使用半一致性读对于要修改的行它会尝试读取“当前版本”的数据以便更精确地判断条件是否满足。如果子查询也在读同一张表两者叠加数据版本可能完全对不上。第三性能陷阱。如果允许子查询直接读取目标表优化器很可能会为每一行外层更新都重新执行一次子查询产生“逐行子查询”性能直接崩掉。MySQL 宁愿编译报错也不让你写出这种隐式的性能炸弹。这一点很像编程语言里“禁止在遍历一个集合时直接修改这个集合”的规则——你非要干它要么报错要么让你用拷贝副本。1.3 MySQL 官方文档怎么说MySQL 官方文档在 “UPDATE Statement” 一节中明确写了You cannot update a table and select directly from the same table in a single subquery.意思是你不能在同一个子查询中更新一张表的同时直接去 SELECT 这张表。文档里推荐的做法就是先用一个派生表derived table把数据“物化”出来或者使用多表 UPDATE 语法。后面讲方案的时候你会看到这几种做法其实都围绕同一个核心思路让 MySQL 认为子查询里的数据来源跟目标表不是同一张表或者至少不是一个“正在被读取的原表”。顺带一提这个限制不仅仅在 UPDATE 语句里DELETE 语句同样如此。所以理解了 1093 的机制等于同时掌握了两个常见报错的解法。2. 五种绕行方案从“包一层”到“联表更新”明白了原因接下来就是实操。下面五种方案每一都是我在真实项目里用过的从最简单到最彻底你按自己的场景和 MySQL 版本选一个用就行。假设有张员工表CREATE TABLE emp ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50), dept_id INT, salary DECIMAL(10,2) );现在要实现一个需求把“工资低于本部门平均工资”的员工工资上调 10%。这条语句直接用子查询写必然报 1093-- 这样写会报错ERROR 1093 UPDATE emp SET salary salary * 1.1 WHERE salary (SELECT AVG(salary) FROM emp e2 WHERE e2.dept_id emp.dept_id);下面看怎么改。2.1 方案一双层子查询派生表最经典MySQL 5.x 通用核心思路把报错的子查询再包一层让 MySQL 先把内层结果物化成一个“虚拟表”外层 UPDATE 再去访问这个虚拟表而不是直接访问原表。改完是这样UPDATE emp SET salary salary * 1.1 WHERE salary ( SELECT avg_sal FROM ( SELECT AVG(salary) AS avg_sal, dept_id FROM emp GROUP BY dept_id ) t WHERE t.dept_id emp.dept_id );内层SELECT ... FROM emp因为被外层包裹MySQL 会创建一个派生的临时表t。更新操作的目标是 emp而子查询的数据来源是派生表不再是 emp 本身所以 1093 不再触发。但这里有个坑MySQL 5.7.6 之后优化器引入了 derived table 的合并merge机制。如果内层查询可以被优化器“展开”回原表查询那它可能还是会去动原表导致 1093 又冒出来。不过办法也有在嵌套子查询里加上LIMIT、GROUP BY、DISTINCT或聚合函数就能迫使 MySQL 把结果物化。上面用了AVG和GROUP BY刚好天然阻止了合并所以很稳定。如果是查主键 ID 集合这种简单查询我建议你在最内层多加一句LIMIT 99999999强制物化UPDATE emp SET salary salary * 1.1 WHERE id IN ( SELECT id FROM ( SELECT id FROM emp WHERE salary 5000 LIMIT 99999999 ) t );这个写法看起来有点笨但它确实是最兼容、最不需要表结构支持的旧版救命稻草。老项目上如果 MySQL 版本不高优先考虑这个。2.2 方案二JOIN 改写工程上最推荐既然 MySQL 不允许“单表子查询”那就干脆绕过子查询用 JOIN 把“子查询结果”和原表关联起来。这是我在生产环境里最常用的写法没有之一UPDATE emp e JOIN ( SELECT dept_id, AVG(salary) AS avg_sal FROM emp GROUP BY dept_id ) d ON e.dept_id d.dept_id SET e.salary e.salary * 1.1 WHERE e.salary d.avg_sal;这里的关键在于UPDATE emp e JOIN (...) d是 MySQL 特有的“多表更新”语法它的效果是连接两张表然后只更新左边emp里的匹配行。子查询d虽然在内部也读了 emp但对优化器来说它的身份是驱动表/连接表而不是“FROM 子句里直接读取的目标表”。这种写法的优势很明显一是语义清晰你会明确看到“用部门平均工资做参照更新员工工资”这个意图二是性能可控子查询先于连接物化或优化不容易出现逐行扫描。如果你只能记一种解法我建议记 JOIN 这种。2.3 方案三多表 UPDATE 语法MySQL 独有扩展严格来说方案二就是多表 UPDATE 的一种。这里单独拎出来是因为有些场景并不需要聚合只是想用一个字段列表关联更新。比如要给所有在“销售部”的员工发补贴UPDATE emp e JOIN dept d ON e.dept_id d.id AND d.name 销售部 SET e.salary e.salary 500;这就是多表 UPDATE 的精华直接在 UPDATE 后面跟多张表用 JOIN 条件圈定范围SET 里写要更新的字段。整个过程中都不需要子查询自然也不会碰 1093。顺便说一句MySQL 的这个语法是它区别于标准 SQL 的一个典型扩展标准 SQL 里其实没有“UPDATE 两张表”的写法。PostgreSQL、SQL Server 各有自己的等价语法所以如果你以后换数据库注意语法要调整。2.4 方案四临时表或普通归档表适合复杂逻辑如果子查询逻辑特别复杂比如涉及多级聚合、多层业务过滤或者你想把中间结果缓存下来复用那就别在 UPDATE 里折腾了。可以先建临时表再把临时表关联回原表-- 第一步建临时表存储部门平均工资 CREATE TEMPORARY TABLE tmp_dept_avg AS SELECT dept_id, AVG(salary) AS avg_sal FROM emp GROUP BY dept_id; -- 第二步给临时表加索引数据量大时很有用 ALTER TABLE tmp_dept_avg ADD INDEX idx_dept (dept_id); -- 第三步关联更新 UPDATE emp e JOIN tmp_dept_avg t ON e.dept_id t.dept_id SET e.salary e.salary * 1.1 WHERE e.salary t.avg_sal; -- 第四步用完即弃会话结束自动删除 DROP TEMPORARY TABLE IF EXISTS tmp_dept_avg;临时表的好处是中间结果实打实存在磁盘或内存里你可以在更新前先查看、校验甚至在一条事务里反复使用。比如你想把“部门平均工资”“全公司平均工资”“上个月绩效”同时作为指标一个复杂的 JOIN 可能变得很拗口这时候把中间结果拆成两张临时表再一起关联可读性会好很多。临时表有个特性要注意它是会话级的连接断开就没了。如果是写脚本跑批处理建议显式DROP TEMPORARY TABLE释放资源免得在长连接里积累临时表。如果是在线业务不推荐这种方式——会话销毁可能带来额外开销临时表数据量也容易失控。一般只在跑批或离线修复场景用。2.5 方案五MySQL 8.0 用窗口函数或 CTE新版首选如果你用的是 MySQL 8.0那有更现代、更优雅的解法。先说 CTE通用表表达式。MySQL 8.0.19 之后WITH子句可以直接用在 UPDATE/DELETE 语句前面。上面“部门平均工资”的需求可以写成WITH dept_avg AS ( SELECT dept_id, AVG(salary) AS avg_sal FROM emp GROUP BY dept_id ) UPDATE emp e JOIN dept_avg d ON e.dept_id d.dept_id SET e.salary e.salary * 1.1 WHERE e.salary d.avg_sal;CTE 在这里的作用类似于命名后的派生表但是可读性高得多。如果你需要多次引用同一个中间结果CTE 也能避免重复写那段子查询。注意MySQL 8.0.19 之前WITH是不能直接修饰 UPDATE 的旧版本老老实实用 JOIN 或派生表。另外用窗口函数也能达到同样效果比如UPDATE emp e JOIN ( SELECT id, AVG(salary) OVER (PARTITION BY dept_id) AS avg_sal FROM emp ) t ON e.id t.id SET e.salary e.salary * 1.1 WHERE e.salary t.avg_sal;窗口函数的好处是不用 GROUP BY 再 JOIN 回去直接把每行对应的部门平均值算出来。但在更新大表时需要注意窗口函数会把中间结果全部物化到临时表内存或磁盘压力会偏大。所以我在线上环境通常更倾向用 GROUP BY 子查询 JOIN而不是窗口函数物化全表。2.6 各方案适用场景对比方案写法适用场景注意点双层子查询UPDATE 嵌套 SELECTMySQL 5.x 老库临时改一条 SQL内层需加 LIMIT/GROUP BY 防止优化器合并JOIN 改写UPDATE ... JOIN (...) ON ...日常开发大多数生产环境字段别名别写错注意连接匹配到多行多表 UPDATEUPDATE t1 JOIN t2 ON ...需要按关联表条件更新标准 SQL 不支持移植性差一点临时表CREATE TEMP TABLE JOIN复杂逻辑、多次复用中间结果会话结束才删除注意显式清理CTE / 窗口函数WITH ... UPDATE ...MySQL 8.0逻辑清晰优先8.0.19 以下不支持 WITH UPDATE3. 实操过程与核心环节实现用案例跑通三种主流写法3.1 准备测试数据口说无凭这里我直接造一点数据来演示。先初始化表和数据你也可以复制到自己库里跑CREATE DATABASE IF NOT EXISTS demo_1093 DEFAULT CHARSET utf8mb4; USE demo_1093; CREATE TABLE emp ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50), dept_id INT, salary DECIMAL(10,2) ); INSERT INTO emp (name, dept_id, salary) VALUES (张三, 1, 8000), (李四, 1, 12000), (王五, 1, 6000), (赵六, 2, 9000), (钱七, 2, 15000), (孙八, 2, 7000), (周九, 2, 8000);现在统计一下各部门平均工资部门 1平均 (8000 12000 6000) / 3 8666.67部门 2平均 (9000 15000 7000 8000) / 4 9750.00所以低于部门平均工资的员工是张三8000 8666.67、王五6000 8666.67、赵六9000 9750.00、孙八7000 9750.00。要给他们涨 10% 工资。3.2 先用 SELECT 验证范围关键习惯执行 UPDATE 之前强烈建议先把“本应该更新哪些行”查出来。这一步能避免你把 UPDATE 写错范围线上误更数据的惨案我见过太多了。SELECT e.*, t.avg_sal FROM emp e JOIN ( SELECT dept_id, AVG(salary) AS avg_sal FROM emp GROUP BY dept_id ) t ON e.dept_id t.dept_id WHERE e.salary t.avg_sal;预期结果里应该包含张三、王五、赵六、孙八这 4 行。确认无误后再把这个 SELECT 改成 UPDATE只动 SELECT 部分把SELECT e.*, t.avg_sal换成SET e.salary e.salary * 1.1JOIN 和 WHERE 完全不动。3.3 实操一JOIN 改写更新-- 更新前的部门平均工资验证 UPDATE emp e JOIN ( SELECT dept_id, AVG(salary) AS avg_sal FROM emp GROUP BY dept_id ) t ON e.dept_id t.dept_id SET e.salary e.salary * 1.1 WHERE e.salary t.avg_sal; -- 查看更新结果 SELECT * FROM emp ORDER BY dept_id, id;执行后张三工资变为 8800王五变为 6600赵六变为 9900孙八变为 7700。这四条数据正好是我们要处理的。其他两行李四、周九因为本来就高于部门平均保持不变。注意一点JOIN 更新时如果连接条件能匹配到多行比如被更新的表在ON里出现重复匹配MySQL 对同一行可能会执行多次更新。要避免这种意外就要保证连接条件在被更新表一侧是唯一的。上面例子连接字段是e.dept_id t.dept_idt是按部门聚合后的结果每个 dept_id 唯一所以安全。3.4 实操二双层子查询更新老版本兼容写法如果是 MySQL 5.5 / 5.6 的旧环境JOIN 同样能用但有些人更喜欢保持原来子查询结构的写法。那就包一层UPDATE emp SET salary salary * 1.1 WHERE salary ( SELECT avg_sal FROM ( SELECT dept_id, AVG(salary) AS avg_sal FROM emp GROUP BY dept_id ) t WHERE t.dept_id emp.dept_id );这个写法在 5.7 上一般也没问题因为聚合查询会使派生表被物化。但如果是简单子查询比如WHERE id IN (SELECT id FROM emp WHERE ...)就必须在LIMIT或DISTINCT上留一手强制物化。我实际遇到过一次奇怪情况MySQL 8.0.20 版本上我写了一个简单的双层子查询外头没加 LIMIT还是报了 1093。排查了半天就是因为优化器把内层合并了。所以凡是走双层子查询方案我给自己定了条规矩内层必须带聚合、DISTINCT、LIMIT 三者之一否则免谈。3.5 实操三临时表方式跑批如果这是每周跑一次的批量修复脚本我会用临时表方式因为可以顺手记录中间统计方便排查-- 生成临时表 CREATE TEMPORARY TABLE tmp_dept_avg AS SELECT dept_id, AVG(salary) AS avg_sal FROM emp GROUP BY dept_id; -- 检查到底哪些员工会受影响 SELECT e.*, t.avg_sal FROM emp e JOIN tmp_dept_avg t ON e.dept_id t.dept_id WHERE e.salary t.avg_sal; -- 确认无误后执行更新 UPDATE emp e JOIN tmp_dept_avg t ON e.dept_id t.dept_id SET e.salary e.salary * 1.1 WHERE e.salary t.avg_sal; -- 清理临时表 DROP TEMPORARY TABLE IF EXISTS tmp_dept_avg;如果你用的连接池是长连接临时表不会因为“语句结束”就消失所以脚本里一定记得 DROP。如果不 DROP同一会话里第二次跑CREATE TEMPORARY TABLE会因为表已存在而报错。另外临时表在数据库客户端工具比如 Navicat 的查询窗口里是那个窗口私有的新开窗口看不到别找半天找不到。3.6 实操四MySQL 8.0 的 CTE 体验在 8.0 环境直接上 CTE 是最舒服的WITH dept_avg AS ( SELECT dept_id, AVG(salary) AS avg_sal FROM emp GROUP BY dept_id ) UPDATE emp e JOIN dept_avg t ON e.dept_id t.dept_id SET e.salary e.salary * 1.1 WHERE e.salary t.avg_sal;如果你用的是 8.0.19 之前的版本执行会报语法错误。解决方案就回到 JOIN 派生表的写法效果一样。从功能上讲CTE 和派生表在 UPDATE 场景里的作用几乎等价区别主要在可读性和多次引用上不影响结果。3.7 实操心得如何确认更新影响行数并回滚跑完 UPDATE 后MySQL 默认会返回一个“Rows matched / Changed / Warnings”的信息。这里面有个容易看懵的点假设你更新了 4 行把工资从 8000 改成 8800再执行一次同样的 UPDATE它可能仍然显示 “Rows matched: 4 Changed: 0”。因为第二次匹配到同样的 4 行但值已经一样了MySQL 认为没有实际变化所以 Changed 是 0。如果更新完发现结果不对比如 WHERE 条件写宽了更新的行数超出了预期你可以用事务快速回滚START TRANSACTION; UPDATE emp e JOIN (...) t ON ... SET e.salary e.salary * 1.1 WHERE e.salary t.avg_sal; -- 先不要提交查一下 SELECT * FROM emp ORDER BY dept_id, id; -- 数据没问题 COMMIT; -- 数据有问题 ROLLBACK;用事务包住 UPDATE是批量修改的保命手段。我每次做敏感数据修复都习惯先把 UPDATE 放在事务里看一眼更新后的结果再 COMMIT。这个习惯帮我躲过了至少两三次线上事故。4. 常见问题与排查技巧实录1093 的周边坑一次排完4.1 为什么我在 SELECT 里这么用没事UPDATE 就报错很多新手会在这一步疑惑同样的子查询放到 SELECT 语句里毫无问题为什么换成 UPDATE 就报 1093原因在于 SELECT 语句只是读不存在“目标表被修改”的语义冲突。而 UPDATE/DELETE 会对目标表加锁、修改期间再读同一张表就会产生前面说的一致性问题。所以不是 MySQL 不让你子查询而是不让你“边读边写”。举个例子下面这条 SELECT 完全合法SELECT * FROM emp e WHERE e.salary (SELECT AVG(salary) FROM emp e2 WHERE e2.dept_id e.dept_id);同样的逻辑改成 UPDATE 就违法了。这是 SQL 标准本身对 DML 的限制不是 MySQL 自己的 bug。4.2 为什么我包了一层还是报 1093这是最让人抓狂的情况。明明按网上的方法包了两层子查询报错还在。原因大概率就是前面提到的MySQL 5.7.6 优化器把外层派生表“合并”回了底层表等同于没有包。解决办法就是在内层加LIMIT或聚合函数强制派生表物化。再给你一个排查技巧用EXPLAIN看执行计划如果table列里依然出现目标表的名字说明派生表被合并了如果出现了derived2这种临时表标识说明它被物化报错自然也就消失了。EXPLAIN SELECT * FROM ( SELECT id, name, salary FROM emp ) t;如果table列显示 emp而不是derived2就说明被合并了。这时候加个LIMIT再看EXPLAIN SELECT * FROM ( SELECT id, name, salary FROM emp LIMIT 100000000 ) t;加了LIMIT之后就能看到derived2了。这个技巧在处理 1093 的时候非常有用。4.3 如何在 UPDATE/DELETE 里安全使用 ORDER BY 和 LIMITMySQL 的 UPDATE 和 DELETE 语法本身就支持ORDER BY和LIMIT这在分页删除、分批更新的场景很有用。但如果目标表仍然在子查询里出现还是会报 1093。我经常用这种写法给线上大表做分批清理DELETE FROM emp WHERE id IN ( SELECT id FROM ( SELECT id FROM emp WHERE salary 3000 ORDER BY id LIMIT 1000 ) t );内层SELECT id FROM emp ... LIMIT 1000每批只取 1000 个主键外层 DELETE 删除它们。包一层派生表是为了绕过 1093LIMIT同时承担了“强制物化”和“分批控制”两个职责一举两得。跑批程序可以循环执行这条语句直到影响行数为 0。同样的模式也适用于大表 UPDATE 字段比如给某张千万级表分批打标签。4.4 触发器Trigger里的 1093 陷阱有一种更隐蔽的情况会让你报错报得莫名其妙你的 UPDATE 语句本身没碰同一张表但表上有个触发器触发器里又更新了同一张表。比如表 A 上有个 AFTER UPDATE 触发器逻辑里对表 A 又做了一次 UPDATE这时 MySQL 会提示 1093因为触发器的底层执行环境和主语句共享同一个目标表写入状态。遇到这种情况常规办法是在触发器里改用SET NEW.field value的方式而不是对整张表执行 UPDATE。如果确实需要对同一张表的其他行做更新那大概率是表结构设计有问题建议重构数据模型或者把触发器逻辑改为异步任务。4.5 可复用的排查清单现象可能原因解决办法直接子查询报 1093目标表在 FROM 子句中被直接读取改成 JOIN、派生表、临时表或 CTE包一层派生表后仍报 1093优化器把派生表合并回原表内层加 LIMIT / DISTINCT / 聚合函数强制物化UPDATE 影响到不该改的行WHERE 子查询逻辑写错或连接条件不唯一先用 SELECT 查出来核对给连接字段建唯一索引临时表第二次创建报错上一会话未清理脚本结束前显式 DROP TEMPORARY TABLE触发器内部操作同一表报错触发器递归读写目标表改用 NEW/OLD 赋值或重构表设计8.0 下 WITH 子句语法错误版本低于 8.0.19降级改为 JOIN 派生表写法MySQL 更新后警告但没改数据新旧值相同Changed0属正常现象关注 Rows matched 即可4.6 再分享一个排查思路WHERE 之前先查更新之后马上验这条心得不花哨但真的管用。任何一次 UPDATE都遵循三步法先 SELECT 查看候选行和数量再 UPDATE 并注意影响行数最后 SELECT 验证结果。尤其是涉及 1093 这种“子查询逻辑不变只是改个写法”的场景你并不知道改写后的 JOIN 会不会因为连接顺序不同产生额外匹配所以先确认范围再动手是永远不会亏的习惯。我在公司内部的 MySQL 规范文档里第一条就写的这个。5. 扩展这类问题在其他数据库里的表现与迁移提示这里多聊两句因为现在很多项目喜欢做数据库迁移从 MySQL 迁到 PostgreSQL 或者反过来都很常见。不同数据库对“更新时子查询引用目标表”的处理策略并不一致。PostgreSQL 在语法层面看起来更宽松同样写UPDATE emp SET ... WHERE salary (SELECT AVG(salary) FROM emp ...)并不会像 MySQL 那样抛 1093。但这并不代表 PostgreSQL 会给出更优的执行计划它同样可能在内部做精细的优化只是错误提示策略不同。真正跨库迁移时如果直接把 MySQL 的 JOIN UPDATE 语法搬到 PostgreSQL会报语法错误因为 PG 的联表更新要写成UPDATE emp e SET ... FROM (SELECT ...) t WHERE e.dept_id t.dept_id。SQL Server 则提供了UPDATE ... FROM ... JOIN的等效写法还支持MERGE语句但心智模型都不一样。所以如果你只是单纯解决 MySQL 1093建议用派生表或 JOIN 就够如果项目有跨库迁移计划尽量在业务代码里把“先取目标ID集合再按主键更新”的逻辑拆成两条语句这样所有数据库都能接受。6. 个人经验与收尾坦白说错误代码 1093 并不算一个“多难”的问题一旦知道了套路基本是十几秒就能改完的事。但我觉得它真正的价值在于提醒我们写 UPDATE 之前一定要想想这条语句到底会“怎么读”和“怎么写”而不只是关心结果集对不对。数据量越大、表结构越复杂一条看起来没毛病的单表更新语句越可能藏着性能或一致性的坑。我在实际工作中踩过几次 1093 之后现在只要一写 UPDATE条件反射就是先用 SELECT 把行范围圈出来然后用 JOIN 或派生表改写最后用事务包一层再提交。这套流程看起来啰嗦但它能拦住绝大多数低级事故。如果你在别人的代码里看到类似这种包了一层又加 LIMIT 的写法也建议先别急着嗤之以鼻——在 MySQL 的老版本环境下那可能是当时唯一稳妥的解法。最后再分享一个小技巧遇到数据库报错不要只搜错误数字最好把错误消息的完整英文原文一起复制到搜索框里比如 “You can‘t specify target table for update in FROM clause”。因为同一个错误码在不同版本、不同语句里可能对应不同细节英文原文能帮你过滤掉大量重复且无效的中文问答。这个习惯比记住任何具体解法都值钱。
返回列表