SQL SERVER大話存儲結構(3)_數據行的行結構

 
 
    一行數據是如何來存儲的呢?
    變長列與定長列,NULL與NOT NULL,實際是如何整理存放到 8k的數據頁上呢?
    對錶格進行增減列,修改長度,添加默認值等DDL SQL,對行存儲結構又會有怎麼樣的影響呢?
    什麼是大對象,什麼是行溢出,存儲引擎是如何處理它們呢?
   
 


 
    若是轉載,請註明博文來源:  www.cnblogs.com/xinysu/   ,版權歸 博客園 蘇家小蘿蔔 全部。望各位支持!
  


 

 1 引入  

    在一個DB內,每個table都能在sys.sysobjects中找到對應的描述,每個列,都能從sys.columns中找到說明。
    這裏發個SQL是平常管理中使用到的,用於描述一個表格的數據結構狀況。
 1 SELECT
 2 
 3       表名 = CASE WHEN A.COLORDER=1 THEN D.NAME ELSE '' END,
 4       表說明 = CASE WHEN A.COLORDER=1 THEN ISNULL(F.VALUE,'') ELSE '' END,
 5       列序列號 = A.COLORDER,
 6       列名 = A.NAME,
 7       標識 = CASE WHEN COLUMNPROPERTY( A.ID,A.NAME,'ISIDENTITY')=1 THEN ''ELSE '' END,
 8       約束 = CASE WHEN EXISTS(
 9                                SELECT 1
10                                FROM SYSOBJECTS
11                                WHERE XTYPE='PK' AND PARENT_OBJ=A.ID AND NAME IN (
12                                                                                   SELECT
13                                                                                         NAME
14                                                                                   FROM SYSINDEXES
15                                                                                   WHERE INDID IN( SELECT INDID FROM SYSINDEXKEYS WHERE ID = A.ID AND COLID=A.COLID )
16                                                                                  )
17                              ) THEN 'PK'
18                  WHEN EXISTS (
19                                SELECT 1 FROM sys.foreign_key_columns
20                                WHERE parent_object_id=A.ID AND parent_column_id=A.COLID
21                              ) THEN 'FK'+'('+(SELECT OBJECT_NAME(referenced_object_id)+'.'+COL_NAME(referenced_object_id,referenced_column_id)+')' FROM sys.foreign_key_columns WHERE parent_object_id=A.ID AND parent_column_id=A.COLID)
22             ELSE '' END,
23       數據類型 = CASE WHEN B.NAME IN ('CHAR','NCHAR','VARCHAR','NVARCHAR') THEN B.NAME+'('+ISNULL(CAST(case when COLUMNPROPERTY(A.ID,A.NAME,'PRECISION')=-1 then null else COLUMNPROPERTY(A.ID,A.NAME,'PRECISION') end AS VARCHAR(10)),'MAX')+')'
24                       WHEN B.NAME ='DECIMAL' THEN B.NAME+'('+CAST(COLUMNPROPERTY(A.ID,A.NAME,'PRECISION') AS VARCHAR(10))+','+CAST(ISNULL(COLUMNPROPERTY(A.ID,A.NAME,'SCALE'),0) AS VARCHAR(10))+')'
25                       ELSE B.NAME END,
26       佔用字節長度 = A.LENGTH,
27       --長度 = COLUMNPROPERTY(A.ID,A.NAME,'PRECISION'),
28       --小數位數 = ISNULL(COLUMNPROPERTY(A.ID,A.NAME,'SCALE'),0),
29       容許空 = CASE WHEN A.ISNULLABLE=1 THEN ''ELSE '' END,
30       默認值 = case when E.TEXT is not null then
31 
32                                                    case when substring(e.text,1,2)='((' then substring(e.text,3,len(e.text)-4)
33                                                                                        when substring(e.text,1,1)='(' then substring(e.text,2,len(e.text)-2)
34                                                                                   else e.text end
35                                   else '' end ,
36       列說明 = ISNULL(G.[VALUE],'')
37 FROM SYSCOLUMNS A LEFT JOIN SYSTYPES B ON A.XUSERTYPE=B.XUSERTYPE
38       INNER JOIN SYSOBJECTS D ON A.ID=D.ID AND D.XTYPE='U' AND D.NAME<>'DTPROPERTIES'
39       LEFT JOIN SYSCOMMENTS E ON A.CDEFAULT=E.ID
40       LEFT JOIN sys.extended_properties G ON A.ID=G.major_id AND A.COLID=G.minor_id
41       LEFT JOIN sys.extended_properties F ON D.ID=F.major_id AND F.minor_id=0
42 WHERE D.NAME IN ('area','','')
43 ORDER BY A.ID,A.COLORDER
查詢表結構SQL

