WIP-P0A-CLEANUP.sql 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. -- WIP-P0A-CLEANUP.sql
  2. -- P0-A 数据纠偏:精准清 std 种群 A + 保全 DWD 不可再生历史 + 清空重建 DWD + schema 收口。
  3. --
  4. -- ⚠️ 占位文件名(CLAUDE.md §二·7):本仓与 origin/master 当前处于分叉状态且 1.0.536~538 已撞号,
  5. -- 无法在此刻取正式版本号。提交前 rename 为 1.0.<next>.sql 并注册 csproj Copy 条目。
  6. --
  7. -- ============================================================================
  8. -- 背景(全部为只读实测结论,证据见本批审计回执)
  9. -- dwd_process_outsource_delivery 曾混装两个业务对象:
  10. -- 种群 A = RoutingOpDetail(S0 标准工艺路线明细,主数据),无 work_order / po_no / po_line,
  11. -- 其 std 物化把 po_no/po_line 硬编码为 NULL;而 uk_work_op_stat 含这两列,
  12. -- MySQL 唯一索引 NULL-distinct 语义使 ODKU 永不命中 → 每轮全量追加。
  13. -- 种群 B = PurOrdMaster(Potype='PW') JOIN PurOrdDetail,真实工序委外采购订单行。
  14. -- 代码侧止血已完成(S3_ROUTING_OUTSOURCE 入站注册与 std INSERT 已移除,DWD 投影已补
  15. -- IFNULL(po_no,'') 使 po_no 不再为 NULL)。本脚本只处理存量。
  16. --
  17. -- Option A 目标粒度:一行 = 一条真实 PW 委外订单行在某 stat_date 的交付状态。
  18. -- 目标唯一键:(tenant_id, factory_id, stat_date, po_no, po_line)。
  19. --
  20. -- 历史 stat_date 不可复现(@StatDate = 运行时墙钟日期,无回填入参),且 31 行对应的源单
  21. -- 已从 PurOrdMaster 物理删除 → 保全那 31 行就是历史恢复方案,不做"重新计算"。
  22. -- ============================================================================
  23. SET NAMES utf8mb4;
  24. -- ===========================================================================
  25. -- Step 0 · 前置门禁(fail-fast;任一不成立即中止,绝不带着错误前提往下做)
  26. -- 断言集合关系而非硬编码行数:行数会随跑批漂移,集合关系不会。
  27. -- ===========================================================================
  28. -- 种群 A 主谓词(与审计一致:批次来源 + biz_key 形态 + 结构特征三重冗余)
  29. SET @std_total := (SELECT COUNT(*) FROM `mdp_std_process_outsource_order`);
  30. SET @pop_a := (
  31. SELECT COUNT(*) FROM `mdp_std_process_outsource_order`
  32. WHERE `sync_batch_id` LIKE 'S3\_MDP\_%'
  33. AND `source_biz_key` NOT LIKE '%|%'
  34. AND `work_order` = ''
  35. AND `po_no` IS NULL
  36. AND `po_line` IS NULL
  37. );
  38. SET @survivors := (
  39. SELECT COUNT(*) FROM `mdp_std_process_outsource_order`
  40. WHERE NOT (`sync_batch_id` LIKE 'S3\_MDP\_%'
  41. AND `source_biz_key` NOT LIKE '%|%'
  42. AND `work_order` = ''
  43. AND `po_no` IS NULL
  44. AND `po_line` IS NULL)
  45. );
  46. SELECT @std_total AS std_total, @pop_a AS population_a, @survivors AS survivors;
  47. -- G1 · 集合完备:种群 A + 幸存者 必须恰好等于总量(不允许存在第三类)
  48. SET @sql := IF(@pop_a + @survivors <> @std_total,
  49. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-G1: population_a + survivors <> std_total — predicate incomplete, abort'",
  50. 'DO 0');
  51. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  52. -- G2 · 血缘交叉验证:主谓词命中集必须与 stg 血缘回溯集完全一致(双向差集为 0)
  53. -- 注意:不得在 EXISTS 里附加 source_system 等值条件 —— stg 侧两代分属
  54. -- AIDOP(colon 形态) 与 AIDOPDEV_MYSQL(digits 形态),而 std 侧被统一硬编码为 'AIDOP',
  55. -- 加上该条件会假阴约 7.6 万行。
  56. SET @main_not_prov := (
  57. SELECT COUNT(*) FROM `mdp_std_process_outsource_order` o
  58. WHERE o.`sync_batch_id` LIKE 'S3\_MDP\_%' AND o.`source_biz_key` NOT LIKE '%|%'
  59. AND o.`work_order` = '' AND o.`po_no` IS NULL AND o.`po_line` IS NULL
  60. AND NOT EXISTS (SELECT 1 FROM `mdp_stg_work_order_material` s
  61. WHERE s.`source_table` = 'RoutingOpDetail'
  62. AND s.`source_biz_key` = o.`source_biz_key`)
  63. );
  64. SET @prov_not_main := (
  65. SELECT COUNT(*) FROM `mdp_std_process_outsource_order` o
  66. WHERE EXISTS (SELECT 1 FROM `mdp_stg_work_order_material` s
  67. WHERE s.`source_table` = 'RoutingOpDetail'
  68. AND s.`source_biz_key` = o.`source_biz_key`)
  69. AND NOT (o.`sync_batch_id` LIKE 'S3\_MDP\_%' AND o.`source_biz_key` NOT LIKE '%|%'
  70. AND o.`work_order` = '' AND o.`po_no` IS NULL AND o.`po_line` IS NULL)
  71. );
  72. SELECT @main_not_prov AS main_not_provenance, @prov_not_main AS provenance_not_main;
  73. SET @sql := IF(@main_not_prov > 0 OR @prov_not_main > 0,
  74. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-G2: cleanup predicate disagrees with stg provenance — abort'",
  75. 'DO 0');
  76. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  77. -- G3 · 幸存者必须全部是真实 PW 行(biz_key 形如 PurOrd|Line 且 po_no 非空)
  78. SET @bad_survivor := (
  79. SELECT COUNT(*) FROM `mdp_std_process_outsource_order`
  80. WHERE NOT (`sync_batch_id` LIKE 'S3\_MDP\_%' AND `source_biz_key` NOT LIKE '%|%'
  81. AND `work_order` = '' AND `po_no` IS NULL AND `po_line` IS NULL)
  82. AND (`source_biz_key` NOT LIKE '%|%' OR IFNULL(`po_no`,'') = '')
  83. );
  84. SET @sql := IF(@bad_survivor > 0,
  85. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-G3: survivor set contains non-PW rows — abort'",
  86. 'DO 0');
  87. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  88. -- G4 · overlap:种群 A 与真实 PW 集不得有交集
  89. SET @overlap := (
  90. SELECT COUNT(*) FROM `mdp_std_process_outsource_order`
  91. WHERE `sync_batch_id` LIKE 'S3\_MDP\_%' AND `source_biz_key` NOT LIKE '%|%'
  92. AND `work_order` = '' AND `po_no` IS NULL AND `po_line` IS NULL
  93. AND `source_biz_key` LIKE '%|%'
  94. );
  95. SET @sql := IF(@overlap > 0,
  96. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-G4: population_a overlaps real PW rows — abort'",
  97. 'DO 0');
  98. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  99. -- G5 · 待保全的 DWD 历史必须非空且 po_no/po_line 干净
  100. -- ⚠️ 谓词必须是 IFNULL(po_no,'') <> '' 而不是 po_no IS NOT NULL:
  101. -- 代码侧 IFNULL 补丁上线后,种群 A 的行 po_no 变成**空串**而非 NULL,
  102. -- 用 IS NOT NULL 会把这些垃圾一并当成"不可再生历史"保下来。
  103. SET @preserve_cnt := (SELECT COUNT(*) FROM `dwd_process_outsource_delivery` WHERE IFNULL(`po_no`,'') <> '');
  104. SELECT @preserve_cnt AS dwd_rows_to_preserve;
  105. SET @sql := IF(@preserve_cnt = 0,
  106. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-G5: no real-PO rows found in DWD — refuse to truncate blindly'",
  107. 'DO 0');
  108. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  109. -- ===========================================================================
  110. -- Step 1 · 保全 DWD 不可再生历史 + 元数据留证
  111. -- 只备份真实 PO 子集(约 31 行),不复制 3269 万行错误派生数据:
  112. -- 零消费者 / 零 FK / 可重建(今后)/ 历史上已 TRUNCATE 过 / 权威源仍在。
  113. -- ===========================================================================
  114. CREATE TABLE IF NOT EXISTS `dwd_process_outsource_delivery_keep_p0a` LIKE `dwd_process_outsource_delivery`;
  115. INSERT IGNORE INTO `dwd_process_outsource_delivery_keep_p0a`
  116. SELECT * FROM `dwd_process_outsource_delivery` WHERE IFNULL(`po_no`,'') <> '';
  117. -- 保全完整性断言:备份行数必须等于源侧真实 PO 行数
  118. SET @kept := (SELECT COUNT(*) FROM `dwd_process_outsource_delivery_keep_p0a`);
  119. SET @sql := IF(@kept <> @preserve_cnt,
  120. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-S1: preserved row count mismatch — abort before truncate'",
  121. 'DO 0');
  122. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  123. -- 元数据留证(复用 1.0.350 既有范式与表)
  124. CREATE TABLE IF NOT EXISTS `aidop_migration_derived_cleanup_log` (
  125. `id` BIGINT NOT NULL AUTO_INCREMENT,
  126. `migration_version` VARCHAR(32) NOT NULL,
  127. `table_name` VARCHAR(128) NOT NULL,
  128. `estimated_rows` BIGINT NOT NULL DEFAULT 0,
  129. `data_length` BIGINT NOT NULL DEFAULT 0,
  130. `reason` VARCHAR(500) NOT NULL,
  131. `captured_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  132. PRIMARY KEY (`id`),
  133. UNIQUE KEY `uk_migration_cleanup` (`migration_version`,`table_name`)
  134. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  135. INSERT INTO `aidop_migration_derived_cleanup_log`
  136. (`migration_version`,`table_name`,`estimated_rows`,`data_length`,`reason`)
  137. SELECT 'WIP-P0A','dwd_process_outsource_delivery',IFNULL(TABLE_ROWS,0),IFNULL(DATA_LENGTH,0),
  138. CONCAT('P0-A:RoutingOpDetail 主数据被误建模为委外交付事实,叠加 uk 含可空 po_no 致 ODKU 永不命中,膨胀至 3269 万行;',
  139. '真实 PO 子集 ', @preserve_cnt, ' 行已存入 dwd_process_outsource_delivery_keep_p0a 并于 schema 收口后回插;',
  140. '其余为错误派生数据,零消费者、零 FK、可由 std 重新生成,不做全量备份')
  141. FROM information_schema.TABLES
  142. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='dwd_process_outsource_delivery'
  143. ON DUPLICATE KEY UPDATE `reason`=VALUES(`reason`), `estimated_rows`=VALUES(`estimated_rows`), `data_length`=VALUES(`data_length`);
  144. -- ===========================================================================
  145. -- Step 2 · 精准清 std 种群 A(保留真实 PW;不得 TRUNCATE 整表)
  146. -- 备份沿用 1.0.351/352/353 的范式 A:CREATE TABLE LIKE + INSERT IGNORE + DELETE。
  147. -- ===========================================================================
  148. CREATE TABLE IF NOT EXISTS `mdp_std_process_outsource_order_bak_p0a` LIKE `mdp_std_process_outsource_order`;
  149. INSERT IGNORE INTO `mdp_std_process_outsource_order_bak_p0a`
  150. SELECT * FROM `mdp_std_process_outsource_order`
  151. WHERE `sync_batch_id` LIKE 'S3\_MDP\_%'
  152. AND `source_biz_key` NOT LIKE '%|%'
  153. AND `work_order` = ''
  154. AND `po_no` IS NULL
  155. AND `po_line` IS NULL;
  156. DELETE FROM `mdp_std_process_outsource_order`
  157. WHERE `sync_batch_id` LIKE 'S3\_MDP\_%'
  158. AND `source_biz_key` NOT LIKE '%|%'
  159. AND `work_order` = ''
  160. AND `po_no` IS NULL
  161. AND `po_line` IS NULL;
  162. -- 清理后立即断言:种群 A 归零,且幸存者数量与门禁阶段一致
  163. SET @pop_a_after := (
  164. SELECT COUNT(*) FROM `mdp_std_process_outsource_order`
  165. WHERE `sync_batch_id` LIKE 'S3\_MDP\_%' AND `source_biz_key` NOT LIKE '%|%'
  166. AND `work_order` = '' AND `po_no` IS NULL AND `po_line` IS NULL
  167. );
  168. SET @std_after := (SELECT COUNT(*) FROM `mdp_std_process_outsource_order`);
  169. SELECT @pop_a_after AS population_a_after, @std_after AS std_total_after;
  170. SET @sql := IF(@pop_a_after <> 0 OR @std_after <> @survivors,
  171. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-S2: std cleanup result mismatch — abort before DWD truncate'",
  172. 'DO 0');
  173. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  174. -- 注:tenant 0 / 1300000000001 的真实 PW 行仍保留在 std。
  175. -- "进不了 DWD"(被 CHECK 拦)不等于"应该从 std 删除"。
  176. -- ===========================================================================
  177. -- Step 3 · 清空 DWD
  178. -- 选 TRUNCATE 而非分批 DELETE:错误数据无业务价值、零 FK、零消费者,
  179. -- 且 DELETE 会产生约 5-8GB binlog(ROW+FULL image)与数十 GB undo,
  180. -- 却**不把空间归还 OS**;TRUNCATE 立即归还约 7.9GB。
  181. -- ===========================================================================
  182. TRUNCATE TABLE `dwd_process_outsource_delivery`;
  183. -- ===========================================================================
  184. -- Step 4 · 空表期 schema 收口(此时行数为 0,DDL 代价最低且不会因存量 NULL 失败)
  185. -- ===========================================================================
  186. -- 4.1 po_no / po_line → NOT NULL DEFAULT ''(幂等:仅当当前仍 nullable 时执行)
  187. SET @po_no_nullable := (SELECT IS_NULLABLE FROM information_schema.COLUMNS
  188. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='dwd_process_outsource_delivery' AND COLUMN_NAME='po_no');
  189. SET @sql := IF(@po_no_nullable = 'YES',
  190. "ALTER TABLE `dwd_process_outsource_delivery` MODIFY COLUMN `po_no` VARCHAR(100) NOT NULL DEFAULT ''",
  191. 'DO 0');
  192. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  193. SET @po_line_nullable := (SELECT IS_NULLABLE FROM information_schema.COLUMNS
  194. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='dwd_process_outsource_delivery' AND COLUMN_NAME='po_line');
  195. SET @sql := IF(@po_line_nullable = 'YES',
  196. "ALTER TABLE `dwd_process_outsource_delivery` MODIFY COLUMN `po_line` VARCHAR(50) NOT NULL DEFAULT ''",
  197. 'DO 0');
  198. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  199. -- 4.2 唯一键改为 Option A 粒度:(tenant_id, factory_id, stat_date, po_no, po_line)
  200. -- 旧键 uk_work_op_stat 含 work_order/op_code,对 RoutingOpDetail 来源无区分度。
  201. SET @has_old_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
  202. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='dwd_process_outsource_delivery' AND INDEX_NAME='uk_work_op_stat');
  203. SET @sql := IF(@has_old_uk > 0,
  204. 'ALTER TABLE `dwd_process_outsource_delivery` DROP INDEX `uk_work_op_stat`',
  205. 'DO 0');
  206. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  207. SET @has_new_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
  208. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='dwd_process_outsource_delivery' AND INDEX_NAME='uk_po_line_stat');
  209. SET @sql := IF(@has_new_uk = 0,
  210. 'ALTER TABLE `dwd_process_outsource_delivery` ADD UNIQUE INDEX `uk_po_line_stat` (`tenant_id`,`factory_id`,`stat_date`,`po_no`,`po_line`)',
  211. 'DO 0');
  212. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  213. -- 4.3 租户守卫 CHECK:按**实际名**处理,不得沿用 1.0.353 声明的名字
  214. -- 实测漂移:1.0.353 声明 ck_dwd_process_outsource_valid_tenant,
  215. -- 但该表在 1.0.353 之后被 CodeFirst 重建过,约束被匿名重建为
  216. -- dwd_process_outsource_delivery_chk_1(全库唯一的自动命名 CHECK)。
  217. -- 此处按语义(CHECK_CLAUSE 含 tenant_id 且含 1300000000001)定位,统一收敛到稳定名。
  218. SET @tenant_chk_cnt := (
  219. SELECT COUNT(*) FROM information_schema.CHECK_CONSTRAINTS cc
  220. JOIN information_schema.TABLE_CONSTRAINTS tc
  221. ON tc.CONSTRAINT_SCHEMA=cc.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME=cc.CONSTRAINT_NAME
  222. WHERE tc.TABLE_SCHEMA=DATABASE() AND tc.TABLE_NAME='dwd_process_outsource_delivery'
  223. AND tc.CONSTRAINT_TYPE='CHECK'
  224. AND cc.CHECK_CLAUSE LIKE '%tenant_id%' AND cc.CHECK_CLAUSE LIKE '%1300000000001%'
  225. );
  226. SET @sql := IF(@tenant_chk_cnt > 1,
  227. "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'P0A-S4: duplicate tenant-guard CHECK constraints found — manual review required'",
  228. 'DO 0');
  229. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  230. SET @tenant_chk_name := (
  231. SELECT tc.CONSTRAINT_NAME FROM information_schema.CHECK_CONSTRAINTS cc
  232. JOIN information_schema.TABLE_CONSTRAINTS tc
  233. ON tc.CONSTRAINT_SCHEMA=cc.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME=cc.CONSTRAINT_NAME
  234. WHERE tc.TABLE_SCHEMA=DATABASE() AND tc.TABLE_NAME='dwd_process_outsource_delivery'
  235. AND tc.CONSTRAINT_TYPE='CHECK'
  236. AND cc.CHECK_CLAUSE LIKE '%tenant_id%' AND cc.CHECK_CLAUSE LIKE '%1300000000001%'
  237. LIMIT 1
  238. );
  239. -- 若现存名不是稳定名,则丢弃后按稳定名重建(空表期,代价可忽略)
  240. SET @sql := IF(@tenant_chk_name IS NOT NULL AND @tenant_chk_name <> 'ck_dwd_process_outsource_valid_tenant',
  241. CONCAT('ALTER TABLE `dwd_process_outsource_delivery` DROP CHECK `', @tenant_chk_name, '`'),
  242. 'DO 0');
  243. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  244. SET @has_stable_chk := (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
  245. WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='dwd_process_outsource_delivery'
  246. AND CONSTRAINT_TYPE='CHECK' AND CONSTRAINT_NAME='ck_dwd_process_outsource_valid_tenant');
  247. SET @sql := IF(@has_stable_chk = 0,
  248. 'ALTER TABLE `dwd_process_outsource_delivery` ADD CONSTRAINT `ck_dwd_process_outsource_valid_tenant` CHECK (`tenant_id` NOT IN (0,1,1300000000001))',
  249. 'DO 0');
  250. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  251. -- ===========================================================================
  252. -- Step 5 · 回插保全的历史
  253. -- 显式排除非法租户(不依赖隐式 filter),并显式要求 po_no/po_line 非空。
  254. -- ===========================================================================
  255. INSERT INTO `dwd_process_outsource_delivery`
  256. SELECT * FROM `dwd_process_outsource_delivery_keep_p0a`
  257. WHERE `tenant_id` NOT IN (0,1,1300000000001)
  258. AND IFNULL(`po_no`,'') <> ''
  259. AND IFNULL(`po_line`,'') <> '';
  260. SELECT COUNT(*) AS dwd_rows_after_restore FROM `dwd_process_outsource_delivery`;
  261. SELECT `estimated_rows` AS rows_before_cleanup FROM `aidop_migration_derived_cleanup_log`
  262. WHERE `migration_version`='WIP-P0A' AND `table_name`='dwd_process_outsource_delivery';
  263. -- ===========================================================================
  264. -- 回滚(注释,非可执行;执行前请确认无消费者)
  265. -- std:INSERT IGNORE INTO mdp_std_process_outsource_order
  266. -- SELECT * FROM mdp_std_process_outsource_order_bak_p0a;
  267. -- dwd:错误派生数据不可回滚也不需要回滚(零消费者、可由 std 重新生成);
  268. -- 真实历史由 dwd_process_outsource_delivery_keep_p0a 保全,可再次回插。
  269. -- schema:ALTER ... MODIFY po_no/po_line 恢复 nullable;
  270. -- DROP INDEX uk_po_line_stat; ADD UNIQUE uk_work_op_stat(...原列序...)。
  271. -- ⚠️ 回滚 schema 会让 ODKU 重新失效、膨胀复发,仅在确认整改方案作废时执行。
  272. -- ===========================================================================