一旦要求我帮助“修复”一个数据库。 检查期间CHECKDB给出了错误列表,其中一些被标记为“无法纠正”。 该应用程序可以运行,但是仍然有些不安。
是的,在这种情况下,正确的解决方案是从仍然没有出现错误的那一刻起进行备份,以定位损坏的数据并从干净副本中覆盖它们。 但是...经常会发现错误,但为时已晚,因此实际上没有任何可恢复的地方。 另一方面-将有一个备份,不会有这个故事。
回忆
第一步是启动DBCC CHECKDB,以了解悲剧的规模。 团队诚实地完成了所有表的工作,大部分时间没有发现任何问题。 在同一结论中,有数百份“可纠正”错误的报告。 像这样:
Index row (1:386974:44) with values (C_FK_6bb5032ec2f94557a7d4a9d39a356168 = '04DA7FC4-B8F2-4D97-B8D2-B207A918D3DF' and C_PK_dd87d9cad5504a1199d4ddaf511ea6a9 = 'F44E8C34-862D-4952-8821-D0E87143A74E') pointing to the data row identified by (C_PK_dd87d9cad5504a1199d4ddaf511ea6a9 = 'F44E8C34-862D-4952-8821-D0E87143A74E').
还有一些更严重的错误:
Table error: Object ID 1333579789, index ID 1, partition ID 72057609974841344, alloc unit ID 72057610075701248 (type In-row data). Page (1:20426) is missing a reference from previous page (1:267203). Possible chain linkage problem.
那好 概述了工作规模,让我们开始吧!
可纠正的错误
为了理解为什么某些错误可以轻松地自动修复,让我们回顾一下MS SQL中索引的排列方式。 您可以将它们分为两种类型:群集和(令人惊讶地)非群集。 (我们不会深入研究诸如列存储索引之类的特殊问题,实际情况并非如此)。 它们都是一棵平衡的树,这对于查找数据非常方便。
重要的是,群集索引在其“叶”级别直接存储表行的内容。 但是非聚集索引仅存储关键数据(如果可用,还包含“ included”字段)以及指向聚集索引行的链接。 也就是说,如果我们在非聚集索引中有问题,我们可以从聚集索引中获取并覆盖损坏的数据。 好吧,或者只是重建人迹罕至的索引-很好,知道字段的组成,并且源数据完整无缺。
这项任务完全没有创造力,因此您可以放心地将其委托给一辆毫无生气的汽车。 执行命令
DBCC CHECKDB (< >, REPAIR_REBUILD)
并研究进度报告。
源日志和“修复”后的日志均具有损坏页面的地址。 我们比较这些地址,并确保确实成功解决了所有标记为“可纠正”的错误。
数据连接障碍
现在,事情变得更严重了。 修复了非聚集索引并从信息消息中清除日志后,该报告包含三个“致命”错误的记录
当然,您可以放弃并使用DBCC CHECKDB命令(<DB名称>,REPAIR_ALLOW_DATA_LOSS)将其砍掉。 但是...我只是不想丢失数据。 我想将所有可能的一切恢复到最大。 因此,让我们更详细地了解错误报告通常会告诉我们什么。
Table error: Object ID 1333579789, index ID 1, partition ID 72057609974841344, alloc unit ID 72057610075701248 (type In-row data). Page (1:20426) is missing a reference from previous page (1:267203). Possible chain linkage problem. Table error: Object ID 1333579789, index ID 1, partition ID 72057609974841344, alloc unit ID 72057610075701248 (type In-row data). The previous link (1:267203) on page (1:267204) does not match the previous page (1:20426) that the parent (1:218898), slot 213 expects for this page. Table error: Object ID 1333579789, index ID 1, partition ID 72057609974841344, alloc unit ID 72057610075701248 (type In-row data). B-tree chain linkage mismatch. (1:20426)->next = (1:267204), but (1:267204)->Prev = (1:267203).
该报告包含一个模糊的故事,一页希望看到一个邻居,但邻居对此一无所知。 一些佩列文斯基水手Zheleznyak:上甲板,但没有甲板。
为了全面理解,需要更多细节,这些细节将必须直接转到页面的内容。 但在此之前,我们将弄清楚SQL Server索引页之间的总体关系。
显然,页面具有分层的“垂直”链接,这些链接形成了B树。 第一页包含指向较低级页面的链接,等等。 正如我已经说过的那样,这对于查找值非常方便:您要查找“ Vasya Pupkin”,并在几页后(“从B到G”→“从Ba到Bb”→“ Vasya Pupkin”)找到所需的值。
但是在某些情况下,查询需要立即选择整个行范围(“从Vasya到Grisha”)。 在这种情况下,每次您从上到下沿着树下走时,都会遇到它。 为了满足此类请求,页面存储“水平”链接:每个页面在“之前”和“之后”都知道邻居编号。 有了这些关系,索引扫描就更容易执行了。
从错误日志来看,我们在水平和垂直连接方面不匹配。 但是,为了最终让我们相信这一点,让我们看一下页面本身。
我们需要更深入!
要查看这些页面,我们将使用陈旧,当之无愧且没有记录的DBCC PAGE团队。 它包含4个参数:
根据最后一个参数,您只能看到服务标题(0)或页面的整个内容(3),或者标题和内容的某些部分(1和2)
顺便说一下,在SQL Server 2019中,最终出现了一个有文档记录的视图
sys.db_db_page_info ,该视图执行类似的任务。 不幸的是,它仅显示标头数据(类似于向下钻取0),因此仍然无法完全回答我们的任务。
因此,对于初学者来说,执行命令
DBCC TRACEON (3604, 1)
这样其余DBCC命令的输出将到达我们的控制台,而不是ErrorLog
之后,查看页面标题20426:
DBCC PAGE (11, 1, 20426, 0)