2 數據行

2.1 數據行結構

    數據行在數據頁面的存儲結構詳見下表,分爲幾個部分:基礎信息4字節、定長列相關、變長列相關及null位圖。詳見下表。這部分的內容具體參考《SQL server技術內幕:存儲引擎》第6章。
   參考下圖,一行數據的大小是這麼計算的:Row_Size=Fixed_Data_Size+Variable_Data_Size+Null_Bitmap+4 。
 
    
     各個部分其實都比較好理解,狀態B位未使用,狀態A位,詳細描述以下。
  • 狀態位A:表示行屬性的位圖,1字節,8bit
    • Bit 0 位,版本信息
    • Bits 1-3 位,行記錄類型
      • 0,primary record,主記錄
      • 1,forwarded record
      • 2,forwarding stub
      • 3,index record,索引記錄
      • 4,blob或者行溢出數據
      • 5,ghost索引記錄
      • 6,ghost數據記錄
    • Bit 4 位,NULL位圖
    • Bit 5 位,表示行中有變長列
    • Bit 6 位,保留
    • Bit 7 位,ghost record(幽靈記錄)
  • 列偏移矩陣
    • 若是一個表格,沒有變長列,那麼這個表格則不須要列偏移矩陣
    • 一個變長列,有一個列偏移矩陣,一個列偏移矩陣2個字節,用於表示變長列中每一個列的結束位置。

2.2 特殊狀況(大對象、行溢出及forword)

2.2.1 大對象

     text, ntext, image, nvarchar(max), varchar(max), varbinary(max), and xml這種數據列,稱爲大對象列, 注意,變長數據類型nvarchar,varchar,varbinary只有當存儲內容大於8k才變爲大對象列。
   行不能跨頁,可是行的部分能夠移出行所在的頁,所以行實際可能很是大。頁的單個行中的最大數據量和開銷是 8,060 字節 (8 KB)。考慮大對象列極爲佔用空間,因此在一行數據的主記錄中,是不存儲大對象列的,僅存儲 16字節 指向 大對象列實際存儲到LOB data頁面的位置。
    好比,一個大對象列text,text列存儲5000的字符,其餘列佔用50個字符,若是是放在一塊兒存儲的話,10行數據就須要10個page,掃描就須要10次IO;而若是不放在一次,一個IN-ROW-DATA page就能存儲這10行數據,text列單獨存放在 LOB data列,那麼,掃描這10行的主記錄,僅須要1次IO。因此,大對象列是不跟主記錄存儲在一塊兒。
 
    這樣,一個8k的數據頁,就能儘量多的存儲主記錄,能夠在查詢的時候,避免 大對象列佔用主記錄空間,致使IO次數增增長。

2.2.2 行溢出

    超過 8,060 字節的行大小限制可能會影響性能,由於 SQL Server 仍保持每頁 8 KB 的限制。當合並 varchar、nvarchar、varbinary、sql_variant 或 CLR 用戶定義類型的列超過此限制時,SQL Server 數據庫引擎 將把最大寬度的記錄列移動到 ROW_OVERFLOW_DATA 分配單元的另外一頁上,而後在主記錄記錄一個24字節的指針,用與描述 被移出的列 實際存儲位置。好比,一行數據總大小超過8k,那麼在insert的過程當中,會把最大寬度的記錄移動到另外的數據頁面。
 
    若是更新操做使記錄變長,大型記錄將被動態移動到另外一頁。若是更新操做使記錄變短,記錄可能會移回 IN_ROW_DATA 分配單元中的原始頁。此外,執行查詢和其餘選擇操做(例如,對包含行溢出數據的大型記錄進行排序或合併)將延長處理時間,由於這些記錄將同步處理,而不是異步處理。
    一行數據(不包括大對象列)總長度超過了8k,則會把最大寬度的列內容移動到ROW_OVERFLOW_DATA頁面上,主記錄上留下一個24字節的指針 描述 被溢出挪走的列內容 實際存儲位置,這個稱爲行溢出。

