踩坑记录:用明细表回写主表,一次“数据救火”后的真心话
日期:2026-11-06
模块:ordermaster / orderdetail 数据同步
操作人:传说
危险等级:🔥🔥🔥(会改生产数据,一定要先备份)
一、这次是来“救火”的
有同事反馈,主表 order_master 里有不少单子的审核人、审核时间、状态是空的,但对应的明细表 order_detail 里其实早就有审核记录了。
一句话:主表缺了数据,明细表里有。得用明细把主表“补”回来。
于是就有了那句很熟悉的 SQL:
select *
from order_master a,
order_detail b
where a.master_slip_no = b.detail_slip_no
and a.master_check_user is null
and b.detail_check_user is not null;需求也很清楚:
- 把
order_detail的check_user、check_date、status写回order_master; - 主表的
remark统一标成明细恢复资料; - 同一个
slip_no在明细表里有多条,必须用最新那一条来恢复。
二、踩的第一个坑:一个单号多条明细,随便 JOIN 会写脏数据
现场还原
查数据时大家就发现,明细表里同一个 slip_no 经常有两条、三条:
- 有的 2023 年一条,2024 年一条;
- 有的
status不一样,审核人也不一样。
这时候如果直接这么写:
update order_master a
set (a.master_check_user,
a.master_check_date,
a.master_status) =
(select b.detail_check_user,
b.detail_check_date,
b.detail_status
from order_detail b
where b.detail_slip_no = a.master_slip_no)
where ...数据库就会一脸无辜地告诉你:
ORA-01427: 单行子查询返回多行
或者更可怕的情况:你用 rownum = 1 硬接,结果回写的是最早那条老数据,最新的审核记录直接被“历史”覆盖。
当时是怎么栽的
一开始我脑子里想的是“取最新”,手上却随手写了个 order by detail_check_date,忘了套 ROW_NUMBER()。
结果一跑,同一单号多条记录还是一起冒出来,逻辑根本没收敛。
正确的姿势(记住这个模板)
select *
from (
select detail_slip_no,
detail_check_user,
detail_check_date,
detail_status,
row_number() over (
partition by detail_slip_no
order by detail_check_date desc
) rn
from order_detail
)
where rn = 1;partition by:按单号分组;order by ... desc:日期最新的排第一;rn = 1:只要最新那一条。
👉 以后凡是“一个主单对多个明细、取最新/最旧”的更新,先把这条 SQL 背下来。
三、踩的第二个坑:一上来就 UPDATE,差点把生产干懵
现场还原
当时我心里想的是:“先看看有多少条要改。”
于是直接跑:
update order_master set ... where ...然后才去数 sql%rowcount。
这是很典型、也很危险的“老司机翻车”——觉得逻辑没问题,就省了那一步。
为什么这是坑
- 万一条件写少一个
is null,会连带更新到本来就对的数据; - 一旦 COMMIT,回滚段不会替你记住“改之前长什么样”;
- 在 ERP / 财务这种系统里,“主表状态被批量改错”的锅,比写不出代码还难背。
这次立下的规矩
任何对生产表的 UPDATE / DELETE,必须先 SELECT 确认结果集,再执行更新。
所以这次我们定下的标准流程是:
-- 第一步:先看清要改什么
select a.master_slip_no,
a.master_check_user as old_user,
b.detail_check_user as new_user,
a.master_check_date as old_date,
b.detail_check_date as new_date,
a.master_status as old_status,
b.detail_status as new_status
from order_master a
join ( ... ) b
on a.master_slip_no = b.detail_slip_no
where a.master_check_user is null
and b.detail_check_user is not null;对着结果集一条条核对,确认无误再跑更新。
四、踩的第三个坑:忘了备份,晚上睡觉都不踏实
现场还原
这个坑不是我这次才踩的,是吃过的无数次亏之后总结出来的。
在做任何“补数、回写、修复”操作之前,如果不做备份,那你其实是在拿职业生涯赌运气。
这次的保命操作
create table order_master_bak_20261106
as
select * from order_master;日期写在表名里,以后谁看到都知道:
- 这是哪天备份的;
- 出事了能直接
insert into order_master select * from order_master_bak_20261106 where ...捞回来。
一条血泪经验
备份不丢人,不备份还敢在生产上 UPDATE 才丢人。
五、踩的第四个坑:改完数据,却没人知道“为什么改”
现场还原
如果只是把字段写回去,几个月后有人查到这条记录,一定会问:
“这个审核人怎么突然变了?谁改的?为什么?”
这时候如果你说“之前是空的,我用明细表补的”,对方大概率会再问:
“那你凭什么保证补的是对的?”
这次的改进
- 主表的
remark统一写上:明细恢复资料; - 把
master_last_send更新成sysdate,留下操作痕迹; - (以后)写进恢复日志表,把改前 / 改后的值都记下来。
数据会说话,但得有人先教它怎么说。
六、真正的深水区:我们到底该怎么设计主表和明细表?
这次救火,让我们团队意识到一个老问题又被摆到了台面上:
主表和明细表,到底是“主从表”,还是“主表 + 日志表”?
6.1 强一致(外键)我们不敢用
- 主外键约束太死,恢复、补数、异常单全都束手束脚;
- 触发器一多,出了问题谁都说不清是哪段代码改的数据。
6.2 完全不管一致性,又迟早出事
- 主表错了,明细不在;
- 明细在,但没人知道哪条是最新、哪条是历史。
6.3 最后我们达成的共识(很重要)
主表和明细表不需要实时强一致,但必须“可验证、可恢复”。
具体落地成三句话:
- 先写明细,后写主表
明细表先落盘并提交,主表后更新。这样即使主表写失败,恢复数据一定在。
- 明细永远不能丢
明细表是“事实表 / 日志表”,它必须可信,它是我们最后的底牌。
- 定期做一致性校验
用 SQL 找出:
- 主表有、明细没有的单;
- 明细有、主表没有的单;
- 两边状态不一致的单。
七、最终版脚本(这次生产用的就是这个)
-- 1. 备份
create table order_master_bak_20261106
as
select * from order_master;
-- 2. 先查(确认再往下走)
select a.master_slip_no,
a.master_check_user as old_user,
b.detail_check_user as new_user,
a.master_check_date as old_date,
b.detail_check_date as new_date,
a.master_status as old_status,
b.detail_status as new_status
from order_master a
join (
select *
from (
select detail_slip_no,
detail_check_user,
detail_check_date,
detail_status,
row_number() over (
partition by detail_slip_no
order by detail_check_date desc
) rn
from order_detail
)
where rn = 1
) b
on a.master_slip_no = b.detail_slip_no
where a.master_check_user is null
and b.detail_check_user is not null;
-- 3. 更新
update order_master a
set (a.master_check_user,
a.master_check_date,
a.master_status,
a.master_remark) =
(select b.detail_check_user,
b.detail_check_date,
b.detail_status,
'明细恢复资料'
from (
select *
from (
select detail_slip_no,
detail_check_user,
detail_check_date,
detail_status,
row_number() over (
partition by detail_slip_no
order by detail_check_date desc
) rn
from order_detail
)
where rn = 1
) b
where b.detail_slip_no = a.master_slip_no),
a.master_last_send = sysdate
where a.master_check_user is null
and exists (
select 1
from order_detail c
where c.detail_slip_no = a.master_slip_no
and c.detail_check_user is not null
);
commit;八、写在最后(一点心里话)
这次维护让我再一次确认:
- 写得快的 SQL,不一定是对的 SQL;
- “看起来能跑”和“真的改对了”,中间隔着很多坑;
- 一个老程序员的价值,不是他会写多少 UPDATE,而是他知道:哪个条件不能少、哪张表必须先备份、哪条数据改错了会出人命。
主表可以错,但明细不能缺; 主表可以重建,明细必须可信。
这次踩的坑,都是下次不踩的台阶。
愿我们写过的每一段 SQL,都能睡得着觉。