-- 合库后校验(在**目标库所在实例**上执行;要求六个源库仍同实例在侧) -- -- 由 sql/tools/gen_migration_sql.py 从 check_subset.py 的 CHECKS 生成 —— 不要手改 -- 判定依据见 ../merge-conflict-plan.md -- -- 🚫 **只对"全量迁移"的库有意义,别在抽样镜像(SAMPLE=N)上跑。** -- §3/§4 断言的是"目标行数 == 源库行数";抽样镜像的行数本来就远小于源库, -- 跑上去会**必然全红**——而一个每次都报红的校验,和一个永远不报红的一样没用 -- (AGENTS.md §1.1 推论 5)。抽样镜像请跑 97_verify_sample.sql。 -- -- 跑法: -- mysql -h$TARGET_HOST -u$TARGET_USER -p < 99_verify.sql -- -- 期望:§1 零行、§2 两个查询零行、§3 全部 OK、§4 全部 0。 -- 任何一条不满足 → **先别删源库**(源库是回滚路径)。 -- -- ⚠️ §3/§4 会 COUNT(*) 十余张表。它们的**实测体量**(生成时现读): -- -- income_temp zt_order_n 万行以下 -- month_dchz zt_order_n 万行以下 -- park_in_out_record zt_park 千万级 -- parking_lot zt_park 万行以下 -- sys_org zt_sys 万行以下 -- t_vip_card_cust_info zt_bill 万级 -- table_15_free zt_order_n 万行以下 -- tb_b_park_income_day zt_order_n 十万级 -- td_b_settlement zt_order_n (未知/空) -- td_b_settlement_org zt_order_n 万行以下 -- td_b_settlement_record zt_order_n 万行以下 -- td_b_settlement_serial zt_order_n 万行以下 -- td_b_settlement_share_record zt_order_n 万行以下 -- td_c_coupon_person zt_order 十万级 -- td_p_parking_share_order_temp zt_order_n (未知/空) -- tp_e_employee_charge zt_park 万行以下 -- tp_p_berths zt_park 万级 -- -- 小表瞬间返回;`park_in_out_record`(约 9700 万行)那两条 COUNT(*) 要数分钟—— -- 慢是正常的,别以为卡死。真正的大表(zt_park 的 16 亿行那些)不在这里。 SET @tgt := 'parking_cloud'; SET @src_dbs := 'zt_sys,zt_park,zt_order,zt_order_n,zt_bill,zt_charge'; -- --------------------------------------------------------------------------- -- §1 覆盖率:源库里每张表都必须在目标库里出现 -- -- 注意:被排除的**副本**与保留份**同名**,所以它们在这里也会显示"已存在" -- (存在的是保留份那份)——本查询查不出"该排除的没排除", -- 那个由 §3 的行数相等来保证。本查询专抓"表整个没了"(dump 漏表)。 -- -- ⚠️ 但 §10.1 拍板**不搬**的那几张(`zt_order.td_b_pay_blncac_diff_copy`、`zt_order_n.td_b_pay_blncac_result_bak`、`zt_park.del20181116td_g_first_guide_info_config`、`zt_park.del20181116td_g_guide_park_berths`、`zt_park.parking_running_statistic2023`、`zt_park.parking_running_statistic2025`、`zt_sys.sys_op_advice_bak`) -- 是**有意**不在目标库里的——其中三张是**有数据的备份**(约 133 GB),不是空表。 -- 不在这里排掉,本查询就会永远报红—— -- 一个每次都喊红的校验会训练操作者忽略红色,比没有校验更坏(§3 的注释同此理)。 -- 期望:0 行 -- --------------------------------------------------------------------------- SELECT s.TABLE_SCHEMA AS `缺失的源库`, s.TABLE_NAME AS `目标库缺少的表`, s.TABLE_ROWS AS `源库约行数` FROM information_schema.TABLES s LEFT JOIN information_schema.TABLES t ON t.TABLE_SCHEMA = @tgt AND t.TABLE_NAME = s.TABLE_NAME WHERE FIND_IN_SET(s.TABLE_SCHEMA, @src_dbs) AND s.TABLE_TYPE = 'BASE TABLE' AND CONCAT(s.TABLE_SCHEMA, '.', s.TABLE_NAME) NOT IN ('zt_order.td_b_pay_blncac_diff_copy','zt_order_n.td_b_pay_blncac_result_bak','zt_park.del20181116td_g_first_guide_info_config','zt_park.del20181116td_g_guide_park_berths','zt_park.parking_running_statistic2023','zt_park.parking_running_statistic2025','zt_sys.sys_op_advice_bak') AND t.TABLE_NAME IS NULL ORDER BY s.TABLE_SCHEMA, s.TABLE_NAME; -- --------------------------------------------------------------------------- -- §2 字符集残留:合库后不应再有任何非 utf8mb4 的表或列 -- (改写在 10_dump_legacy.sh 的 rewrite_charset,自测见 tests/) -- 期望:两条查询各 0 行 -- --------------------------------------------------------------------------- SELECT TABLE_NAME AS `非 utf8mb4 的表`, TABLE_COLLATION AS `排序规则` FROM information_schema.TABLES WHERE TABLE_SCHEMA = @tgt AND TABLE_TYPE = 'BASE TABLE' AND (TABLE_COLLATION IS NULL OR TABLE_COLLATION NOT LIKE 'utf8mb4%') ORDER BY TABLE_NAME; SELECT TABLE_NAME AS `含非 utf8mb4 列的表`, COLUMN_NAME AS `列`, CHARACTER_SET_NAME AS `列字符集`, COLLATION_NAME AS `列排序规则` FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = @tgt AND CHARACTER_SET_NAME IS NOT NULL AND CHARACTER_SET_NAME NOT LIKE 'utf8mb4%' ORDER BY TABLE_NAME, ORDINAL_POSITION; -- --------------------------------------------------------------------------- -- §3 重名表:目标库行数必须**等于**保留侧行数 -- 多于 → 把该丢的副本也搬进来了;少于 → 保留份没搬全 -- 并集表不在此列(它们的目标行数天生大于保留侧,由 §4 负责) -- 期望:全部 OK -- --------------------------------------------------------------------------- SELECT 'income_temp' AS `表`, 'zt_order_n' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`income_temp`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_order_n`.`income_temp`) k UNION ALL SELECT 'month_dchz' AS `表`, 'zt_order_n' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`month_dchz`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_order_n`.`month_dchz`) k UNION ALL SELECT 'park_in_out_record' AS `表`, 'zt_park' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`park_in_out_record`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_park`.`park_in_out_record`) k -- 保留侧千万级(TABLE_ROWS 估算),这里两条 COUNT(*) 需数分钟 UNION ALL SELECT 'parking_lot' AS `表`, 'zt_park' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`parking_lot`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_park`.`parking_lot`) k UNION ALL SELECT 'sys_org' AS `表`, 'zt_sys' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`sys_org`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_sys`.`sys_org`) k UNION ALL SELECT 't_vip_card_cust_info' AS `表`, 'zt_bill' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`t_vip_card_cust_info`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_bill`.`t_vip_card_cust_info`) k UNION ALL SELECT 'table_15_free' AS `表`, 'zt_order_n' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`table_15_free`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_order_n`.`table_15_free`) k UNION ALL SELECT 'tb_b_park_income_day' AS `表`, 'zt_order_n' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`tb_b_park_income_day`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_order_n`.`tb_b_park_income_day`) k UNION ALL SELECT 'td_b_settlement' AS `表`, 'zt_order_n' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`td_b_settlement`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_order_n`.`td_b_settlement`) k UNION ALL SELECT 'td_b_settlement_org' AS `表`, 'zt_order_n' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`td_b_settlement_org`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_order_n`.`td_b_settlement_org`) k UNION ALL SELECT 'td_b_settlement_record' AS `表`, 'zt_order_n' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`td_b_settlement_record`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_order_n`.`td_b_settlement_record`) k UNION ALL SELECT 'td_b_settlement_serial' AS `表`, 'zt_order_n' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`td_b_settlement_serial`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_order_n`.`td_b_settlement_serial`) k UNION ALL SELECT 'td_b_settlement_share_record' AS `表`, 'zt_order_n' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`td_b_settlement_share_record`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_order_n`.`td_b_settlement_share_record`) k UNION ALL SELECT 'td_c_coupon_person' AS `表`, 'zt_order' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`td_c_coupon_person`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_order`.`td_c_coupon_person`) k UNION ALL SELECT 'td_p_parking_share_order_temp' AS `表`, 'zt_order_n' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`td_p_parking_share_order_temp`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_order_n`.`td_p_parking_share_order_temp`) k UNION ALL SELECT 'tp_e_employee_charge' AS `表`, 'zt_park' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`tp_e_employee_charge`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_park`.`tp_e_employee_charge`) k UNION ALL SELECT 'tp_p_berths' AS `表`, 'zt_park' AS `保留侧`, k.`保留侧行数`, t.`目标行数`, IF(t.`目标行数` = k.`保留侧行数`, 'OK', IF(t.`目标行数` > k.`保留侧行数`, CONCAT('🔴 多 ', t.`目标行数` - k.`保留侧行数`, ' 行——目标不该比源多,只可能是重复导入/多搬了副本'), CONCAT('⚠️ 少 ', k.`保留侧行数` - t.`目标行数`, ' 行——需解释:dump 之后的在线新增,还是真漏搬?(见 98 的水位线法)'))) AS `结论` FROM (SELECT COUNT(*) AS `目标行数` FROM `parking_cloud`.`tp_p_berths`) t JOIN (SELECT COUNT(*) AS `保留侧行数` FROM `zt_park`.`tp_p_berths`) k; -- --------------------------------------------------------------------------- -- §4 并集:丢弃侧还有多少行没进目标库 -- NOT EXISTS 的键与 20_union_lost_rows.sql 用的是同一个业务键 -- 期望:全部 0 -- --------------------------------------------------------------------------- SELECT 'td_c_cust_person' AS `表`, 'zt_order_n' AS `丢弃侧来源`, (SELECT COUNT(*) FROM `zt_order_n`.`td_c_cust_person` d WHERE NOT EXISTS (SELECT 1 FROM `parking_cloud`.`td_c_cust_person` k WHERE k.`cust_id` = d.`cust_id`)) AS `仍未进目标库的行数` UNION ALL SELECT 'td_b_wx_config' AS `表`, 'zt_order' AS `丢弃侧来源`, (SELECT COUNT(*) FROM `zt_order`.`td_b_wx_config` d WHERE NOT EXISTS (SELECT 1 FROM `parking_cloud`.`td_b_wx_config` k WHERE k.`appid` = d.`appid`)) AS `仍未进目标库的行数`; -- --------------------------------------------------------------------------- -- §5 目标库默认字符集 -- 期望:utf8mb4 / utf8mb4_general_ci -- --------------------------------------------------------------------------- SELECT DEFAULT_CHARACTER_SET_NAME AS `默认字符集`, DEFAULT_COLLATION_NAME AS `默认排序规则` FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = @tgt;