【SqlServer】 理解數據庫中的數據頁結構


這篇文章,我將會帶你深入分析數據庫中 數據頁(Page) 的結構。通過這篇文章的學習,你將掌握以下知識點:

1. 查看一個 表/索引 占用了多少了頁。

2. 查看某一頁中存儲了什么的數據。

3. 驗證在數據庫中用 GUID類型時用 newid() 生成的數據作為聚集索引時的缺陷。

 

首先需要清楚 頁(Page) 和 盤區(Extent) 的概念。頁是SQL Server中數據存儲的基本單元,每一頁的大小都是8K。而盤區是一組頁的集合,每一個盤區都是由8個相鄰的頁組合而成的。

上面的這張圖片引用自微軟官方文檔,它展示了頁的基本結構。

 

一個盤區是8個頁的集合,所以每一個盤區的大小就是64K。1M的數據就包含16個盤區。 盤區分為兩種:

1. 統一區(Uniform):由單個對象所有。區中的所有 8 頁只能由所屬對象使用。

2. 混合區(Mixed):最多可由八個對象共享。區中八頁的每頁可由不同的對象所有。

 

除此之外,還需要了解一個概念,就是IAM頁,它的全稱是Index Allocation Map Page。IAM是對盤區(Extent)的管理,每個IAM最大為4G。當數據超過4G時,或者IAM頁中的 Extent 存儲跨文件時,就會形成IAM鏈。

 

可以通過  sys.system_internals_allocation_units  來查看 一個分配單元(allocation unit)的第一個IAM頁 地址。

IAM鏈的邏輯概念圖:

 

上面只是簡單地介紹了一下 頁,區,和分配單元 的基本概念,更多信息,請查看 Pages and Extents Architecture Guide.

 

有了上面的基本概念后,接下來進行實際案例分析。

首先創建一個測試的數據庫,並且插入一些測試數據。

CREATE DATABASE TEST
GO
USE TEST
CREATE TABLE DBO.EMPLOYEE
(
    EMPLOYEEID INT IDENTITY(1,1),
    FIRSTNAME VARCHAR(50) NOT NULL,
    LASTNAME VARCHAR(50) NOT NULL,
    DATE_HIRED DATETIME NOT NULL,
    IS_ACTIVE BIT NOT NULL DEFAULT 1,
    CONSTRAINT PK_EMPLOYEE PRIMARY KEY (EMPLOYEEID),
    CONSTRAINT UQ_EMPLOYEE_LASTNAME UNIQUE (LASTNAME, FIRSTNAME)
)

GO
INSERT INTO DBO.EMPLOYEE (FIRSTNAME,LASTNAME,DATE_HIRED)
SELECT 'George', 'Washington', '1999-03-15'
GO
INSERT INTO DBO.EMPLOYEE (FIRSTNAME,LASTNAME,DATE_HIRED)
SELECT 'Benjamin', 'Franklin', '2001-07-05'
GO
INSERT INTO DBO.EMPLOYEE (FIRSTNAME,LASTNAME,DATE_HIRED)
SELECT 'Thomas', 'Jefferson', '2002-11-10'
GO

現在,上面的表和索引已經成功創建了,並且SQL Server將這些數據以頁的形式存起來了。我們可以通過DBCC IND命令來羅列出這些信息。

DBCC IND語法:

DBCC IND
(
['database name'|database id], -- the database to use
table name, -- the table name to list results
index id, -- an index_id from sys.indexes; -1 shows all indexes and IAMs, -2 just show IAMs
)

接下來,讓我們來看看 EMPLOYEE表的頁信息:

-- List data and index pages allocated to the EMPLOYEE table
DBCC IND('Test',EMPLOYEE,-1)
GO

輸出結果:

字段解釋,PageFID:文件編號。PagePID:文件里頁的編號。IAMFID:IAM頁所在文件的編號。IAMPID:IAM頁在文件里的編號。ObjectID:對象編號,可以由OBJECT_NAME獲得其名稱。IndexID:是sys.indexes中的的index_id值,1是聚集索引,2是非聚集索引。PartitionNumber:分區數。PartitionID:分區編號。iam_chain_type:IAM鏈類型,IN_ROW DATA 表示用於存儲堆分區或索引分區,每個堆和索引的分區都有IN_ROW DATA的分配單元。Page Type: 頁類型,1是數據頁,2是索引頁,10是IAM頁。IndexLevel:表示頁所在樹中的層級,0表示葉子節點。NextPageFID:下一個文件的編號。NextPagePID:下一個頁編號。PrevPageFID:前一個文件的編號。PrevPagePID:前一個頁編號。

 

有了這些信息后,我們進行進一步的分析。上面的EMPLOYEE表,有一個聚集索引為PK_EMPLOYEE,所以它的index_id就為1,並且PageType也應該為1(因為聚集索引就是實際存儲數據的順序)。因此我們可以鎖定為上面的第2條數據,就可以得出PageFID和PagePID的值,有了這兩個值后,我們就可以深入到頁里面去觀察了。使用DBCC PAGE命令,可以清楚地觀察到頁里面到底存了什么數據。

-- TRACEON(3604) 表示將結果輸出到控制台
-- 1 是 PageFID
-- 368 是 PagePID
-- 3 表示輸出Header和Data信息
DBCC TRACEON(3604)
DBCC PAGE('Test',1,368,3) WITH TABLERESULTS
GO

