我们最近有个项目要上线,需要导入一些存量数据。
但有时候导入的时候,可能会出现字段格式不对、空值、重复、关联缺失等一系列问题,有时候为了图快,直接就开始用 DML 手动 update。
那在修数据的时候,怎样尽可能保证不出错呢。
说实话,一条 UPDATE 的条件写宽一点,几千行就没了。生产库又没有撤销键,按下去就回不来了。
所以下面这几件事,我每次都会做,一个也不敢省。
一、先让这批数据能被我找回来
动手改之前,我得先能回答一个问题,这批数据是哪一批,怎么把它们捞出来。
如果连这个都说不清,后面真出了事,你只能靠「大概下午三点那会儿导的」这种记忆去翻,那基本等于没救。
我的做法是导入的时候给组件 ID 加一个日期前缀,精确到分钟。
sql
-- 导入生成的 ID 长这样 202610192105000123
SELECT COUNT(*) FROM biz_component WHERE id LIKE '202610192105%';
前缀匹配是能走主键索引的,所以数据量不大的话,基本没什么感觉。
这样一来,改完也好,改错了也好,一条 SQL 就能把整批捞回来。
如果表结构允许加字段,其实单独加一个 batch_no 会更干净。不能加的时候,ID 前缀就是成本最低的折中,因为它不用动表结构。
有两个细节得注意一下。
一个是分钟级前缀可能会发生 id 冲突。同一分钟导两次,前缀就重了,这时候,两批数据分不开。加个自增序号或者秒级后缀可以解决问题。
另一个是主键带时间,等于 ID 里公开了哪天导的。内部系统无所谓,如果这串 ID 会露给外部看,那得掂量一下。
二、先查后改,WHERE 原样搬过去
这一步是最容易被跳过的,也是最救命的。
我的习惯是先别急着写 UPDATE,先用同一个 WHERE 查一遍,大致看一下,查询返回的数据是不是你要改的数据。
sql
-- 第一步,确认范围
SELECT id, name, status FROM biz_component
WHERE biz_type = 'A' AND status = 0 AND update_time < '2026-01-01';
-- 第二步,同一个 WHERE 挪到 UPDATE 后面
UPDATE biz_component
SET status = 1
WHERE biz_type = 'A' AND status = 0 AND update_time < '2026-01-01';
道理很简单,UPDATE 执行完,你只知道它影响了几行,到底改的是哪几条数据,它不会告诉你。SELECT 就不一样,它会把每一条都列出来给你看。所以先把范围查出来确认一遍,再动手改。
落地的时候有两个小地方要注意。
一是别只看条数,要看数据本身。我们这个查询能返回满足条件的行数,也能把所有的 rows 都列出来,所以我一般会顺手扫一眼,这些行是不是我要改的那些,字段值对不对,有没有多出来的行混进去。
二是把 WHERE 后面的查询条件直接粘到 UPDATE 后面。查的时候写的是什么条件,改的时候就还是那段条件,中间不用手打第二遍。这样 update 涉及的范围,肯定就是我们刚才 select 出来的范围,不多不少。
不过就算这样,还是会怕出错。范围对了,不代表中间不会出别的问题,所以后面还得再加一层事务兜着,真出事了能回滚。
三、SELECT 和 UPDATE 放在同一个事务里
这里有个特别容易想当然的地方。
很多人觉得,既然两条语句在同一个事务里,那它们看到的数据就是一样的。
不是这样的。
MySQL 的 InnoDB,默认隔离级别是 REPEATABLE READ。 普通的 SELECT 是一致性读,读的是事务开始时候的快照,而 UPDATE 是当前读,读的是最新提交的那一版。这两条语句之间别人提交的数据,UPDATE 是能看到的。
PostgreSQL 默认是 READ COMMITTED。 每条语句执行的时候各取一个新快照,SELECT 之后别人提交的行,UPDATE 一样看得见。
所以同一个事务只保证了一件事,要么都提交,要么都回滚。它不保证你查询的范围和修改的范围是同一个。
真要把范围锁死,得用加锁读。
sql
SELECT id FROM biz_component
WHERE biz_type = 'A' AND status = 0
FOR UPDATE;
这样后面的 UPDATE 面对的,就是你刚刚锁住的那批行。
如果这张表平时只有你一个人在写,那这一层可以省。要是共享表,说不定有别人同时在改,那就得加上。
四、范围对了,不代表值对了
影响行数正确,只能证明你改的行数是对的,证明不了你改的字段是对的。
sql
UPDATE biz_component SET status = amount WHERE id IN (...);
这种错误行数一个不差,等你 SELECT 查回来才发现字段串了。
所以改完还得再查一遍,把字段的值拉出来看,确认跟你想的一样,不能只看影响了几行。
sql
SELECT id, status, amount FROM biz_component WHERE id IN (...);
再稳一点的顺序是,改之前先把目标行备份一份。小表直接复制就行。
sql
CREATE TABLE biz_component_bak_20261019 AS SELECT * FROM biz_component;
大表可别这么干,一条语句可能锁很久,业务那边直接就炸了。换成按主键分批导出,或者先落一张变更记录表,把要改的 id 和改前改后的值都留下来。
分批的时候按主键区间往前走,一批几千行,跑一批提交一批。中途断了也能从断点接着来,不用从头再跑一遍。代价是它不再是一个整体,所以这时候批次号又多了一个用处,你随时能看出来哪一批跑过、哪一批没跑。
最后
如果你是一个新人,对业务的理解不够深,建议还是先去理解业务,再来操作数据库。
今天就先说到这里,我们下期再见。