一個字節造成的巨大性能差異——SQL Server存儲結構

今天同事問了我一個SQL的問題,關於SQL Server內部存儲結構的,我覺得挺有意思,所以寫下這篇博客討論並歸納了一下。問題是這樣的:

首先我們創建兩張表,一張表的列長度是4039字節,另一張表的長度是4040字節,他們就只有一個字節的差距,比如以下創建表的SQL:


<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->CREATE TABLE tb4039(c1INT IDENTITY,c2 char(4035)not null)
CREATE TABLE tb4040(c1 INT IDENTITY,c2char(4036)not null)

由於INT類型是4個字節,所以我們創建的tb4039表有4+4035=4039個字節的長度,tb4040中的c2字段比tb4039中的c2字段多了一個字節,總長度是4040字節,其他沒有區別了。接下來是向這兩個表中插入數據,比如插入100條數據,SQL語句是:


<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->DECLARE @i INT
SET @i=1
WHILE @i<=100
BEGIN
INSERT INTO tb4039 (c2) VALUES('test'+CONVERT(VARCHAR(5),@i));
INSERT INTO tb4040 (c2) VALUES('test'+CONVERT(VARCHAR(5),@i));
SET @i=@i+1
END

好,現在我們使用SSMS來查看一下這兩個表的空間佔用量,如果是SQL2005,那麼可以使用SSMS自帶的報表查看,如果是SQL2008,那麼直接使用對象資源管理器詳細信息界面進行查看(如果使用的是SQL2008而不知道怎麼查看錶空間使用量那麼請查看我以前寫的一篇博客:SQL Server 2008新特性之SSMS增強)。我這裏使用的是SQL2008,查看到的情況如圖:


當然,我們也可以使用T-SQL來查詢系統視圖,得出這兩個表的數據佔用的空間,查詢代碼爲:


<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->SELECT OBJECT_NAME(i.object_id)AS TableName,data_pages*8AS DataSize--這裏返回的是數據頁個數,1頁是8K,所以乘以8
FROM sys.indexesas i
JOIN sys.partitionsas p ON p.object_id= i.object_idand p.index_id= i.index_id
JOIN sys.allocation_unitsas a ON a.container_id = p.partition_id
where i.object_id=OBJECT_ID('tb4039')OR i.object_id=OBJECT_ID('tb4040')

系統返回結果:

TableName DataSize
tb4039 400
tb4040 800

和我們通過報表或者SSMS查看到的結果相同,兩個表只相差了一個字節,可是一個佔用了400K的存儲空間,另一個卻佔用了800K的存儲空間,是另一個表的雙倍!!!

一個字節的差距就造成了存儲空間成倍的增加,爲什麼會這樣呢?這就要從SQL Server存儲結構講起。

------------------------------------------------華麗的分割線,進入主題-----------------------------------------------------------------

SQL Server最小的存儲單位是頁(Page),一個頁的大小是8K=8192字節。一個數據頁是由3部分組成:頁頭、數據行和行偏移矩陣,具體結構如圖:


頁頭保存了頁的編號、上一頁ID、下一頁ID、可以字節數等等關於該頁的基本信息。頁頭的大小是固定的96個字節,所以剩下8192-96=8096個字節用於存儲數據行和行偏移矩陣。

行偏移矩陣在頁的最後面,而且是倒序排列的,使用2個字節來表示數據行在頁面內部的偏移量,有1行數據則行偏移矩陣的大小是2字節,有2行數據則行偏移矩陣的大小是4字節,以此類推。

除了頁頭佔用的空間和行偏移矩陣佔用的空間,中間剩下的空間就是給數據行使用的。假設我們要在一個頁中保存2行數據,那麼這2行數據可以使用8096-4=8092個字節的空間,也就是說1行數據可以使用8092/2=4046個字節的空間。這裏的4046個字節並不是完全都用來保存數據行,一個數據行中還存在其他的信息用於表示該行數據,具體的結構是這樣的:

狀態位A 狀態位B 定長數據類型的長度 定長數據的內容 列數 NULL位圖 變長列的個數 變長列的偏移矩陣 變長列的數據
1字節 1字節 2字節 具體定長數據字節 2字節
列數/8個字節 2字節 變長列個數*2個字節 具體變長數據字節

不管我們對錶的定義是多麼的簡單,一行數據除了數據自身佔用的空間外,至少還要佔用1+1+2+2+1=7個字節。如果定義的數據列很多,或者裏面有變長數據列,那麼佔用的空間可能會更多。

現在回到我們前面講到的2個表tb4039和tb4040,要存儲tb4039中的一行數據需要1+1+2+4039+2+1=4046字節,所以正好可以在一個頁中保存2行數據。所以插入了100行數據,實際上是保存在50個數據頁中,大小就是8K*50=400K。而對於tb4040表,要存儲一行數據需要4047個字節,沒法在一個頁中保存2行數據,所以一行數據就佔用一個數據頁,100行數據佔用了100個數據頁,大小就是8K*100=800K。

--------------------------------------------做了一堆加減乘除,下面總結下--------------------------------------------

這裏只是舉了一個極端的例子,所以造成了一個字節的偏差而使佔用的存儲空間翻倍,在實際應用中很少會出現這麼極端的情況,但是很有可能使一個頁存儲5條數據的因爲某個列多了1個2個字節所以只能存儲4條數據。也許大家認爲少存一條數據並沒有什麼,但是在數據量變的非常龐大以後一頁4條數據和一頁5條數據將會產生明顯的性能差異。使得一頁中存放更多的數據並不是爲了節約存儲成本,現在的硬盤已經很便宜了很多服務器都是幾百個G的硬盤,本來5G的數據現在變長了10G,相對幾百個G上T的硬盤來說又算得了什麼。

實際上我們要讓一個數據頁中存放更多行的數據主要是出於性能的考慮。SQL Server進行數據庫讀寫操作的基本單位是頁,如果一頁中存放了更多的數據,那麼對錶進行掃描和查找時進行的IO操作將減少,畢竟IO操作是非常消耗時間影響性能的。假設tb4039中有100W條數據,那麼進行全表掃描就要讀取50W個數據頁,如果讀取10W個數據頁花費1秒鐘,那麼對錶tb4039進行掃描需要花費5秒鐘時間,而如果是使用tb4040存儲這100W條數據,進行全表掃描則需要讀取100W個數據頁,總共花費10秒鐘時間。就一個字節的差別,一個是5秒另一個是10秒,對性能的影響非常明顯。


爲了提高數據庫查詢的性能,在表設計時可以遵循以下建議:

1、主鍵儘可能的短,能用tinyint的就不要用int,能用char(5)的就不要用成varchar(50)。

2、計算好表列的長度,能夠在一個頁中存放5條數據的,那就不要將字段設置的太長使得一個頁中只能存放3條或者4條數據。

3、儘量將字段設置爲不允許爲NULL,因爲NULL值在存儲和數據處理時系統需要專門的處理,降低了性能。

4、能夠用固定長度的就不要用變長字段,比如身份證號就可以使用CHAR(18),而不應該使用VARCHAR(18)。

5、不要在一個表中建立太多的列,如果一個實體的屬性太多時可以考慮進行垂直分割,將常用的字段放在一個表,不常用的字段放另外的表,這樣可以減小常用字段表中數據列佔用的空間,使得一個數據頁中存儲更多的數據行。

6、不要將大對象、長字符串和常用的字段放在同一個表中。同樣還是出於性能上的考慮,比如有個產品表,裏面有產品ID、產品名字、產品售價、產品圖片、產品描述等字段,那麼我們可以將產品ID、產品名字、產品售價這幾個常用的而且佔用空間小的列放在一個表,然後建立產品ID、產品圖片、產品描述這樣的表,通過外鍵約束的方式將大對象數據和長字符串數據放在另一個表中。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章