知道了 该页面希望位于页面267203和267204之间的中间。但是这些相同的页面呢?


没有睡眠,没有精神! 他们很好,没有不速之客。
好吧,让我们看一下即将发布的目录:

难题逐渐发展:
- 在“垂直”链接(索引树结构)方面,第20426页应介于267203和267204之间
- 水平连接与此矛盾,并说在267203和267204之间没有人。
现在,让我们尝试了解故障导致的数据类型。
页面中的行按索引键排序。 因此,知道了页面上的第一个和最后一个键值,您可以找到“受影响”记录的范围。
可以在索引说明中简单地查看该键。 我们将为此使用
sp_helpindex命令。 在这种情况下,键只有一列,包括。 全神贯注于她。

为了找到页面上的键,我们使用DBCC PAGE,其详细程度最多为3级。 如果滚动查看输出日志,则可以看到每一行的“原始”内容以及每个字段的细分:

顺便说一下,关键边界值(页面上的第一个值)也可以在上级索引页面的DBCC PAGE输出中看到(218898,请参见上面的屏幕截图)。 它们显示在页码之后的列中。
对页面的检查显示,页面20426恰好包含来自页面267203的行的一半。故障的性质变得很清楚。 当数据库页面已满并且没有更多空间插入新数据时,它将被分成两半,分成两个新页面。 显然,当页面267203装满时,就创建了问题页面20426,DBMS开始重建其链接:它设法将新页面写入索引结构。 但是由于某种原因,无法更新水平记录,并且新页面处于“挂起”状态。
这对我们有什么意义?
当然没有太多好处。 页面链接显然是访问数据所必需的。 在执行查询时,DBMS独立确定获取该数据的方式。 但是一般规则(可能会发生例外)是这样的:
- 使用水平记录执行“宽”选择(例如,用于报告)。 依次滚动几页,选择较大范围更容易
- “点”查询(更新特定记录)是通过“目录”搜索执行的。
原来,当特定记录发生更改时,DBMS进入“问题”页面20426。当运行报表时,它“水平”读取数据,而看不到所做的更改。 再说一遍:在实践中,算法可能更复杂,但是可能的问题类别仍然可以理解。
俄罗斯知识分子的永恒问题
老实说,我仍然不清楚这是怎么发生的。 现代DBMS实际上是相当可靠的事情。 数据库文件中的所有更改(包括水平和垂直关系的调整)都在事务中执行。 这些操作将记录在事务日志中,如果在此日志中没有确认操作成功的信息,则会回滚所有操作。 在这里,您可以看到事务已成功完成,但是数据文件路径上的某些更改已“丢失”。
我想到的唯一可能的解释是磁盘子系统高速缓存发生故障。 所有数据进入缓存,然后将数据文件和日志中的部分记录写入磁盘-然后有人拔出了开关。 结果,修改后的“水平”记录没有被记录,但是数据库对此一无所知。 (在这里,许多读者应该赶紧检查他们的工业磁盘控制器上的电池)
要做的事情更加清楚。 要恢复一个表的连接性,将其数据复制到另一个表就足够了,以便在复制时使用水平读取。 为了保真,您可以使用FORCESCAN提示明确指定正确的DBMS访问方法
select * into T_bca79e9e77c24cdc8bbb7cfd0ddc16fd_BKP from T_bca79e9e77c24cdc8bbb7cfd0ddc16fd with (FORCESCAN)
之后,您可以清除源表并在其中返回复制的数据。
但是随后在20426页中所做的更改将丢失,
因此,在复制表之前,您需要复制具有20426页中的标识符的行 。 标识符必须显式地访问页20426的记录:
select * from T_bca79e9e77c24cdc8bbb7cfd0ddc16fd where C_PK_bca79e9e77c24cdc8bbb7cfd0ddc16fd = ''
可以通过使用相同的DBCC PAGE读取页面的内容来获取标识符。 正如我已经写过的,这些页面267203中恰好有一半,即15个链接。
备份损坏的页面,重新加载表并匹配匹配的记录-在几十分钟后恢复了表。
万岁,我们赢了! 但这是真的吗?
真相 数据已恢复,CHECKDB停止倾倒错误,即使太阳从窗口望出去也是如此。 您可以放心地称赞自己,举杯当之无愧的轻骑轻便的饮料,并且……请记住,数据库中的数据不仅与页面链接相关联。 因此,是时候开始使用
DBCC CHECKCONSTRAINTS并将其直接插入已损坏的外键列表中了。 但这是一个完全不同的故事...
还有什么要阅读的话题