
做数据库的朋友,大概率都遇到过查询结果逻辑自洽、但数学上完全说不通的诡异场景。最近就有粉丝遇到一个典型坑:
明明IN+NOT IN+NULL的计数加起来是1748976,全表count(*)却只有 1748973,SQL 语法没毛病、id无NULL值、索引正常,可数字就是对不上。
直到跑了DBCC CHECKDB,真相才浮出水面:不是SQL写错了,是数据库底层数据页损坏了!
一、现场还原:一道离谱的 “数据库数学题”
先看这套让 DBA 怀疑人生的查询结果:
-- 1. 不在这3个ID里:1748973 条
SELECT count(*) FROM 表 WHERE id NOT IN (1820872,1820873,1820874)
-- 2. 在这3个ID里:3 条
SELECT count(*) FROM 表 WHERE id IN (...)
-- 3. ID 无 NULL:0 条
SELECT count(*) FROM 表 WHERE id IS NULL
-- 4. 全表 count:1748973 条
SELECT count(*) FROM 表理想总和:3 + 1748973 + 0 = 1748976
实际总数:1748973
3+1748973=1748973?
数学老师看了都沉默。更诡异的是单独查那3个ID能正常返回,NOT IN也正常,唯独全表count对不上。
二、排查踩坑:90% DBA first 反应都错了
遇到这种问题,大多数人第一反应是:
是不是 NOT IN 遇到 NULL 翻车? 是不是统计信息过期? 是不是查询计划走错了?
结果逐一验证:
✅ id 无 NULL
✅ 重复执行结果一致
✅ 索引查询正常
✅ 无锁、无阻塞
直到执行:
DBCC CHECKDB('库名') WITH ALL_ERRORMSGS一连串页损坏错误直接炸出来:
消息 8976,级别 16,状态 1,第 11 行
表错误: 对象 ID 974626515,索引 ID 1,分区 ID 72057594054443008,分配单元 ID 72057594065321984 (类型为 In-row data)。在扫描过程中未发现页 (1:3106327),但该页的父级 (1:3079660) 和上一页 (1:3106325) 都引用了它。请检查以前的错误消息。
消息 2533,级别 16,状态 1,第 11 行
表错误: 看不到分配给对象 ID 974626515,索引 ID 1,分区 ID 72057594054443008,分配单元 ID 72057594065321984 (类型为 In-row data)的页 (1:3106336)。该页可能无效,或者页头中可能包含错误的分配单元 ID。
消息 8913,级别 16,状态 3,第 11 行
区 (1:3106336) 已分配给 'dbo.TEST表' 和至少一个其他对象。
消息 2533,级别 16,状态 1,第 11 行
表错误: 看不到分配给对象 ID 974626515,索引 ID 1,分区 ID 72057594054443008,分配单元 ID 72057594065321984 (类型为 In-row data)的页 (1:3106337)。该页可能无效,或者页头中可能包含错误的分配单元 ID。
消息 2533,级别 16,状态 1,第 11 行
表错误: 看不到分配给对象 ID 974626515,索引 ID 1,分区 ID 72057594054443008,分配单元 ID 72057594065321984 (类型为 In-row data)的页 (1:3106338)。该页可能无效,或者页头中可能包含错误的分配单元 ID。
消息 2533,级别 16,状态 1,第 11 行
表错误: 看不到分配给对象 ID 974626515,索引 ID 1,分区 ID 72057594054443008,分配单元 ID 72057594065321984 (类型为 In-row data)的页 (1:3106339)。该页可能无效,或者页头中可能包含错误的分配单元 ID。
消息 2533,级别 16,状态 1,第 11 行
表错误: 看不到分配给对象 ID 974626515,索引 ID 1,分区 ID 72057594054443008,分配单元 ID 72057594065321984 (类型为 In-row data)的页 (1:3106340)。该页可能无效,或者页头中可能包含错误的分配单元 ID。
消息 2533,级别 16,状态 1,第 11 行
表错误: 看不到分配给对象 ID 974626515,索引 ID 1,分区 ID 72057594054443008,分配单元 ID 72057594065321984 (类型为 In-row data)的页 (1:3106341)。该页可能无效,或者页头中可能包含错误的分配单元 ID。
消息 2533,级别 16,状态 1,第 11 行
表错误: 看不到分配给对象 ID 974626515,索引 ID 1,分区 ID 72057594054443008,分配单元 ID 72057594065321984 (类型为 In-row data)的页 (1:3106342)。该页可能无效,或者页头中可能包含错误的分配单元 ID。
消息 2533,级别 16,状态 1,第 11 行
表错误: 看不到分配给对象 ID 974626515,索引 ID 1,分区 ID 72057594054443008,分配单元 ID 72057594065321984 (类型为 In-row data)的页 (1:3106343)。该页可能无效,或者页头中可能包含错误的分配单元 ID。
对象 'TEST表' 的 740652 页中有 1748972 行。问题根源:表物理存储已经损坏,只是查询没触发而已。
查看数据库日志,可以看到较多的如下信息:
消息
Attempt to fetch logical page (1:3106344) in database 7 failed. It belongs to allocation unit 72057594092388352 not to 72057594087276544.
其中database 7对于的便是表所在的库。
三、底层解密:为什么页损坏会让 count “算错数”?
这是最关键、最容易被忽略的知识点:
SQL Server 里,不同查询走的 “数据源” 不一样!
1. 你的表 = 目录(索引页)+ 正文(数据页)
索引页(聚集索引):只存 id + 指针,相当于书的目录
数据页:存整行数据,相当于书的正文
2. 两类查询,两条完全不同的路
只需要读 “目录”,不用翻 “正文”。索引页完好无损,所以结果正确。
默认走数据页全扫描,要逐页遍历真实数据。一旦遇到损坏页,SQL Server 会直接跳过损坏页继续统计,不会报错,也不会计入总数。
3. 最终真相
索引页记录:真实总行数 = 1748976
数据页可访问:有效行数 = 1748973
差值 3 行:正好在损坏的数据页里,读不出来,也统计不到
这就是为什么:目录数是对的,正文数少了,数学上永远不成立。
四、这 3 个页损坏错误,DBA 一定要认出来
错误码 | 含义 | 对查询的影响 |
|---|---|---|
8976 | 页链断裂,索引引用的页找不到 | 遍历中断,漏统计 |
2533 | 数据页无效 / 页头错误 | 整页数据不可读 |
8913 | 区分配冲突,一页分给多个表 | 数据混乱、重复、报错 |
只要出现这类错误,查询结果再正常都是假象。
而且我们在执行已损坏的表的查询的时候会出现如下报错:
消息605,级别21,状态3.第5行
尝试在数据库7中提取逻辑页 (1:3106337)失败。该逻辑页属于分配单元 72057594087276544,而非72057594065321984。
五、紧急修复方案(按优先级,无数据丢失优先)
1. 最优方案:从完好备份恢复(零数据丢失)
有备份永远是第一选择,直接回退到健康状态。
2. 次优方案:REPAIR_REBUILD 重建索引结构
ALTER DATABASE 库名 SET SINGLE_USER;
DBCC CHECKDB(库名, REPAIR_REBUILD);
ALTER DATABASE 库名 SET MULTI_USER;只修复结构,不删数据。
3. 兜底方案:导出可用数据到新表
如果修复失败,把能读的数据全部导出到新表,替换损坏表。
4. 最后手段:REPAIR_ALLOW_DATA_LOSS
会删除损坏页数据,不到万不得已不要用。
5. 后续操作
最后,无论用什么方式修复后,必须要做如下检查及监控
六、总结
count (*) 对不上,不一定是SQL写错了,也可能是页损坏;索引页完好 ≠ 数据页完好,查询路径不同,结果也会天差地别。下次再遇到逻辑自洽、数学不对的查询,别再死磕 SQL 语法了,多方面进行检查,有时候答案往往就在底层。
💡 如果你也遇到过数据库或系统相关的诡异故障,欢迎留言交流!
关注「数据库干货铺」,每天一条实战级 DBA 避坑指南。