sql server 索引闡述系列三 表的堆組織
一. 概述
這一節來詳細介紹堆組織,通過講解堆的結構,堆與非聚集索引的關系,堆的應用場景,堆與聚集索引的存儲空間占用,堆的頁拆分現象,最後堆的使用建議 ,這幾個維度來描述堆組織。在sqlserver裏,表有二種組織方式,在表上沒有創建聚集索引時,表就是堆組織, 有聚集索引就是B樹組織。無論哪種組織方式,都可以在表上建多個非聚集索引。表的組織方式也稱為HOBT。
之所以稱為堆,是因為它的數據不按任何順序進行組織,而是按分區組對數據進行組織。 在一個堆中。用於保存數據之間的關系的唯一結構是索引分配映射(IAM , index allocation map)的位圖頁,上一章節中有說過頁文件類型。
IAM位圖頁有指向數據頁的指針,如果一個IAM不足以覆蓋所有頁,將維護一個IAM頁的鏈,在查詢數據時,先使用IAM頁來遍歷分配單元的數據。
堆結構在數據插入沒有更改時是有存儲順序的,但一改動如修改刪除,結構就會發生變化, 因為沒有特定的順序來維護數據, 所以在新增表中的行時,可以保存到任何數據頁上。
Sql server內部使用文件頁(PFS, Page Free Space)可用空間頁,PFS位圖來跟蹤數據頁中的可用空間, 以便可以快速找到有足夠空間能容納新行的頁面,如果沒有則分配一個新數據頁面。
1.1 堆組織結構
在堆組織中對於一個select查詢,首先查詢IAM頁,然後根據IAM頁提供的信息,遍歷每個區,把區內符合條件下的數據頁返回,在堆中查詢從上到下依次是Heap-->IAM-->區-->數據頁。如下圖所示:
1.2 堆上的非聚集索引
非聚集索引也可以結構化為一顆B樹,與聚集索引類似,唯一區別就是非聚集索引的葉子層只包含索引鍵列和指向數據行的指針(行定位符)。如果是在堆上建立非聚集索引,則指針指向堆結構中的數據行
在堆中非聚集索引都有一個相對應的partition, 在這個partition下都有一個連接指向Root page根,在葉子層有會一個連接(文件號,頁號,行號)指向真正的數據,真正的數據還是以堆結構存放的。在堆上建立的非聚集索引查詢從上到下依次是Heap-->Root根-->root index中間層-->葉節點(文件號,頁號,行號)-->數據頁。如下圖所示:
二. 堆應用場景
堆最常用的現象就是使用臨時表,一般都很少會主動加clustered primary關鍵詞,很多時間臨時對象的應用也沒有必要使用聚集索引。但如果臨時表在會話裏需要使用多次條件查詢,排序 等操作,聚集索引則少一部分開銷。下面演示下:
--創建臨時表堆 CREATE TABLE #tempWithHeap([SID] INT, model VARCHAR(50)) --插入數據 INSERT INTO #tempWithHeap SELECT [sid],model FROM dbo.Product WHERE UpByMemberID=3000 --查詢 SELECT Product.* FROM Product JOIN #tempWithHeap ON #tempWithHeap.[SID] = dbo.Product.[SID]
下圖在執行計劃裏能看到臨時表是表掃描方式
--創建臨時表聚集 CREATE TABLE #tempWithCLUSTERED([SID] INT PRIMARY KEY CLUSTERED, model VARCHAR(50)) --插入 INSERT INTO #tempWithCLUSTERED SELECT [sid],model FROM dbo.Product WHERE UpByMemberID=3000 --查詢 SELECT Product.* FROM Product JOIN #tempWithCLUSTERED ON #tempWithCLUSTERED.[SID] = dbo.Product.[SID]
下圖在執行計劃裏能看到臨時表是聚集索引掃描方式
下面來演示堆和索引在排序下不同的執行計劃
--臨時表堆上排序 SELECT Product.SID FROM Product JOIN #tempWithHeap ON #tempWithHeap.SID=Product.SID ORDER BY #tempWithHeap.SID
在下圖執行計劃中排序顯示開銷15%
--臨時表聚集索引上排序 SELECT Product.SID FROM Product JOIN #tempWithCLUSTERED ON #tempWithCLUSTERED.SID=Product.SID ORDER BY #tempWithCLUSTERED.SID
在下圖執行計劃中排序開銷沒有
三.堆上的頁拆分
堆上的頁拆分叫Forwarded records,是指更新數據後,原有頁面空間大小已經無法存放該數據,sql server 會把這個數據移到堆中的新數據頁裏,並在新舊頁中分別添加一個指針,標識這個數據在新舊頁中的位置,從舊頁指向新頁的指針叫Forwarded records pointer 存放於舊頁中, 從新頁指向舊頁的指針叫作back pointer 存放於新頁中。
下面來演示下頁拆分現象
--這裏定義一個堆表,使用變長字段2500 CREATE TABLE HeapForwardedRecords ( ID INT IDENTITY(1,1), DATA VARCHAR(2500) ) --插入數據,這裏data字段插入2000,插入24條 INSERT INTO HeapForwardedRecords(data) SELECT TOP 24 REPLICATE(‘X‘,2000) FROM sys.objects --查看碎片信息 select OBJECT_NAME(object_id),object_id, index_type_desc,page_count,record_count, forwarded_record_count from sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID(‘HeapForwardedRecords‘) ,null,null,‘Detailed‘)
下圖顯示:共6頁,24條數據,頁拆分0條。 (一行數據2000字節,一頁存儲4行, 24行共6頁)
下面將data字段存儲的2000字節,修改為2500字節,每頁4行更新二行,原來一頁存儲4行(4*2000<8060),現更新後就是(2*2000 +2*2500)>8060字節,原頁就只能存儲三行,這時堆上的頁就會拆分。
--更新數據,12行受影響 UPDATE HeapForwardedRecords SET DATA=REPLICATE(‘X‘,2500) WHERE ID%2=0
再次查看碎片信息,發現原來6頁存儲變為了9頁, forwarded_record_count是指頁拆分次數(是指向另一個數據位置的指針的記錄數,在更新過程中,如果在原始位置存儲的空間不足,將會出現此狀態) 如下圖:
總結:通過sys.dm_db_index_physical_stats 我們可以查詢到碎片信息,page count的頁數越多,內存消耗就越多。 要整理碎片可以重建聚集索引。若要減少堆的區碎片,請對表創建聚集索引,然後刪除該索引。更多碎片信息查看 https://docs.microsoft.com/zh-cn/previous-versions/sql/sql-server-2008-r2/ms188917(v=sql.105)
如下圖:forwarded_record_count為0了
四.堆存儲結構對空間使用的影響
4.1 等量數據的存儲方式,使用DBCC SHOWCONFIG來查看
下面演示表結構相同情況下在堆組織和聚集索引組織二種方式, 存儲等量數據,來查看空間的占用。
--堆表 CREATE TABLE [dbo].[ProductWithDeap]( [SID] [int] IDENTITY(1,1) NOT NULL, [Model] [nvarchar](100) NULL, [Brand] [nvarchar](100) NULL, [UpdateTime] [datetime] NULL, [UpByMemberID] [int] NULL, [UpByMemberName] [nvarchar](200) NULL) ON [PRIMARY] --插入表堆數據(60703 行) INSERT INTO ProductWithDeap(Model,Brand,UpdateTime,UpByMemberID,UpByMemberName) SELECT Model,Brand,UpdateTime,UpByMemberID,UpByMemberName FROM dbo.Product WHERE UpByMemberID=3000
--聚集索引 CREATE TABLE [dbo].[ProductWithClustered]( [SID] [int] IDENTITY(1,1) NOT NULL, [Model] [nvarchar](100) NOT NULL, [Brand] [nvarchar](100) NULL, [UpdateTime] [datetime] NULL, [UpByMemberID] [int] NULL, [UpByMemberName] [nvarchar](200) NULL, CONSTRAINT [PK_ProductWithClustered] PRIMARY KEY CLUSTERED ( [SID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) --插入表聚集數據(60703 行) INSERT INTO ProductWithClustered(Model,Brand,UpdateTime,UpByMemberID,UpByMemberName) SELECT Model,Brand,UpdateTime,UpByMemberID,UpByMemberName FROM dbo.Product WHERE UpByMemberID=3000
存儲方式 | 使用頁面數量 | 使用區數量 |
堆組織 | 517 | 69 |
聚集索引 | 518 | 66 |
4.2 刪除數據後,對空間的釋放情況
delete from ProductWithDeap
delete from ProductWithclustered
存儲方式 | 剩余空間數量 | 剩余區數量 |
堆組織 | 50 | 11 |
聚集索引 | 1 | 1 |
使用delete後我們發現,建立堆組織的空間不會馬上釋放掉,聚集索引能很好的釋放空間,但也存在1頁未釋放,如果完全釋放使用truncate table。
總結:當我們考慮表是用堆組織還是用聚集索引時,通過上面的演示我們知道,聚集索引的葉子層就是數據本身,並不會因為建立聚集索引而消耗過多的空間(註意非聚集索引會占用空間,不管是建立在堆組織上還是聚集索引上),而且能夠更好的管理數據和空間的釋放。除非特殊情況(後面有選擇堆的理由)
五.堆的使用建議
5.1堆需要考慮點
過多的產生forwarded records 來維護堆表,產生額外的io操作。
5.2 堆選擇理由
高頻率的增刪操作。
鍵值經常改變,特別在索引上的位置改變。
插入大量數據列到表中。
主鍵值並不自增或者唯一。
sql server 索引闡述系列三 表的堆組織