一:背景1.讲故事相信很多朋友在学习SQLSERVER的时候都听过这句话,但是大多都是靠记忆。最近在研究SQLSERVER,所以从底层存储的角度来理解一下。二:理解数据页1.数据页的组织我在上一篇文章中也说过,一个数据页的大小是8k,那么这8k是怎么组织的呢?为了更好的表达,我先画一张图,如下图。从图中可以看出,一个数据页大致分为三部分:页头相当于数据页的元数据区,标记了数据页类型和各种统计信息。这里的数据存储区存储了表的每条记录以及该记录的相关元数据。此元数据计数,例如固定长度和可变长度字段的数量、记录类型等。记录插槽列表。slot记录了行记录在这个数据页上的偏移地址。我们可以稍后验证。如果我们用C++伪代码,大概是这样的。//行记录struct_record{charmeta_s[8];int字段1;字符字段2;短场;charmeta_e[8];};classpage{private:charheader[96];//1。页眉_record记录[n];//2。行记录短槽[n];//3。Slot(指针从后向前)};2.了解一行的最大大小,相信大家从各种教材上都能知道,我们可以定义的最大行大小是8060byte,其中包含了行的7byte元数据大小,所以我们人肉的大小可以定义只能是8053byte。按照上一节的理解,这个8060byte落在数据存储区了,这里我们简单的计算一下这个page是刚满还是有保留区?然后用一个简单的公式来计算。0:103>?0n8192-0n96-0n2-0n8060Evaluateexpression:34=00000000`00000022上式为:保留大小=页大小-页头-槽-数据存储区,所以页中真的有34bytes的保留大小。下面我们简单验证一下这个推理。先自定义一行8054byte的大小,看看能不能通过?USEMyTestDBGOCREATETABLEt6(achar(8000),bCHAR(54))INSERTINTOt6VALUES(REPLICATE('a',8000),REPLICATE('b',53))从报错信息中可以清楚的看出,我的行记录总大小为8061,超过了系统规定的行大小8060。接下来我们验证数据页的最小保留大小是否为34字节?只要找到表数据页。使用MyTestDBGOCREATETABLEt6(achar(8000),bCHAR(53))INSERTINTOt6VALUES(REPLICATE('a',8000),REPLICATE('b',53))SELECT*FROMdbo.t6;DBCCTRACEON(3604)DBCCIND(MyTestDB,t6,-1)从图中可以看出行记录分配在456号数据页上,再用DBCCPAGE观察。DBCCPAGE(MyTestDB,1,456,2)输出以下内容:SQLServer分析和编译时间:CPU时间=0毫秒,运行时间=0毫秒。PAGE:(1:456)BUFFER:BUF@0x00000251CEB3F180bpage=0x00000251BCBE2000bPmmpage=0x0000000000000000bsort_r_nextbP=0x00000251CEB3F0D0bsort_r_prevbP=0x0000000000000000bhash=0x0000000000000000bpageno=(1:456)bpart=0ckptGen=0x0000000000000000bDirtyRefCount=0bstat=0x9breferences=0berrcode=0bUse1=38957bstat2=0x0blog=0x15ab215absampleCount=0bIoCount=0resPoolId=0bcputicks=0bReadMicroSec=135bDirtyContext=0x0000000000000000bDbPageBroker=0x0000000000000000bdbid=10bpru=0x00000251CA5A0040PAGEHEADER:Page@0x00000251BCBE2000m_pageId=(1:456)m_headerVersion=1m_type=1m_typeFlagBits=0x0m_level=0m_flagBits=0x8200m_objId(AllocUnitId.idObj)=193m_indexId(AllocUnitId.idInd)=256Metadata:AllocUnitId=72057594050576384Metadata:PartitionId=72057594043826176Metadata:IndexId=0Metadata:ObjectId=1349579846m_prevPage=(0:0)m_nextPage=(0:0)pminlen=8057m_slotCnt=1m_freeCnt=34m_freeData=8156m_reservedCnt=0m_lsn=(37:1704:26)m_xactReserved=0m_xdesId=(0:0)m_ghostRecCnt=0m_tornBits=1904590527DBFragStatusGAM=1AllocationIDAM=1)=已分配SGAM(1:3)=未分配PFS(1:1)=0x44已分配100_PCT_FULLDIFF(1:6)=已更改ML(1:7)=未MIN_LOGGED数据:内存转储@0x000000E456778000000000E456778000:0101000000820001000000000000791F00000000.................000000E45677803C:bfbe8571000000000000000000000...0000000q....0......000000E456778050:00000000000000000000000000000000100...0.791f...........y.000000E456778064:6161616161616161616161616161616161616161aaaaaaaaaaaaaaaaaaaa...000000E456779FCC:6262626262626262626262626202000000002121bbbbbbbbbbbbbb.....!!000000e456779fe0:21212121212121212121212121212121!!`.OFFSETTABLE:Row-Offset0(0x0)-96(0x60)刚才说了页元数据占96byte,里面包含了各种统计信息,比如m_freeCnt=34是当前的剩余空间page,这和我们刚才的计算公式是一致的,这个34byte是页尾默认的0x21填充三:总结其实从上面的分析可以得出数据页还有34byte的预留空间,可能是因为某些原因,不想再塞进去了。当然也可以用WinDbg观察源码逻辑,可以得到一个C++异常断点。0:113>sxeeh0:113>g(6aec.6a20):C++EH异常-代码e06d7363(第一次机会)在任何异常处理之前报告第一次机会异常。可以预期并处理此异常。KERNELBASE!RaiseException+0x69:00007ff8`f61b3e490f1f440000nopdwordptr[rax+rax]0:020>k#Child-SPRetAddrCallSite0000000022`cddf7b8000007ff8`dd2f6720KERNELBASE!RaiseException+0x690100000022`cddf7c6000007ff8`bab85763VCRUNTIME140!_CxxThrowException+0x90[D:\a\_work\1\s\src\vctools\crt\vcruntime\src\eh\throw.cpp@75]0200000022`cddf7cc000007ff8`bab85339sqldk!TurnUnwindAndThrowImpl+0x5820300000022`cddf81b000007ff8`bab8531bsqldk!SOS_OS::TurnUnwindAndThrow+0x90400000022`cddf81e000007ff8`bab84fcasqldk!ex_raise2+0x56e0500000022`cddf852000007ff8`5cf2c056sqldk!ex_raise+0xc30600000022`cddf85a000007ff8`5cf2e54dsqlmin!RaiseHoBtRowsizeError+0x1560700000022`cddf860000007ff8`5c33ac06sqlmin!SECreateRowset+0x4440800000022`cddfa94000007ff8`995632f8sqlmin!DDLAgent::SECreateRowsets+0x2e0...从线程栈可以看出,逻辑是在SECreateRowset()方法中抛出RaiseHoBtRowsizeError()异常,应该是Constantcmp对比,留给大家研究
