← 返回首页

踩坑记录:用明细表回写主表,一次“数据救火”后的真心话

编程开发 亲身经历 公开
内容简要
数据救火:主表order_master数据缺失,通过明细表order_detail补回,过程中遇到多个坑点。
坑点
单号多条明细记录时,直接JOIN会导致写脏数据,且容易覆盖最新记录。
注意事项

1. 使用ROW_NUMBER()函数确保只取最新记录。

  • partition by单号分组
  • order by日期降序排列
  • rn = 1只取最新记录

2. 更新操作前先SELECT确认结果集,避免误更新。

  • 核对结果集后再执行更新

3. 操作前必须备份数据。

  • 备份时记录日期
  • 出问题时可恢复

4. 更新记录时留下操作痕迹,如更新remark字段和master_last_send字段。

  • remark字段标明明细恢复资料
  • master_last_send字段记录操作时间

5. 设计主表和明细表时,确保数据可验证、可恢复。

  • 先写明细,后写主表
  • 明细表先落盘并提交
  • 明细表是事实表/日志表,必须可信
  • 定期做一致性校验

踩坑记录:用明细表回写主表,一次“数据救火”后的真心话

日期: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;

需求也很清楚:

  1. 把 order_detail 的 check_user、check_date、status 写回 order_master;
  2. 主表的 remark 统一标成 明细恢复资料;
  3. 同一个 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 才丢人。

五、踩的第四个坑:改完数据,却没人知道“为什么改”

现场还原

如果只是把字段写回去,几个月后有人查到这条记录,一定会问:

“这个审核人怎么突然变了?谁改的?为什么?”

这时候如果你说“之前是空的,我用明细表补的”,对方大概率会再问:

“那你凭什么保证补的是对的?”

这次的改进

  1. 主表的 remark 统一写上:明细恢复资料;
  2. 把 master_last_send 更新成 sysdate,留下操作痕迹;
  3. (以后)写进恢复日志表,把改前 / 改后的值都记下来。

数据会说话,但得有人先教它怎么说。


六、真正的深水区:我们到底该怎么设计主表和明细表?

这次救火,让我们团队意识到一个老问题又被摆到了台面上:

主表和明细表,到底是“主从表”,还是“主表 + 日志表”?

6.1 强一致(外键)我们不敢用

  • 主外键约束太死,恢复、补数、异常单全都束手束脚;
  • 触发器一多,出了问题谁都说不清是哪段代码改的数据。

6.2 完全不管一致性,又迟早出事

  • 主表错了,明细不在;
  • 明细在,但没人知道哪条是最新、哪条是历史。

6.3 最后我们达成的共识(很重要)

主表和明细表不需要实时强一致,但必须“可验证、可恢复”。

具体落地成三句话:

  1. 先写明细,后写主表​

明细表先落盘并提交,主表后更新。这样即使主表写失败,恢复数据一定在。

  1. 明细永远不能丢​

明细表是“事实表 / 日志表”,它必须可信,它是我们最后的底牌。

  1. 定期做一致性校验​

用 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,都能睡得着觉。

本文内容仅代表作者个人观点、经历或转述,不代表任何官方立场,仅供参考,不构成任何专业建议。据此操作产生的风险由读者自行承担。
发布于 2026-11-06 10:47:50 ID: 45