輸出結果:

通過上面的結果圖可以看出,數據是按照聚集索引的順序存儲的(EMPLOYEEID)。每一條數據都對應一個slot,slot從0開始,每次增加1,slot 0, slot 1, slot 2 ...... slot n。Field和Value字段,清楚地展示了我們所存儲數據。每次的偏移(Offset)都是上次的 Offset 加上上一個字段的長度。

 

EMPLOYEE表除了聚集索引,還有一個非聚集索引(UQ_EMPLOYEE_LASTNAME)。由於非聚集索引的index_id的值為2, 並且PageType也應該為2,所以我們知道它的PagePID為1, PagePID為400,接下來看看頁里的詳細信息:

-- TRACEON(3604) 表示將結果輸出到控制台
-- 1 是 PageFID
-- 400 是 PagePID
-- 3 表示輸出Header和Data信息
DBCC TRACEON(3604)
DBCC PAGE('Test',1,400,3) WITH TABLERESULTS
GO

輸出結果:

滑倒最下面,可以看到一張更清楚的索引邏輯表。

從這個表中可以清楚地看到非聚集索引是按照邏輯存儲的。並且每條數據都有一個EMPLOYEEID,也就是主鍵。換句話說,在有聚集索引的表中,非聚集索引是通過主鍵和原始數據關聯。這一點和堆表(heap table, 沒有聚集索引的表)不一樣。

 

上面觀察了聚集索引和非聚集索引的頁信息,除了這兩個,還有一個是IAM頁的信息,這里筆者不做過多描述。有興趣的朋友,可以自己打印出來看看。打印方法和上面的一致。接下來我們再來看看堆表(heap table)中的索引頁是如何存儲的。

alter table EMPLOYEE drop constraint PK_EMPLOYEE
GO
ALTER TABLE DBO.EMPLOYEE ADD CONSTRAINT PK_EMPLOYEE PRIMARY KEY NONCLUSTERED (EMPLOYEEID)
GO
DBCC IND('Test',EMPLOYEE,-1)

DBCC PAGE('Test',1,440,3)

輸出結果:

我們可以看出堆表中的非聚集索引都有一個HEAP RID,它指向了實際的數據源。RID值的格式為 FileID:PageID:SlotID 組成,移步Heaps(Tables Without Clustered Indexes)獲取詳細信息。

 

通過上面的學習你已經知道表的數據頁的存儲結構了,現在,筆者解決一下最開始提出的問題。

1. 查看一個 表/索引 占用了多少了頁 ?

可以通過命令DBCC IND輸出所有的頁信息,然后再通過NextPagePID來得出某一個索引的全部頁鏈。

2. 查看某一頁中存儲了什么的數據 ?

可以通過命令DBCC PAGE某一個頁里存儲的數據詳情。

3. 驗證在數據庫中用 GUID類型時用 newid() 生成的數據作為聚集索引時的缺陷?

通常情況,將newid()作為聚集索引是非常不好的設計,使用如下的測試案例來評測一下將newid()作為聚集索引時的存儲缺點。

USE TEST
CREATE TABLE DBO.EMPLOYEE
(
    EMPLOYEEID [uniqueidentifier] not null default newid(),
    FIRSTNAME VARCHAR(50) NOT NULL,
    LASTNAME VARCHAR(50) NOT NULL,
    DATE_HIRED DATETIME NOT NULL,
    CONSTRAINT PK_EMPLOYEE PRIMARY KEY (EMPLOYEEID)
)
INSERT INTO DBO.EMPLOYEE (FIRSTNAME,LASTNAME,DATE_HIRED)
SELECT 'George', 'Washington', '1999-03-15'
GO
INSERT INTO DBO.EMPLOYEE (FIRSTNAME,LASTNAME,DATE_HIRED)
SELECT 'Benjamin', 'Franklin', '2001-07-05'
GO
INSERT INTO DBO.EMPLOYEE (FIRSTNAME,LASTNAME,DATE_HIRED)
SELECT 'Thomas', 'Jefferson', '2002-11-10'

然后查看一下內存頁的數據存儲情況

DBCC IND('Test',EMPLOYEE,-1)
DBCC TRACEON(3604)
DBCC PAGE('Test',1,456,3) WITH TABLERESULTS

輸出結果:

你會發現實際數據的存儲順序和插入數據的順序不一致,也就是說在SQL Server在插入新數據時,可能會移動其它的數據(因為newid()每次生成的數據都是隨機的),插入新數據時候,移動其它的數據無疑是一種額外的消耗,在大數據量的表中,缺陷尤其明顯。

怎么解決這個問題呢? 有兩個方法,第一是不用uniqueidentifier作為主鍵類型,第二種是使用這里 NEWSEQUENTIALID() 替換 NEWID() 。NEWSEQUENTIALID()每次生成的值都會比它以前生成的值大。

 

感謝讀者耐心地閱讀完本文,上面提到的 DBCC IND  和 DBCC PAGE命令,微軟官方並沒有提供相應的文檔。未來,這些命令的功能可能會改變或是移除。目前筆者的數據庫是2016的版本。本文參考了Armando Prato的Using DBCC PAGE to Examine SQL Server Table and Index Data文章,有興趣的朋友,可以移步Armando Prato的博客查看更多內容。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM