99_verify.sql 19.5 KB
-- 合库后校验(在**目标库所在实例**上执行;要求六个源库仍同实例在侧)
--
-- 由 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;