關係型數據在磁盤上的存儲佈局
1.基於page的heap file
Heap file是保存page數據的一種數據結構。從功能上來說,Heap file類似於內存數據結構中的鏈表。它可以作爲通用數據項的一種無序容器。
Heap file和鏈表結構類似的地方:
–高效的增加(append)功能
–支持大規模順序掃描
–不支持隨機訪問
下面是Heap file自有的一些特性:
–數據保存在二級存儲體(disk)中:Heapfile主要被設計用來高效存儲大數據量,數據量的大小隻受存儲體容量限制;
–Heapfile可以跨越多個磁盤空間或機器:heapfile可以用大地址結構去標識多個磁盤,甚至於多個網絡;
–數據被組織成頁;
–頁可以部分爲空(並不要求每個page必須裝滿);
頁面可以被分割在某個存儲體的不同的物理區域,也可以分佈在不同的存儲體上,甚至是不同的網絡節點中。我們可以簡單假設每一個page都有一個唯一的地址標識符PageAddress,並且操作系統可以根據PageAddress爲我們定位該Page。
下面是heap file的一些主要的接口:
class HeapFile
{
PageIterator scan();
PageAddress allocate();
void deallocate(PageAddress);
Page read(PageAddress);
void write(PageAddress, Page);
PageAddress find_free(int minsize);
};
class PageIterator
{
Page first();
boolean hasNext();
Page next();
};
1.1 基於鏈表的heap file實現
一種簡單的基於鏈表的heap file實現方案的磁盤結構佈局:
但是這種方案存在這樣的問題:HeapFile::find_free(int minsize)的開銷比較大。如上圖,我們要定位存在剩餘空間的page(page3),那麼我們必須從開始遍歷很多page(page1和page2)。我們發現,這時的遍歷I/O開銷是這個數據存儲區,顯然這樣I/O開銷就太高。
1.2 基於索引的heap file實現
一個簡單的,但是比較高效的heap file的實現是:用少量的block去存放所有page的地址索引和它們剩餘的空間大小。
從page1到page2都是滿的,它們的剩餘空間大小n1和n2都爲零。我們尋找一個存在剩餘空間的page仍然需要遍歷索引,但是這時我們的開銷僅僅是一個磁盤的I/O。
如果索引page是多個的話,那麼我們可以採用鏈表的形式存儲page索引,如下圖所示
我們假設有如下參數:
Page size:16K
Page address size:32bits
那麼,每個索引項(即是索引Page的記錄項)的大小:
Bits for free space+Bits for page addresslog2(16 KB)+32=17+32 bits=49 bits
則,每個索引page能索引的數據大小爲:
16 KB/49 bits=2674 pages=42 MB
2.數據的序列化
我們稍稍從磁盤整體佈局離題討論一下如何從tuple record轉化成字節數組。
2.1定長record的序列化
對於一些關係型數據,所有的record必須具有相同的長度。例如,思考一下用下面的SQL定義的關係型schema:
CREATE TABLE Person1 (
name CHAR(100) NOT NULL,
age INTEGER NOT NULL,
birthdate DATETIME NOT NULL
)
所有的記錄都具有一樣的字節數:
100 * sizeof(CHAR) + sizeof(INTEGER) + sizeof(DATETIME)
那麼定長record可以採用如下的序列化方式:
從schema上來說,我們可以檢查每個字段的大小,從而將字節數組解碼成不同的字段。
2.2變長record的序列化
假設我們改變上面Person1表的schema定義,使name可以是不超過1024個字符。Schema如下:
CREATE TABLE Person2 (
name VARCHAR(1024) NOT NULL,
age INTEGER NOT NULL,
birthdate DATETIME
)
上面table的record是邊長的是由於:
–name字段是一個變長的字符串;
–birthdate可以爲NULL;
變長record的序列化的關鍵是字段邊界的界定。一種比較流行的方法是在record的首部保存字段邊界的offset。
Person2的record的編排方式如下:
Note:我們在首部設置4個整型去存儲三個字段的四個邊界offset。
上面的編排方式很自然的提供一種NULL字段的編排方式–可以標識該字段的值爲NULL,如下圖:
第三個offset和第四個offset指向同一個位置,那麼就表明第三個字段的大小是零,即是一個NULL值。
3 Page Format
現在我們要具體的描述一下數據在page中是如何編排的。
我們做一個簡單化的設想:每個page中的tuple record具有相同的schema。
這種簡單化的設想有兩個重要的結果:
1. 每個關係表至少佔用一個page;
2. 每個page中保存的要麼是定長的數據,要麼是變長數據;
在Page編排格式中有兩個變量:一個是爲了定長數據,一個是爲了變長數據。對於DMS(Database Management System)來說,Page Format必須具有如下的特性:
1.高效利用磁盤空間;
2.支持scan、insert、delete和modify record的功能,並且要求高效和儘量少的I/O;
3.有一個高效的爲每一個record分配唯一ID的方式。並且某個record的ID不會由於數據庫的modify、insert、delete而改變;
4.每個record的大小不能超過一個Page的有效存儲大小,這個特性在以後的章節將會被放寬。
3.1基於定長record的page format
在Page中組織編排定長record相對來說是比較容易的。常用的方法是將一個Page的初始數據段分割成大小相等的M個slot,每個slot裝載一個record。Page的最後一段存儲着一個整數M和標識對應的slot是否爲空的M bit。
定長record的Page的維護也非常簡單。
Record ID
record r的Record ID可以是這種結構<pid§, offset®>。
Record ID的一個重要的特性是它的分配是持久的——它不因數據庫的改變而受到影響。甚至,Record ID作爲數據庫定位該record在物理磁盤上位置的依據,是完全能勝任的。
創建一個新的Record
將record r插入到page P中,
1.將page P中的最後sizeof(int)個字節讀出來作爲M,
2.然後在倒着遍歷M bits,
3.如果所有M bit都是1,那就表明page P沒有多餘的空間存儲record r,
4.如果K-th bit是0(當然這個K是離結尾最近的那個爲0的),那麼,record r可以存儲在P[nk:n(k+1)]的位置,其中n=sizeof®,最後將K-th bit置爲1.
修改record
修改一個record,我們可以很簡單的用該record的新值去覆蓋舊的值。這是由於都是定長數據,它的邊界不會因爲record的值而受到影響。
刪除record
刪除一個record,就將對應的那個bit從1改變成0即可。
3.2.變長record的Page Format
在一個Page中保存變長數據以及對該數據的維護相對來說都是比較困難的,因爲:
1.record的邊界以及字段的邊界都依賴於數據內容(即是record的值);
2.Record被修改時,record的大小可能會改變,這就造成物理上對該record的重新定位。這就需要一個非常高效的在一個Page內的或是多個Page的record遷移方法;
3.如果Record ID需要持久,那麼儘管該record已經被遷移,那麼該record被分配到的Record ID也不能改變。
一個有效的設計方法是維護一個Page首部去跟蹤free空間和record的索引。
Record ID的分配
Record ID是由page ID和目錄索引組成的。
創建一個新的record
將record r插入到page P中,
1.確保page P中的剩餘空間能存放sizeof®的數據和(sizeof(offset)+sizeof(int))的目錄索引;
2.將r從剩餘空間的開始寫入;
3.修改目錄索引信息:offset和record size;
4.修改空餘空間指針的指向。
刪除record
將record i從page P中刪除:
1.如果i是最後一個record,我們只需將最後一個目錄索引刪除,在修改空餘空間的指向即可;
2.如果i不是最後一個,我們只需將該record對應的目錄索引的offset設置爲-1即可。
修改record
假設r是一箇舊的值,r’是一個新的值。i是r的索引。
如果|r’|<=|r|,那麼就將新的值寫的offset(i),修改對應的目錄索引的size:sizeof(r’)。
如果|r|<=|r’|:
–如果該record所在的page P還有空餘的空間能夠存放r’,那麼
1.將r’寫入page P;
2.修改offset(i),更新到r’存儲的位置;
3.更新空餘空間的指針。
–如果剩餘空間不足以存放r’,那麼我們就嘗試壓縮page P(由於刪除操作使得page中有很多空洞,所以可以壓縮)。壓縮後,如果存在足夠的空間存放r’,那麼就按上面的順序增加r’;如果還是沒有足夠的空間存放r’,那麼:
1.申請一個新page,將r’寫入到新page中。由於r’在不同的page中,所以它有一個不同的Record ID;
2.將r’的Record ID寫入到r中去。
4.處理比較大的record
在這個部分,我們專注討論一下一個record保存在多個page中的方法。
4.1.record跨page保存的動因
一個明顯的動因是當一個record的大小超出了page size。record域被設計用來承載一個大的對象,比如:文件,多媒體數據等等。幾乎可以肯定的是,BLOB記錄必須跨越多個page。
另一個動因是避免空間的浪費。思考一下,在上面介紹的定長record的存儲中,如果record的大小略大於1/2page size,那麼我們的一個page只能存放一個record,我們浪費幾乎浪費了一半的存儲空間。在這種情況下,我們採用record跨page存儲將獲益匪淺。
4.2.Spanned records(跨page的record)
–如果一個record存儲在多個page中,那麼我們就說它spanned。否則,unspanned。
–Spanned record分別保存在不同的record fragment中。每個片段在一個page中。
–每個record必須用1 bit去標識該record是否spanned。
–每一個record fragment都需要保存下一個record fragment的地址,使得衆多的record fragment能夠串聯起來。