標籤:identity href 儲存結構 over har 版本 select create 反序
閱讀目錄(Content)
- 1 引入
- 2 資料行
- 2.1 資料行結構
- 2.2 特殊情況(大對象、行溢出及forword)
- 2.2.1 大對象
- 2.2.2 行溢出
- 2.2.3 forword
- 3 測試儲存情況
- 3.1 堆表分析
- 3.2 添加主鍵
- 3.3 增加一列:可空變長列
- 3.4 增加一列:非空變長列+預設值
- 3.5 刪除無資料的列
- 3.6 刪除有資料的列
- 3.7 行溢出
- 3.8 Forword
- 4 行結構與DDL
一行資料是如何來儲存的呢? 變長列與定長列,NULL與NOT NULL,實際是如何整理存放到 8k的資料頁上呢? 對錶格進行增減列,修改長度,添加預設值等DDL SQL,對行儲存結構又會有怎麼樣的影響呢? 什麼是大對象,什麼是行溢出,儲存引擎是如何處理它們呢? 如果轉載,請註明博文來源: www.cnblogs.com/xinysu/ ,著作權歸 部落格園 蘇家小蘿蔔 所有。望各位支援! 本系列上一篇博文連結:SQL SERVER大話儲存結構(2)_非叢集索引如何尋找到行記錄回到頂部(go to top) 1 引入 在一個DB內,每一個table都能在sys.sysobjects中找到對應的描述,每一個列,都能從sys.columns中找到說明。 這裡發個SQL是日常管理中使用到的,用於描述一個表格的資料結構情況。 查詢表結構SQL
回到頂部(go to top)
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位元組的指標。回到頂部(go to top)
3 測試儲存情況 測試思路
- 先建立一個只有2列非空定長列的堆表,然後INSERT一行資料,檢查page頁面儲存內容
- 添加主鍵,檢查儲存頁面內容
- 增加一列:可空變長列
- 增加一列:非空變長列+預設值(分大對象和非大對象)
- 刪除無資料的列
- 刪除有資料的列
- 行溢出
- 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 202002000010001200 01000000 78696e79 73752020 202002000010001200 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=311dbcc 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 skilldbcc 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 0000d107000000004b0100000100000030 00 1200 01000000 78696e79737520202020 0500 04 0300 1d0022003280 616c6c2041 0000d107000000004b01000001000000*/ 得出結論:刪除一行無資料的列時,不需要修改行內資料存放區情況,僅需要修改涉及的資料字典跟刪除期間持有架構鎖,這是一個非常快的過程(但是如果表格一直被其他使用者進行操作,那麼申請架構鎖也會出現等待情況)。
3.6 刪除有資料的列--根據之前的查詢結果,skill這一列是沒有儲存資料的alter table tbrow drop column namedbcc 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=2dbcc 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)回到頂部(go to top)
4 行結構與DDL
SQL SERVER大話儲存結構(3)