ThinkPHP 8 本身不提供数据一致性校验的内置功能,所谓「SQL法校验」需手动构造查询比对源与目标,如用 Db::query() 执行 COUNT 和 MD5 校验和初筛,注意 GROUP_CONCAT 长度限制、NULL 处理、时区统一、主键分片、内存规避及异构库方言适配等问题。

ThinkPHP 8 本身不提供数据一致性校验的内置功能,所谓「SQL法校验」不是框架能力,而是你用 Db 类手写 SQL 去比对源和目标——必须自己构造查询、控制分片、处理类型转换、规避浮点/时区/空值陷阱。别指望 validate 或 saveAll 能干这事。
用 Db::query() 对比两张表的 COUNT 和校验和
这是最轻量的初筛,但仅限同构库、字段类型严格一致的场景。直接执行两条 SQL,比对行数和聚合结果:
Db::query("SELECT COUNT(*) as cnt, MD5(GROUP_CONCAT(CONCAT(id,'|',status,'|',amount) ORDER BY id)) as hash FROM user WHERE created_time >= '2026-01-01'")- 注意:
GROUP_CONCAT默认长度限制为 1024,大表必须先设SET SESSION group_concat_max_len = 1000000,否则 hash 截断导致误判 - MySQL 8.0+ 的
MD5对 NULL 处理敏感,CONCAT(id, '|', status)遇到任一字段为 NULL 会整体返回 NULL,建议改用CONCAT_WS('|', IFNULL(id,''), IFNULL(status,'')) - 时间字段要统一转为 UTC 或显式指定时区,否则跨库比对因
NOW()或TIMESTAMP行为差异直接失败
字段级逐行比对必须用主键分片 + 流式拉取
千万级表不能 SELECT * FROM t1 全量加载,得靠主键区间切片,每次查一段 ID 范围,再 JOIN 或子查询比对:
- 先获取最小/最大 ID:
SELECT MIN(id), MAX(id) FROM user - 按每 5000 行切片:
SELECT * FROM user WHERE id BETWEEN ? AND ? ORDER BY id,绑定参数防注入 - 对比逻辑不能在 PHP 内存里做 array_diff —— 两万行数据就吃掉 200MB+ 内存;应拼成临时表或用
LEFT JOIN ... ON ... WHERE t2.id IS NULL让数据库算差异 - 字符串字段注意 COLLATION 差异:MySQL 的
utf8mb4_0900_as_cs和utf8mb4_general_ci对大小写/重音判断不同,比对前统一用BINARY强制二进制比较
异构库(如 MySQL → Doris)校验只能靠 db-check 这类专用工具
TP8 的 Db 类无法同时连两种数据库,更没法自动适配 Oracle 的 TO_CHAR、Doris 的 toDateTime 或 PostgreSQL 的 EXTRACT(EPOCH FROM ts)。硬写 SQL 会陷入方言泥潭:
立即学习“PHP免费学习笔记(深入)”;
- 日期字段:MySQL 用
UNIX_TIMESTAMP(created_at),Doris 必须写toUnixTime(created_at),拼错一个就全表比对失败 - JSON 字段:MySQL 的
JSON_EXTRACT(data, '$.name')和 Doris 的get_json_string(data, '$.name')返回类型可能不同(字符串 vs NULL),直接=比较永远为 false - 浮点字段:MySQL 的
DECIMAL(10,2)和 Doris 的DECIMALV3在高精度计算时存在微小舍入差,需用ABS(a - b) 替代相等判断 - 别自己造轮子——直接用开源
db-check,它已封装好六种数据库的元数据探查、流式读取、分片策略和差异定位,支持 Web 页面导出具体哪一行、哪个字段不一致
真正难的不是写 SQL,是让比对过程可中断、可复核、可定位到原始业务记录。哪怕只差一条数据,你也得知道是导入时丢的、同步中间件漏的,还是字段映射配置写错了。这些信息 SQL 本身给不了,得靠外部工具补全上下文。



