2.2.3 forword

    在一堆表內的一個數據頁面,存儲了N行數據,如今,其中一行數據的某一列發生修改,致使其列的長度加大,而剩餘的頁面空間沒法存儲該列數據,那麼這個時候,就會把該列數據移動到新的 IN_ROW_DATA 頁面上,在主記錄留下一個 9個字節的 指針,指向實際列的存儲位置,這個稱之爲 forword。
    forward的條件是:堆表、變長列、更新操做及其數據頁面剩餘空間不足存儲新列內容。
    爲何必定要是堆表呢?由於若是是彙集索引表格,遇到這種狀況,數據頁會split,把一半的內容另外存儲到新的數據頁,因爲彙集索引上的非彙集索引鍵值查詢根據是主鍵,因此split操做不會影響到非彙集索引,可是堆表的非彙集索引結構查找行是根據RID,若是也split,那麼全部非彙集索引都須要修改鍵值RID,故在堆表上,使用了forword。
    爲何是更新操做呢?由於若是是INSERT操做,一開始就出現空間不足的狀況,它老早就跑路到新的數據頁上了,不會再空間不足的數據頁面坐INSERT操做。
     好比,一行數據本來存儲在一個數據頁面中,可是update某一列,增大其存儲內容,發現該數據頁沒有空閒的空間能夠存儲該列內容,該列則會forword到另外的數據頁IN_ROW_DATA存儲,主記錄留下一個9字節的指針。

3 測試存儲狀況

   測試思路
  1. 先創建一個只有2列非空定長列的堆表,而後INSERT一行數據,檢查page頁面存儲內容
  2. 添加主鍵,檢查存儲頁面內容
  3. 增長一列:可空變長列
  4. 增長一列:非空變長列+默認值(分大對象和非大對象)
  5. 刪除無數據的列
  6. 刪除有數據的列
  7. 行溢出
  8. forword

3.1 堆表分析

 
create table tbrow(id int not null identity(1,1),name char(20) not null)
 
insert into tbrow(name) select 'xinysu';
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1) 
--根據返回結果,判斷324爲數據頁,若是不理解,請查看本系列第一篇博文
 
dbcc page('dbpage',1,324,3)
 

    查看 `消息` 內容,能夠看到 slot 0 存儲的行數據大小爲21字節,因爲如今的 tbrow表格中,只有兩列 int 跟 char ,因爲都是定長列,全部變長列的存儲模塊均爲空,可是注意一點,即便整個表格都沒有容許Null的列,Null位圖仍然會佔用一個字節。
    因此 該行記錄的長度=狀態A+狀態B+定長字段長度+定長字段內容+總烈屬+null位圖=1+1+2+(4+10)+2+1= 21 bytes。
    根據行的16進制記錄:10001200 01000000 78696e79 73752020 2020020000,來詳細分析這行數據的存儲狀況。先把這串字符按照字節數區分,其中注意部分須要反序後再轉換十進制。詳細分析及推導見下圖。

3.2 添加主鍵

alter table tbrow add constraint pk_tbrow primary key(id)
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1)
    能夠看到,表格的IAM頁及數據頁所有都改變了,由於當一個堆表添加主鍵變爲彙集索引表格的時候,須要從新組織數據頁,按照彙集索引的鍵值順序存儲,因此看到,整個數據頁存儲狀況發生了變化。若是是一個大堆表添加彙集索引,那麼這是一個很是耗時及耗費IO、CPU的操做,而且會鎖表直到操做結束,需謹慎操做。
    再次來分析如今的行記錄。
 
dbcc page('dbpage',1,311,3)
    能夠看到,數據行的內容並無發生變化,添加主鍵(彙集惟一索引),會重組整個表格的存儲順序,可是不會影響到行內的數據狀況。

3.3 增長一列:可空變長列

alter table tbrow add constraint pk_tbrow primary key(id)
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1)
 
dbcc page('dbpage',1,311,3)

    這裏開始有趣了,發現,添加了一列可空可null的列後,行記錄16進制並無發生變化。對好比下。
 
