UPDATE 语句看起来简单,但稍不注意就可能把整张表给毁了。这篇说说 UPDATE 的常见事故和正确写法。

经典事故:忘了加 WHERE

-- 本意:给 id=1 的用户加 100 积分
UPDATE user SET points = points + 100 WHERE id = 1;

-- 事故:忘了加 WHERE!
UPDATE user SET points = points + 100;  -- 全表所有用户都加 100 积分!

结果:积分全部加 100,如果用户量 100万,后果不堪设想。

怎么避免?

1. 先写 SELECT 验证

-- 先查一下,确认数据范围
SELECT * FROM user WHERE id = 1;  -- 确认只有一条
SELECT COUNT(*) FROM user WHERE points > 0;  -- 确认范围

-- 再执行 UPDATE
UPDATE user SET points = points + 100 WHERE id = 1;

2. 用事务包裹

-- 开启事务,先看结果,确认没问题再提交
START TRANSACTION;
UPDATE user SET points = points + 100 WHERE id = 1;

-- 检查一下
SELECT * FROM user WHERE id = 1;

-- 确认无误,提交
COMMIT;

-- 或者回滚
-- ROLLBACK;

3. 用 LIMIT 限制

-- 加 LIMIT 限制,即使忘加 WHERE 也只影响一条
UPDATE user SET points = points + 100 WHERE id = 1 LIMIT 1;

4. 加上 ORDER BY + LIMIT

-- 更新排序后的前几条
UPDATE user 
SET points = points + 100 
WHERE status = 'vip' 
ORDER BY created_at DESC 
LIMIT 100;

常见错误写法

1. 多表 UPDATE 忘了关联条件

-- 本意:给 Tom 的订单加 100
UPDATE order o, user u 
SET o.amount = o.amount + 100 
WHERE u.name = 'Tom' AND o.user_id = u.id;

-- 事故:没有关联条件,order 表全部更新!
UPDATE order o, user u 
SET o.amount = o.amount + 100 
WHERE u.name = 'Tom';  -- 所有订单都加 100!

正确写法

-- 方式1:用 JOIN
UPDATE order o
INNER JOIN user u ON o.user_id = u.id
SET o.amount = o.amount + 100
WHERE u.name = 'Tom';

-- 方式2:用子查询
UPDATE order 
SET amount = amount + 100 
WHERE user_id = (SELECT id FROM user WHERE name = 'Tom');

2. 字符串更新漏了引号

-- 正确:字符串加引号
UPDATE user SET name = 'Tom' WHERE id = 1;

-- 错误:没加引号,数据被截断
UPDATE user SET name = Tom WHERE id = 1;  -- 报错或数据错误

3. 负数没处理

-- 本意:扣 100 积分,最低扣到 0
UPDATE user SET points = points - 100 WHERE id = 1;
-- 事故:如果原来只有 50 积分,结果变成 -50!

-- 正确:IFNULL + GREATEST
UPDATE user SET points = GREATEST(points - 100, 0) WHERE id = 1;

4. 浮点数精度问题

-- 金额计算用 DECIMAL,别用 FLOAT
UPDATE account SET balance = balance - 0.1 WHERE id = 1;  -- FLOAT 有精度问题
UPDATE account SET balance = balance - DECIMAL('0.1') WHERE id = 1;  -- DECIMAL

批量 UPDATE 的正确姿势

1. 分批更新

-- 更新 10000 条数据,每批 500 条
UPDATE user SET points = points + 10 WHERE status = 'vip' LIMIT 500;
-- 执行 20 次

2. 用主键 IN

-- 已知 ID 列表,用 IN
UPDATE user 
SET points = points + 10 
WHERE id IN (1, 2, 3, 4, 5);

3. 临时表 + JOIN

-- 创建临时表存要更新的 ID
CREATE TEMPORARY TABLE tmp_ids (id BIGINT);
INSERT INTO tmp_ids VALUES (1), (2), (3);

-- JOIN 更新
UPDATE user u
INNER JOIN tmp_ids t ON u.id = t.id
SET u.points = u.points + 10;

DROP TEMPORARY TABLE tmp_ids;

怎么恢复误更新?

1. 有备份

-- 从备份恢复
mysql -u root -p database < backup.sql

2. 用 binlog 恢复

-- 查看 binlog,找误更新的时间点
SHOW BINLOG EVENTS IN 'mysql-bin.000001' FROM 123456;

-- 用 mysqlbinlog 解析
mysqlbinlog --start-datetime='2024-01-15 10:00:00' /var/lib/mysql/mysql-bin.000001

3. 反向补偿

-- 如果误加了 100 积分,扣回来
UPDATE user SET points = points - 100 WHERE id = 1;  -- 补偿

-- 如果误删了数据,从其他表恢复或从备份恢复

最佳实践

做法 说明
先 SELECT 再 UPDATE 验证数据范围
用事务包裹 可以回滚
加 LIMIT 防止全表事故
字符串加引号 防止数据错误
金额用 DECIMAL 避免精度问题
批量用主键 IN 精确控制范围

UPDATE 操作的黄金法则:先 SELECT,后 UPDATE;加 LIMIT,不忘 WHERE


相关阅读:

  • [MySQL 事务隔离级别详解]
    • [MySQL 锁机制完全指南]
    • [MySQL 批量更新最佳实践]
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