首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >诡异!SQL Server count (*) 数学不成立:3+1748973=1748973?底层页损坏真相

诡异!SQL Server count (*) 数学不成立:3+1748973=1748973?底层页损坏真相

作者头像
俊才
发布2026-03-04 15:10:16
发布2026-03-04 15:10:16
580
举报
文章被收录于专栏:数据库干货铺数据库干货铺

做数据库的朋友,大概率都遇到过查询结果逻辑自洽、但数学上完全说不通的诡异场景。最近就有粉丝遇到一个典型坑:

明明IN+NOT IN+NULL的计数加起来是1748976,全表count(*)却只有 1748973,SQL 语法没毛病、id无NULL值、索引正常,可数字就是对不上。

直到跑了DBCC CHECKDB,真相才浮出水面:不是SQL写错了,是数据库底层数据页损坏了!

一、现场还原:一道离谱的 “数据库数学题”

先看这套让 DBA 怀疑人生的查询结果:

代码语言:javascript
复制
-- 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

✅ 重复执行结果一致

✅ 索引查询正常

✅ 无锁、无阻塞

直到执行:

代码语言:javascript
复制
DBCC CHECKDB('库名') WITH ALL_ERRORMSGS

一连串页损坏错误直接炸出来:

代码语言:javascript
复制
消息 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 行。
  • 8976:页链断裂,找不到索引引用的数据页
  • 2533:数据页无效、页头信息错误
  • 8913:同一个区被分配给多个对象

问题根源表物理存储已经损坏,只是查询没触发而已

查看数据库日志,可以看到较多的如下信息:

代码语言:javascript
复制
消息
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. 两类查询,两条完全不同的路

  • 走索引页的查询(IN / NOT IN / IS NULL)

只需要读 “目录”,不用翻 “正文”。索引页完好无损,所以结果正确。

  • 全表 count (*)

默认走数据页全扫描,要逐页遍历真实数据。一旦遇到损坏页,SQL Server 会直接跳过损坏页继续统计,不会报错,也不会计入总数。

3. 最终真相

索引页记录:真实总行数 = 1748976

数据页可访问:有效行数 = 1748973

差值 3 行:正好在损坏的数据页里,读不出来,也统计不到

这就是为什么:目录数是对的,正文数少了,数学上永远不成立。

四、这 3 个页损坏错误,DBA 一定要认出来

错误码

含义

对查询的影响

8976

页链断裂,索引引用的页找不到

遍历中断,漏统计

2533

数据页无效 / 页头错误

整页数据不可读

8913

区分配冲突,一页分给多个表

数据混乱、重复、报错

只要出现这类错误,查询结果再正常都是假象。

而且我们在执行已损坏的表的查询的时候会出现如下报错:

代码语言:javascript
复制
消息605,级别21,状态3.第5行
尝试在数据库7中提取逻辑页 (1:3106337)失败。该逻辑页属于分配单元 72057594087276544,而非72057594065321984。

五、紧急修复方案(按优先级,无数据丢失优先)

1. 最优方案:从完好备份恢复(零数据丢失)

有备份永远是第一选择,直接回退到健康状态。

2. 次优方案:REPAIR_REBUILD 重建索引结构

代码语言:javascript
复制
ALTER DATABASE 库名 SET SINGLE_USER;
DBCC CHECKDB(库名, REPAIR_REBUILD);
ALTER DATABASE 库名 SET MULTI_USER;

只修复结构,不删数据。

3. 兜底方案:导出可用数据到新表

如果修复失败,把能读的数据全部导出到新表,替换损坏表。

4. 最后手段:REPAIR_ALLOW_DATA_LOSS

会删除损坏页数据,不到万不得已不要用。

5. 后续操作

最后,无论用什么方式修复后,必须要做如下检查及监控

  • 排查硬件、磁盘坏道、RAID 异常
  • 内存错误、服务器意外断电
  • 存储 I/O 异常
  • 页损坏 90% 源于硬件或异常关机
  • 配置任务检查数据库表页面健康
  • 监控数据库日志中的错误信息
  • 最重要的,做好备份及备份文件的恢复演练!

六、总结

count (*) 对不上,不一定是SQL写错了,也可能是页损坏;索引页完好 ≠ 数据页完好,查询路径不同,结果也会天差地别。下次再遇到逻辑自洽、数学不对的查询,别再死磕 SQL 语法了,多方面进行检查,有时候答案往往就在底层。

💡 如果你也遇到过数据库或系统相关的诡异故障,欢迎留言交流!

关注「数据库干货铺」,每天一条实战级 DBA 避坑指南。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-02-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据库干货铺 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档