查NULL值应统一用TRIM(COALESCE(name, '')) = ''兜底,兼容NULL、空串和纯空格;COUNT(*)与COUNT(字段)差值即该字段NULL数;时间字段需用NOW()筛选异常值并双向ORDER BY验证趋势;完整性验证须覆盖NULL、外键、唯一性、时间逻辑及隐式类型转换。

查 NULL 值别只用 IS NULL,漏掉空字符串和空白符是高频翻车点
很多同学写 SELECT * FROM users WHERE name IS NULL 就以为清掉了所有“无效姓名”,结果上线后发现导出报表里一堆空格或 ''。MySQL 和 PostgreSQL 对 ''、' '、NULL 的处理逻辑不同,但业务上往往把三者都视为“缺失”。
- 统一兜底建议:用
TRIM(COALESCE(name, '')) = ''判断“逻辑空值”,兼容 NULL、空串、纯空格 - PostgreSQL 可简写为
NULLIF(TRIM(name), '') IS NULL;MySQL 8.0+ 支持TRIM,老版本得用TRIM(BOTH ' ' FROM name) - 别在 WHERE 里直接用
name NOT IN ('a', 'b')——只要列表里有NULL,整条条件就恒为UNKNOWN,结果为空
用 COUNT(*) 和 COUNT(字段) 差异暴露隐性数据断层
COUNT(*) 统计行数,COUNT(字段) 只统计该字段非 NULL 行。两者差值就是这个字段的 NULL 数量——这是最轻量、最可靠的完整性快筛方式。
- 执行
SELECT COUNT(*), COUNT(email), COUNT(phone) FROM users;,一眼看出 email 缺失率(比如 1000 行中只有 620 个 email) - 如果
COUNT(id) ,说明主键字段本身就有 NULL——这通常意味着建表时没加 <code>NOT NULL或插入时绕过了约束 - 注意:某些 ORM(如 Django 的
.count())默认走COUNT(*),但加了.values('email').count()可能触发去重逻辑,结果不等价
外键关联断裂?用 LEFT JOIN ... WHERE 右表主键 IS NULL 直接揪出孤儿记录
外键约束被禁用、或历史数据未校验时,订单表里的 user_id 可能指向一个已删除的用户。靠应用层日志很难回溯,SQL 层面必须主动探测。
- 典型写法:
SELECT o.id FROM orders o LEFT JOIN users u ON o.user_id = u.id WHERE u.id IS NULL; - 别用
NOT IN (SELECT id FROM users)——子查询结果含 NULL 时整个条件失效,返回空集 - 大表慎用:确保
orders.user_id和users.id都有索引,否则LEFT JOIN会全表扫描 - PostgreSQL 中可加
EXPLAIN ANALYZE看实际是否走了 Hash Join;MySQL 8.0+ 注意检查optimizer_switch是否启用了hash_join=on
时间字段乱序或未来时间?用 WHERE created_at > NOW() + ORDER BY 双向验证
时间字段不是主键,但业务强依赖其单调性和合理性。单纯看最大最小值容易忽略中间异常点,比如某批数据因时区配置错误写入了 2099 年。
- 先筛明显错:
SELECT * FROM logs WHERE created_at > DATE_ADD(NOW(), INTERVAL 1 DAY);(MySQL),或created_at > NOW() + INTERVAL '1 day'(PostgreSQL) - 再查趋势异常:
SELECT id, created_at FROM events ORDER BY created_at DESC LIMIT 5;看最新几条是否符合预期;反过来ORDER BY created_at ASC LIMIT 5看最早几条有没有 1970-01-01 这类占位值 - 注意:
NOW()是会话时区,而created_at字段可能是TIMESTAMP(自动转时区)或DATETIME(不转),混用会导致误报
检查完整性不是一锤子查询,而是围绕 NULL、外键、唯一性、时间逻辑这四个锚点反复交叉验证。最容易被跳过的,是确认字段类型定义和实际写入值之间的隐式转换——比如 VARCHAR(10) 存了 11 位手机号,MySQL 可能截断但不报错,数据就静默损坏了。

