/*
第一個行爲堆錶行記錄
第二個行爲添加主鍵後的行記錄
第三個行爲添加可空變長列後的行記錄
 
10001200 01000000 78696e79 73752020 2020020000
10001200 01000000 78696e79 73752020 2020020000
10001200 01000000 78696e79 73752020 2020020000
*/
   
    即便表格有爲null的列,有變長的列,可是,只有這些列上沒有值,是不會影響這一行的數據記錄的,這很是重要!由於意味着,給一個表格添加可爲空的列時,存儲引擎不須要去修改表格內的行記錄存儲狀況,只須要在數據字典上添加作變更便可,這須要獲取到表格的架構鎖,而後執行,這個執行速度很是快。
 
    這一點的處理,跟MySQL的處理極爲不同,雖然5.6添加了OnLine DDL,避免了DDL期間對錶格鎖表影響,可是處理添加列的時候,涉及表結構變更,須要新建臨時文件來存儲frm跟ibd文件,這是一個耗費IO的處理方式,詳細可查看以前博文: MySQL Online DDL的改進與應用 。

3.4 增長一列:非空變長列+默認值

3.4.1 非大對象列

alter table tbrow add task varchar(20) not null default 'all A' ;
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1)
 
dbcc page('dbpage',1,311,3)
 
    查看16進制的行記錄:10001200 01000000 78696e79 73752020 2020020000,發現與以前的是同樣的,查看錶格內容,設置了NOT NULL帶默認值的列後,實際上,查詢出來 task列是有值存儲的,存儲內容爲 'all A',可是查看16進制內容的時候,卻發現,這個數據頁內的行記錄存儲內容並無發生變化。
    這是一個神奇的處理方式!爲啥呢?
    仔細查看page的解析內容,發現 :Slot 0 Column 4 Offset 0x0 Length 5 Length (physical) 0 。該列數據長度爲5,可是,實際存儲長度爲0,也就是這一列壓根沒有存儲在數據頁面中。
     我的推測:當添加了NOT NULL列+默認值(非大對象列)的狀況下,不對以往數據存儲記錄發生修改,可是在查詢的時候,會判斷該列是否有存儲數據,若是沒有則使用默認值顯示。 這樣有一個很是大的好處:節約存儲空間,不變動行記錄,DDL期間,無需對以往記錄作處理,僅需修改數據字典便可。
 
3.4.2 大對象列     
 
alter table tbrow add descriptions text not null default 'i love sql server' ;
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1)
 
    單薄的表格,一行的記錄,由於添加了大對象列,來了個 LOB data的IAM頁 以及 LOB data的數據頁 。不過,此次僅分析主記錄數據頁面pageid=311。
 
--主記錄數據頁面pageid=311
dbcc page('dbpage',1,311,3)
 
    依舊來分析下這行存儲記錄,原先長度都是21,爲啥添加了一個 text帶默認值的列,長度就增長爲50bytes呢?
 
    這裏注意兩個地方:原先的 task列跟 description列。task列以前是實際不存儲數據內容的,可是如今存儲了數據內容,description大對象列並無存儲數據在主記錄中,而是存儲在另外的lob data數據頁中,在主記錄僅存儲 描述 該列具體位置內容,佔16bytes。
 
    因此 該行記錄的長度=狀態A+狀態B+定長字段長度+定長字段內容+總列數+null位圖+變長列數量+列偏移矩陣+變長數據內容=1+1+2+(4+10)+2+1+2+2*3+(5+16)= 50 bytes。
 
    來看看這個16進制的字符串:30001200 01000000 78696e79 73752020 20200500 0403001d 00220032 80616c6c 20410000 d1070000 00004b01 00000100 0000,詳細分析這行數據的存儲狀況。先把這串字符按照字節數區分,詳細分析及推導見下圖。

 

    由此能夠獲得幾個推論:大對象的列NOT NULL+默認值,是在數據頁上實際存儲默認值的,並且會對錶格中的其餘本來不存儲默認值的列形成影響,整個表格變成了把默認值實際存儲到數據頁面中去。當一個大表,須要增長一列大對象列NOT NULL+默認值時,會影響到表格裏面的每一行記錄,每行記錄都要增長一個16字節的來描述 大對象列的存儲位置,同時,本來不存儲默認值的列,也會實際存儲默認值到數據頁面中,這是一個鎖表久耗費IO的操做,對於一個大表來講。
    是否是發現本身 添加一個大對象列+默認值是一件可怕的事情?若是真有這種需求,並且仍是個大表,請謹慎考慮。

3.5 刪除無數據的列 

