-- 1.0.296.sql S0 Warehouse P1:ItemPackMaster / NbrControl 唯一索引租户化(幂等 + 冲突前置守卫)。 -- 目的:唯一约束从「全局业务键」改为「租户内业务键」,使不同租户可复用相同业务编码, -- 同一租户内重复业务键仍被数据库拒绝。 -- 不改字段类型、不改 tenant_id 可空性、不插入/更新/删除任何业务数据、不动其它表索引。 -- 业务键依据(information_schema 实证 + 消费链路审计): -- ItemPackMaster:历史唯一键 (Domain, ItemNum);Domain 与 factory_ref_id 在数据中 1:1; -- tenant 内唯一键 = (tenant_id, Domain, ItemNum)。 -- NbrControl :历史唯一键 (domain_code, NbrType);domain_code NOT NULL(现值全为空串,作安全唯一锚), -- NbrType 为真实单号类型码;tenant 内唯一键 = (tenant_id, domain_code, NbrType)。 -- ============================================================================ -- ========== 一、冲突前置检测(同租户内重复则跳过创建,绝不自动清理历史数据) ========== SET @ipk_dup := (SELECT COUNT(*) FROM ( SELECT tenant_id, `Domain`, ItemNum FROM ItemPackMaster GROUP BY tenant_id, `Domain`, ItemNum HAVING COUNT(*) > 1) t); SET @nbr_dup := (SELECT COUNT(*) FROM ( SELECT tenant_id, domain_code, NbrType FROM NbrControl GROUP BY tenant_id, domain_code, NbrType HAVING COUNT(*) > 1) t); SELECT @ipk_dup AS itempack_intratenant_dup, @nbr_dup AS nbrcontrol_intratenant_dup; -- 期望均为 0;若非 0,本脚本不会创建新唯一索引(下方 ADD 被守卫跳过),需人工治理后重跑。 -- ========== 二、ItemPackMaster:旧全局唯一索引 -> 租户内唯一索引 ========== -- 2.1 删除旧唯一索引 uk_ItemPackMaster_domain_itemnum(存在才删) SET @has_old_ipk := (SELECT COUNT(DISTINCT INDEX_NAME) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ItemPackMaster' AND INDEX_NAME = 'uk_ItemPackMaster_domain_itemnum'); SET @sql := IF(@has_old_ipk > 0, 'ALTER TABLE ItemPackMaster DROP INDEX uk_ItemPackMaster_domain_itemnum', 'DO 0'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; -- 2.2 创建新租户内唯一索引(不存在且无同租户冲突才建) SET @has_new_ipk := (SELECT COUNT(DISTINCT INDEX_NAME) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ItemPackMaster' AND INDEX_NAME = 'uk_ItemPackMaster_tenant_domain_itemnum'); SET @sql := IF(@has_new_ipk = 0 AND @ipk_dup = 0, 'ALTER TABLE ItemPackMaster ADD UNIQUE INDEX uk_ItemPackMaster_tenant_domain_itemnum (tenant_id, `Domain`, ItemNum)', 'DO 0'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; -- 2.3 处理隐藏的第二个全局唯一索引 ix_ItemPackMaster (Domain, ItemNum)。 -- 该索引 NON_UNIQUE=0(唯一),与被删的 uk 同样强制全局唯一,会继续阻断跨租户相同 (Domain, ItemNum),必须一并处理。 -- 方案:删除其“唯一”版本,重建为“非唯一”同名索引(保留 (Domain, ItemNum) 查询性能,但不再强制全局唯一)。 SET @ix_unique := (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ItemPackMaster' AND INDEX_NAME = 'ix_ItemPackMaster' AND NON_UNIQUE = 0); SET @sql := IF(@ix_unique > 0, 'ALTER TABLE ItemPackMaster DROP INDEX ix_ItemPackMaster', 'DO 0'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; SET @has_ix := (SELECT COUNT(DISTINCT INDEX_NAME) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ItemPackMaster' AND INDEX_NAME = 'ix_ItemPackMaster'); SET @sql := IF(@has_ix = 0, 'ALTER TABLE ItemPackMaster ADD INDEX ix_ItemPackMaster (`Domain`, ItemNum)', 'DO 0'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; -- ========== 三、NbrControl:旧全局唯一索引 -> 租户内唯一索引 ========== -- 3.1 删除旧唯一索引 uk_NbrControl_domain_nbrtype(存在才删) SET @has_old_nbr := (SELECT COUNT(DISTINCT INDEX_NAME) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'NbrControl' AND INDEX_NAME = 'uk_NbrControl_domain_nbrtype'); SET @sql := IF(@has_old_nbr > 0, 'ALTER TABLE NbrControl DROP INDEX uk_NbrControl_domain_nbrtype', 'DO 0'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; -- 3.2 创建新租户内唯一索引(不存在且无同租户冲突才建) SET @has_new_nbr := (SELECT COUNT(DISTINCT INDEX_NAME) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'NbrControl' AND INDEX_NAME = 'uk_NbrControl_tenant_domain_nbrtype'); SET @sql := IF(@has_new_nbr = 0 AND @nbr_dup = 0, 'ALTER TABLE NbrControl ADD UNIQUE INDEX uk_NbrControl_tenant_domain_nbrtype (tenant_id, domain_code, NbrType)', 'DO 0'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; -- ========== 四、执行后验证 ========== SELECT TABLE_NAME, INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX, COLUMN_NAME FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('ItemPackMaster', 'NbrControl') AND INDEX_NAME <> 'PRIMARY' ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX; -- 期望:ItemPackMaster 仅 uk_ItemPackMaster_tenant_domain_itemnum(唯一) + ix_ItemPackMaster(NON_UNIQUE=1); -- NbrControl 仅 uk_NbrControl_tenant_domain_nbrtype(唯一)。均无全局唯一 (Domain,ItemNum)/(domain_code,NbrType)。 -- ============================================================================ -- 回滚 SQL(如需恢复到全局唯一索引,历史数据无跨租户同键冲突时可执行): -- ALTER TABLE ItemPackMaster DROP INDEX uk_ItemPackMaster_tenant_domain_itemnum; -- ALTER TABLE ItemPackMaster DROP INDEX ix_ItemPackMaster; -- ALTER TABLE ItemPackMaster ADD UNIQUE INDEX uk_ItemPackMaster_domain_itemnum (`Domain`, ItemNum); -- ALTER TABLE ItemPackMaster ADD UNIQUE INDEX ix_ItemPackMaster (`Domain`, ItemNum); -- ALTER TABLE NbrControl DROP INDEX uk_NbrControl_tenant_domain_nbrtype; -- ALTER TABLE NbrControl ADD UNIQUE INDEX uk_NbrControl_domain_nbrtype (domain_code, NbrType); -- 注意:回滚前必须确认不存在跨租户相同业务键,否则回滚 ADD 会因重复而失败(这正是本次改造要放开的场景)。 -- ============================================================================