99_verify.sql
19.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
-- 合库后校验(在**目标库所在实例**上执行;要求六个源库仍同实例在侧)
--
-- 由 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;