--根據以前的查詢結果,skill這一列是沒有存儲數據的
alter table tbrow drop column skill
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1)
 
dbcc page('dbpage',1,311,3)
    能夠發現,刪除這一列,對實際數據存儲並無影響,可是該列會有一個標識值 DROPPED=[NULL]代表該列已被刪除,注意,這個表示只並非存儲在每一行數據中,而是數據庫存儲引擎記錄。
    截取數據頁面裏邊的16進制內容:30 00 1200 01000000 78696e79737520202020 0500 04 0300 1d0022003280 616c6c2041 0000d107000000004b01000001000000,發現與刪除前的是同樣的,對好比下:
 
/*
 
第一個行記錄爲刪除前
第二個行記錄爲刪除後
 
30 00 1200 01000000 78696e79737520202020 0500 04 0300 1d0022003280 616c6c2041 0000d107000000004b01000001000000
30 00 1200 01000000 78696e79737520202020 0500 04 0300 1d0022003280 616c6c2041 0000d107000000004b01000001000000
 
*/
 
    得出結論:刪除一行無數據的列時,不須要修改行內數據存儲狀況,僅須要修改涉及的數據字典跟刪除期間持有架構鎖,這是一個很是快的過程(可是若是表格一直被其餘用戶進行操做,那麼申請架構鎖也會出現等待狀況)。

3.6 刪除有數據的列

--根據以前的查詢結果,skill這一列是沒有存儲數據的
alter table tbrow drop column name
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1)
 
dbcc page('dbpage',1,311,3)
 
    分析到這裏,能夠發現,SQL SERVER在處理刪除列這一塊處理的很是巧妙,最大程度的減小了對錶格可用性的影響,不管帶不帶數據,刪除的時候,只處理數據字典類相關內容,標識該列已被刪除,可是實際上沒有去到每個頁面中去刪除數據,而是把這些列佔用的空間在邏輯上修改成不存在,容許之後寫覆蓋。
 
    做爲一名小小的DBA,我的以爲在行數據的存儲結構這一塊,針對於增長列或者刪除列的處理,SQL SERVER 設計很是巧妙及高效!相對與 MySQL改進後的Online DDL,SQL SERVER將表格的可用性大大提升以及下降對系統資源的影響。(僅討論列的增長刪除DDL這一塊)

3.7 行溢出

    行溢出這塊,不分析其16進制行記錄,着重在 行溢出的處理方式上。
#新表格測試
create table tbflow(id int not null ,cola varchar(6000),colb varchar(6000),colc varchar(6000))
INSERT INTO tbflow SELECT 1,replicate('1',1000),replicate('1',5000),replicate('1',3000)
 
dbcc traceon(3604)
dbcc ind('dbpage','tbflow',-1)
dbcc page('dbpage',1,334,3)
 
    cola列1000個字符,colb列5000個字符,colc列3000個字符,不算其餘字節使用,光着3列長度之和就大於8k,按照行溢出的處理,能夠推測出 是colb 被移動到 Row-overflow data列,因此,先分析page 334 ,看主記錄的存儲狀況,實際狀況與推測一致。

3.8 Forword

    Forword這塊,不分析其16進制行記錄,着重在Forword的處理方式上。
 
create table tbforword(id int not null ,cola varchar(6000),colb varchar(6000),colc varchar(6000))
insert into tbforword select 1,replicate('1',1000),replicate('1',500),replicate('1',500)
insert into tbforword select 2,replicate('1',1000),replicate('1',500),replicate('1',500)
insert into tbforword select 3,replicate('1',1000),replicate('1',500),replicate('1',500)
 
dbcc traceon(3604)
dbcc ind('dbpage','tbforword',-1) #記錄 IAM是385,主記錄是384頁
 
update tbforword set colb=replicate('1',4500) where id=2
 
dbcc traceon(3604)
dbcc ind('dbpage','tbflow',-1)
    pageid=384數據頁面中,存儲3行記錄大概用了6k+的空間,這時候,把id=2的colb列修改成4.5k長度,超過了一個頁面8k的範圍,也就意味着,這個被修改的列會被forword,根據新增的數據頁386,可推測出 forword的列存儲在386中。如今分析 pageid 384來驗證推測。詳見截圖,發現與推測一致。
 
dbcc page('dbpage',1,384,3)

4 行結構與DDL

相關文章
相關標籤/搜索