更新操作是业务中最常见的写操作之一,也是线上故障的高发区。一条没有加 WHERE 的 UPDATE 能在几秒内把整张表的数据毁掉。因此,UPDATE 的使用不仅要会写语法,更要养成安全执行的习惯。
8.2.1 单表更新
单表更新是最基本的形式,语法如下:
UPDATE 表名
SET 列名1 = 值1, 列名2 = 值2, ...
WHERE 条件;
要点解析:
SET子句可以同时更新多个列,用逗号分隔,比如SET status = 2, updated_at = NOW()。WHERE条件用来限定哪些行需要更新。不加 WHERE 会更新全表,这是最危险的误操作之一。- 更新时可以使用当前列的值进行运算,比如
SET count = count + 1,这在扣减库存、增加点赞数时非常常见。 - 更新操作受表上约束(主键、唯一键、非空等)的限制,违反约束时会报错回滚。
实用示例:
-- 更新某用户的状态
UPDATE users SET status = 1 WHERE id = 1001;
-- 批量更新:将过期的优惠券标记为无效
UPDATE coupons SET is_valid = 0 WHERE expire_time < NOW();
-- 运算更新:增加文章的阅读量
UPDATE articles SET view_count = view_count + 1 WHERE id = 2024;
性能提示: 单表更新一行如果 WHERE 条件用到了主键或唯一索引,速度非常快。如果 WHERE 条件没有索引,InnoDB 会对扫描到的所有行加锁,可能导致锁范围扩大,影响并发。因此在更新频繁的热点表上,务必保证 WHERE 条件能命中合适的索引。
8.2.2 多表更新
在实际业务中,我们经常需要根据另一个表的数据来更新当前表,比如“将订单表中所有已发货的子订单的父订单状态改为已完成”。这种跨表更新就是多表更新。MySQL 支持两种常见写法:JOIN 语法和子查询语法(8.0 还能用 CTE),推荐使用 JOIN 语法的 UPDATE,因为它更直观且性能往往更好。
语法结构:
UPDATE 表1
[INNER | LEFT] JOIN 表2 ON 连接条件
SET 表1.列 = 值或表达式, ...
[WHERE 条件];
示例:
-- 根据物流表的发货时间,更新订单表的发货状态
UPDATE orders o
INNER JOIN shipments s ON o.id = s.order_id
SET o.delivery_status = 'shipped', o.shipped_at = s.shipped_at
WHERE o.delivery_status = 'paid';
这里通过 INNER JOIN 连接了两张表,条件 o.id = s.order_id 表示只更新那些在 shipments 中有关联记录的订单。
也可以使用 LEFT JOIN 来更新,比如:
-- 将没有对应支付记录的订单标记为异常
UPDATE orders o
LEFT JOIN payments p ON o.id = p.order_id
SET o.status = 'abnormal'
WHERE p.id IS NULL AND o.status = 'pending';
注意:
- 在多表更新中,被更新的表必须出现在
UPDATE关键字后面,并且不能直接在 FROM 子句中再次引用(MySQL 的 UPDATE 不支持 FROM,要用 JOIN)。 - 连接条件和 WHERE 条件共同决定哪些行被更新。建议先在 SELECT 查询中确认要更新的行数和内容,再执行 UPDATE。
- 多表更新涉及的行锁可能分布在多张表上,如果 JOIN 的表较大或者缺少索引,可能导致大量的行锁甚至间隙锁,引发死锁风险。因此要确保连接列和 WHERE 条件列都有合适的索引。
8.2.3 安全更新规范
UPDATE 是一条威力巨大的语句,生产环境中对其使用必须慎之又慎。以下规范能够有效避免数据灾难。
1. 强制使用 WHERE 条件(或开启安全模式)
MySQL 提供了一个安全保护参数 sql_safe_updates,当设置为 ON 时,UPDATE 和 DELETE 必须满足以下条件之一才能执行:
- 包含 WHERE 且 WHERE 条件中使用了索引列;
- 包含 LIMIT 子句。
可以在会话级临时开启:SET sql_safe_updates = ON;。这样如果忘记写 WHERE,或者 WHERE 没有命中索引,MySQL 会直接报错拒绝执行。这是开发环境最推荐的安全设置。
2. 执行前用 SELECT 验证
在执行 UPDATE 之前,先用 SELECT 语句配合相同的 WHERE 条件查看会被影响的数据行。例如:
SELECT COUNT(*) FROM orders WHERE status = 'pending' AND created_at < '2023-01-01';
-- 确认数量符合预期后,再写 UPDATE
UPDATE orders SET status = 'expired' WHERE status = 'pending' AND created_at < '2023-01-01';
这个习惯能避免因为条件写错造成的误更新。
3. 将更新操作放入事务中,操作后二次确认
对于重要的数据变更,养成使用事务并手动提交的习惯:
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 此时可以查询确认更新结果
SELECT * FROM accounts WHERE id = 1;
-- 确认无误后提交
COMMIT;
-- 如果发现改错了,ROLLBACK 即可
注意,在 MySQL 8.0 中,DDL 操作(如 ALTER TABLE)已经支持原子化,但 DML 操作仍然依赖事务的显式控制。用事务包装修改,即使出错也能快速回滚。
4. 大批量更新分批处理
如果一次需要更新几百万行,不要写一条 SQL 全部更新。长事务会持有锁和 Undo Log,阻塞其他操作,还可能引起主从延迟。正确做法是分段批量更新,每批更新几千行便提交一次:
DELIMITER //
CREATE PROCEDURE batch_update()
BEGIN
DECLARE affected INT DEFAULT 1;
WHILE affected > 0 DO
START TRANSACTION;
UPDATE large_table SET status = 1 WHERE status = 0 LIMIT 5000;
SET affected = ROW_COUNT();
COMMIT;
DO SLEEP(0.1); -- 短暂休眠,避免持续冲击
END WHILE;
END//
DELIMITER ;
很多运维脚本都采用类似的批量处理方式,配合 LIMIT 和短暂休眠,把对线上服务的影响降到最低。
5. 避免在大表上更新索引列
频繁更新一个被许多查询依赖的索引列(如订单状态),可能会引发索引维护开销,还会导致查询计划频繁变化,产生锁竞争。如果确实需要更新,考虑是否可以通过增加字段(如 status_version)来减少对核心索引列的变更。
6. 线上变更流程与代码规范
- 敏感表的 UPDATE 操作建议走工单审批或代码 Review。
- 所有线上执行的 SQL 变更(包括 UPDATE)应当先在测试环境验证,并在从库或预发布环境试跑。
- 编写程序时,不要拼接来自用户输入的 WHERE 条件值,应使用参数化查询,防止 SQL 注入和条件被篡改。
以上规范并不复杂,但每一条背后都有血的教训。把它们融入到日常开发习惯中,可以避免绝大多数因手误导致的线上事故。