六库合一:重名表冲突处理表
方案 §4.4 第 1 步要求的交付物。数据来源见 <code>legacy-db-actual.md</code>。
行数分两层,后一层取代前一层:
(首版;information_schema.TABLES.TABLE_ROWS估算TABLE_ROWS是采样值,不准)COUNT(*)精确值 + 跨库LEFT JOIN子集证明 —— 由 <code>tools/check_subset.py</code> 产出,2026-10-09 跑。 六个库在同一实例上,所以"副本里有多少行是主份没有的"可以问死,不用抽样。 本文件正文里的行数一律是这一层的实测值;被推翻的首版估算不再保留,避免误引。⚠️
zt_park是在线库,仍在写入:park_in_out_record两次跑分别是 97,980,500 / 97,980,455 行。差值是活数据,不影响结论(结论是"0 行独有")。
0. 通用规则
默认动作:保留"业务域归属库"里的那一份,其余份先不搬(老库保留只读,随时可取)。
因为 legacy-db-actual.md §1 的模块×库矩阵已证明代码访问的就是归属库那一份。
⚠️ 三条必须逐一确认的例外:
- 副本上有独有列 → 说明可能有代码在写副本,先查调用方(标 🔴)
- 两侧行数都非 0 → 不能假定"主份已包含副本全部行",必须做子集证明(标 🟡/🔴)
- 键要选对——见下
关于「键」
判定"副本的行在不在主份里",必须用业务键而不是主键:副本常换过 id 空间
(parking_lot 的 A150402000 在主份是 P15040225Z;td_b_wx_config 的
wx1e9001e5940605b1 在两侧 id 不同)。但键选错会造出成片假独有:
| 表 | 键 | 独有行数 | 真话在哪 |
|---|---|---|---|
t_vip_card_cust_info |
id → 27"独有" |
card_no → 0 |
同一批卡换了 id |
td_b_wx_config |
org_id → 18"独有" |
appid → 3 |
老库 org_id 大量为 NULL,NULL=NULL 不成立,join 必然落空 |
规律:键列里可能有 NULL,用它 join 就一定会误报。选键前先问 「这个列在老数据里是否普遍为空」。
td_b_wx_config的真实查询键是appid——WxPayApiConfigKit就是按 appId 找配置的(找不到直接抛IllegalStateException)。
1. ✅ 结构完全一致 —— 7 组,精确校验全部通过
结构(列+索引+排序规则)逐字节一致。子集证明全部 0 行独有,可直接丢副本。
| 表 | 保留 | 丢弃 | 行数(保留/丢弃) |
|---|---|---|---|
td_b_settlement |
zt_order_n |
zt_order |
0 / 0 |
td_b_settlement_org |
zt_order_n |
zt_order |
9 / 1 |
td_b_settlement_serial |
zt_order_n |
zt_order |
4 / 0 |
td_b_settlement_share_record |
zt_order_n |
zt_order |
33 / 20 |
td_p_parking_share_order_temp |
zt_order_n |
zt_order |
0 / 0 |
income_temp |
zt_order_n |
zt_park |
353 / 353 |
month_dchz |
zt_order_n |
zt_park |
199 / 199 |
table_15_free |
zt_order_n |
zt_park |
344 / 344 |
首版对
td_b_settlement_share_record/income_temp/month_dchz/table_15_free标的是"需抽样比对 / 取并集或查清差的 2 行"。精确校验后四项都是 0 行独有, 无需并集:table_15_free那个"差 2 行"是TABLE_ROWS估算误差,实测两侧都是 344。month_dchz/table_15_free两侧都没有主键,已改用业务键time,pl_no/dateStr,pl_no判定(键已固化进check_subset.py)。
2. ⚠️ 结构不一致 —— 12 组
2.1 可直接定:主份是超集且活跃(5 组)
主份列是超集,且子集证明 0 行独有 → 直接保留主份、丢副本。
| 表 | 保留 | 丢弃(行数) | 列差异 | 子集证明 |
|---|---|---|---|---|
parking_lot |
zt_park 3124 |
zt_bill 1603、zt_order 1465、zt_order_n 1979 |
主份 47 列;副本 37/37/45 | 见下方专段 |
park_in_out_record |
zt_park 97,980,455 |
zt_order 1,242,863 |
— | ✅ 0 行独有 |
t_vip_card_cust_info |
zt_bill 108,583 |
zt_order 1,912 |
主份多 cust_name/cust_phone/parent_cust_card_no |
✅ 0 行独有(按 card_no) |
tp_p_berths |
zt_park 32,750 |
zt_order_n 20,554 |
主份多 car_number_color |
见下方专段 |
tp_e_employee_charge |
zt_park 2,333 |
zt_order_n 940 |
主份多 7 列 | ✅ 0 行独有 |
park_in_out_record(原 §2.1 的抽样项,现已问死) 首版只做了"从副本抽 300 个order_id回主份查,命中 300/300"。 现在改跨库LEFT JOIN全量:副本 1,242,863 行,独有 0 行。 副本时间范围1970 ~ 2018-03-27(同库td_b_order也冻结在 2018-04-08), 印证它是那一代系统的历史切片。可直接丢弃。
tp_p_berths(判定为"陈旧快照",可丢) 按主键id:副本 20,554 行里 31 行的主份没有;但另有 1,681 行同id却指向了不同的(pl_no, berth_no)—— 即主份把这些车位号重新分配了。 按业务键(pl_no, berth_no):1,692 行"独有",涉及 54 个pl_no, 而这 54 个pl_no全部存在于zt_park.parking_lot(0 个是陌生停车场)。 且tp_p_berths是车位实时状态表(car_no/is_occupy/order_id/pda_in_time), 不是静态资产台账——陈旧快照没有保留价值。 交叉验证:grep -rl tp_p_berths zteits-order/src/main/= 无引用,order 代码根本不读它。 结论:丢。
parking_lot(4 库都有,最需要交代的一张)
保留 zt_park(3124 行,47 列 = 结构超集)。三份副本按 pl_no 的独有情况:
| 副本 | 行数 | 按 pl_no 独有 |
这些"独有"是什么 |
|---|---|---|---|
zt_bill |
1603 | 1 | P1101020TH(白米斜街立体停车场)→ 主份已有,是 P1101020ZR(pl_id 12407) |
zt_order |
1465 | 81 | 见下 |
zt_order_n |
1979 | 2 | P1101020TH(同上)、003(区域卡虚拟停车场)→ 主份都有同名行,换了号 |
zt_order 那 81 个独有 pl_no 分两类:
- 重编号的:
A150402000 创业东路→ 主份P15040225Z;A150402001 百货东路→P15040225W;A150402005 嘉华东路→P15040225Y。老A/C前缀系列是早期编号 - 剩余的:赤峰市路侧停车(双桥东区/粮市中区/三中街东区…),形如
P150402189~236
三份副本都没有代码在读(这是判定的关键):
grep -rl parking_lot zteits-bill/src/main/ → 无引用
grep -rl parking_lot zteits-pay/src/main/ → 只有注释("和 parking_lot 表中 source_type 保持一致")
grep -rl parking_lot zteits-order/src/main/ → 1 处:TdBParkDurationRepSmapper.xml:376 的一条维护 UPDATE
grep -rl parking_lot zteits-charge/src/main/ → 无引用
即:parking_lot 只有 park 模块在用;bill/pay/charge 那三份是纯死数据,
order 那份只被一条 update td_b_park_duration_rep pdr, parking_lot pl 维护语句碰到。
且副本是结构子集(缺 area_type/pl_category/push_state/support_etc 等列),
所以绝不可能"反向超集"。
结论:三份副本全丢;那 84 个独有 pl_no 先导出留痕(见 §5),
不并进主份——并进去等于把已下线/已重编号的老停车场重新塞回前台列表。
另注 zt_park 自身有 2 个重复 pl_no(P1101020SE、P320211004,各 2 行,
data_status 0/1 各一),合并时不阻塞,但新代码按 pl_no 取单条的地方要留意。
2.2 🔴 主份缺列 / 反向差异(4 组)
| 表 | 保留 | 丢弃 | 精确结论 |
|---|---|---|---|
sys_org |
zt_sys 242 |
zt_park 20、zt_order_n 20、zt_order 0 |
✅ 三份都 0 行独有(按 org_code)→ 全丢 |
td_c_cust_person |
zt_order 104,205(41 列,超集) |
zt_order_n 28,844(38 列) |
🔴 29 行独有 → 取并集 |
td_c_coupon_person |
zt_order 149,789 |
zt_order_n 5,738 |
✅ 0 行独有 → 丢 |
td_b_settlement_record |
zt_order_n 12,610 |
zt_order 90 |
✅ 0 行独有 → 丢(首版"建议并集"不必要) |
sys_org:首版的顾虑是"zt_order版short_name是 NOT NULL、zt_sys版可空, 说明有代码按非空语义写"。现在两件事分开看: ① 数据上三份副本都没有zt_sys里不存在的org_code→ 丢副本不丢数据; ② 保留的是可空那一侧,即约束变松,不会让谁的写入开始失败。 所以是全丢。⚠️ 但org_code在zt_sys里不唯一,sys_org只有 242 行却有重复org_code——这是主份自身的数据质量问题,与合库无关,单独记一笔。
td_c_cust_person(必须并集):29 行独有(按cust_id),其中 26 行data_state=1(有效), 建于 2017-08 ~ 2019-04,名称如排水1(×8)、高自办月(×8)、队里(×2)、文化馆、测试。 注意 order 侧只有 javadoc 注释提到该表(OrderChargeInfo.java:37的字段说明), 没有 SQL 读它;真正读td_c_cust_person的是 pay(TdBCustBindingSMapper/TdCAcctPayoutSMapper/TdCCouponPersonSMapper/TdBParkCardCustSMapper) 以及保留侧zt_order。也就是说这 29 个客户现在只有直接查库才能看到。处置:补进保留侧(29 行,
INSERT ... SELECT)。理由:① 无损原则; ② pay 侧那四个 mapper 若绑定了这些cust_id,补进去是修复(之前 join 落空); ③ 代价仅 29 行。⚠️ 这 29 行里有测试这类疑似测试数据,补进去会出现在客户列表—— 这一点需要业务方认一下(见 §5 待确认)。
2.3 🟡 两侧都像配置(3 组)
| 表 | 保留 | 丢弃 | 精确结论 |
|---|---|---|---|
td_b_wx_config |
zt_order_n 27 |
zt_order 22 |
🔴 按 appid 独有 3 行 → 取并集 |
tb_b_park_income_day |
zt_order_n 348,424 |
zt_order 1,774 |
✅ 0 行独有 → 丢(首版"1,796 行待定性"已定性) |
td_b_settlement_serial |
见 §1 | — | 结构差异(多 settlement_month)已在 §1 计为结构一致组;丢失侧 0 行 |
td_b_wx_config(这张必须走INSERT ... SELECT,不能手抄) 这是含密钥支付配置表(app_secret/encoding_aes_key/pay_api_secret), 所以全程只比appid、不看任何别的列。
- 按
org_id比会得到 18 行"独有"——假警报:老库那些行org_id是 NULL- 按
appid比:真正的独有只有 3 行,全是 2017 年建的、org_id为空:wxc70ad87faf3c67b9(2017-07-21)、wx099b7a61d9867fe0(2017-07-26)、wxe98c8ee886986377(2017-09-11)- 两侧
id空间不同(zt_order.id=4的wx1e9001e5940605b1在zt_order_n是id=1), 所以并集要按appid判重,不能按id插入,否则主键冲突- 另有一处结构差异:
zt_order版没有cert_name列,zt_order_n版有处置:把那 3 行补进保留侧(
INSERT ... SELECT,密钥值随 SQL 走、 不出现在脚本或日志里)。若不补:WxPayApiConfigKit按 appId 找不到配置会直接抛异常, 而这三个 appId 已 8 年未动、org_id为空,属早期试点。 建议补(代价 3 行,避免支付链路隐患),但同样列入 §5 待确认。
3. 与"跨模块直连"清单的交叉
legacy-db-actual.md §4.2 的副本表与 <code>table-inventory.md</code> §2 的
13 张"跨模块直连表"高度重叠。合库时两份清单合并处理:副本丢弃 + 跨模块调用改走
parking-module-api,正是项目红线第 7 条(禁止跨模块直接访问数据库)的落地。
⚠️ 本节新增两条证据级发现,都指向"副本其实没在被读":
parking_lot在zt_bill/zt_order/zt_charge里零引用,zt_order_n只有 1 处维护 UPDATEtp_p_berths在zteits-order里零引用td_c_cust_person在zteits-order里只有 javadoc 注释,真正在读的是 pay
→ 这三张的副本"丢弃"不仅数据安全,而且不存在调用方需要一起改, 比首版预估的改动面小。
4. 执行顺序
- §1(7 组):结构一致且证明 0 独有 → 直接丢副本
- §2.1(5 组):保留主份、丢副本(
park_in_out_record的证明已完成) - §2.2 / §2.3 的 3 处并集:
td_c_cust_person+29、td_b_wx_config+3 (td_b_wx_config按appid判重插入)——这是唯一需要搬数据的动作 - 留痕导出:
parking_lot84 个独有pl_no先导出成文件再丢
全程遵循:老库保留只读,任何一份在确认无用之前都不删。 合并后
check_subset.py应能全部退出 0(3 处并集做完后,"需人工处置"清零)。
5. 精确校验怎么重跑 / 结果记在哪
cd parking-cloud
python3 -I sql/tools/check_subset.py --env 私密/legacy-ro.env --sample 5 --timeout 900
- 退出码 0 = 所有待丢弃份都已被证明是保留份的子集
- 退出码 1 = 仍有需人工处置的(当前应剩 6 组:
parking_lot×3、tp_p_berths、td_c_cust_person、td_b_wx_config)。这 6 组是"待执行动作",不是"未定"——parking_lot×3 与tp_p_berths的判定为"丢"(§2.1 已给证据), 另两组的动作是"并集"(§2.2/§2.3)
仍需业务方点头的两处(数据无损,但会改变"看得见的内容")
td_c_cust_person补进 29 行客户 —— 其中含测试、高自办月等疑似测试账号, 补进后会出现在客户列表td_b_wx_config补进 3 行 2017 年的微信支付配置 —— 补进后这三个 appId 从"查不到配置"变成"查得到"
两处都不做也不会丢"业务不可见"的数据(order 侧没有任何 SQL 读它们), 但都不补就等于默认删掉这批历史行。按"先取并集"的保守方向默认补; 若业务方确认是废弃数据,则改为"只导出留痕、不并库",并在本节记录决定。