#0. 所需預備知識
諸位需要對於PostgreSQL中的儲存方式有一個初步的瞭解。瞭解元組,頁等術語所描述的含義。雖然這些術語不是必須知道,但是對於這些術語的瞭解有助於對應PostgreSQL儲存方式的瞭解。
#1. PostgreSQL如何儲存資料.
PostgreSQL中資料是按照頁的形式組織,一個頁的大小通常為8K。在資料表建立的過程中建立相應的資料檔案,而這些資料檔案就是我們通常所說的表中資料所存放的位置。
正如資料庫功能所描述的一樣,通過以一種合理的方式將資料儲存至非易失裝置上,並提供某類方式來快速的查詢出所儲存的資料。這也是資料庫原理的一種樸素的解釋。當然,資料庫在實現的過程中並非如上述我們所給出的一句話可以解釋清楚。
一個可使用的資料庫系統,工程上應該包括:伺服器框架部分:該部分管理使用者請求連線,為使用者提供使用資料庫服務提供介面;儲存引擎:用來完成與檔案系統的之間的操作,將使用者所需要儲存的資料按照一定的形式組織起來,並將其儲存至非易失裝置中;查詢/執行引擎:負責將所儲存的資料以一種快速且高效的方式獲取並將其展現給使用者。其它模組:例如:後設資料管理等,在此就不在詳細討論了。
在上一段中,我們曾提及,PostgreSQL中資料檔案是以Page的方式儲存的。那麼下面我們首先來給出一個Page的Layout。
我們曾在另一篇文章中從整體架構上討論了PostgreSQL的資料儲存相關知識[…]該文章中我們討論了Page構成的詳細說明。並討論了PageHeaderData,HeapTupleHeaderData,HeapTupleData等資料結構。在此文章中,我們從理解層面討論了儲存層的相關知識,而本文中我們從程式層面來討論,如何將資料組裝成特定Page形式並討論如何在程式碼層實現儲存。
在進行後續的介紹之前,我們首先來看看除了PostgreSQL外,SQL Server以及Oracle中的一行記錄的相關格式。首先,我們來看看Sql server的Row Format:
其每個記錄行由五部分構成:(1)用來描述該行記錄的頭部資訊;(2)不變資料(固定長度的資料型別,例如:int, double等等);(3)NULL值的點陣圖資訊,由於NULL值在資料庫中屬於一個特別的資料型別,其與空有著區別,因此在儲存該NULL值的時候,為了能夠節省儲存空間我們並不是儲存一個特殊的值(因為無論我們使用何種方式,即使最小的使用1 bit來表示,當資料量巨大時,也會造成儲存空間的增長),例如:INF或是其它值來標識,而是使用一個NULL點陣圖資訊來描述該行記錄中NULL值所在的位置。同樣,在PostgreSQL中,我們也使用相同的方式來描述NULL在一條記錄中的相關資訊,在HeaptTupleHeader中的t_bits就描述了該條記錄(Tuple)中NULL值的分佈情況;(4)可變資料,由於我們在支援類似於string, varchar, varchar2, text等可變長度的資料,正是由於這些資料長度的不確定性,使得我們將這些需要對此類的資料做特殊的處理(通常是記錄該資料的真實長度)。讀者可以思考一下,為什麼SQL-Server會將可以長度的資料放在該記錄的最後一部分,該種方式下,有何有點(提示:可以從儲存引擎的特定出發,由於儲存引擎的設計到資料的儲存和讀取,且由於資料庫的特點,任何單條記錄存取空間或者效率的改進都將會極大的影響整個資料庫系統的效能)。(5)版本資訊(可選)。
相關的上述關於sql-server以及oracle的row格式的相關資料均來自於Sydney University[…]
由上述PostgreSQL,SQL-Server以及Oracle的row的資料格式可以看出,無論哪種型別的資料庫,都需要對於變長資料型別和NULL資料型別進行額外的處理,當然前提條件是資料庫系統需要支援著兩種型別的資料型別,當然我們無法想象一個不支援變長資料型別和NULL型資料型別的資料庫是如何存在與市場中。PostgreSQL為了支援NULL型別,在HeapTupleHeaderData資料結構中使用t_bits來描述該tuple中的NULL屬性的相關資訊。
我們知道,heap_form_tuple函式為,PostgreSQL中構成一個Tuple組裝函式。有該函式的如下原型:
1 |
HeapTuple heap_form_tuple(TupleDesc tupleDescriptor, Datum *values,bool *isnull) |
我們可以知道,該函式以Datum*型別的資料values為基礎並按isnull陣列中所描述的一行資料中為NULL的屬性資料。 從heap_form_tuple函式中,我們可以有一個問題:為什麼會在 heap_form_tuple 函式中,首先技術heaptupleheaderdata時候,只是偏移到t_bits,而非是使用sizeof (heaptupleheaderdata)來計算呢?
1 |
len = offsetof(HeapTupleHeaderData, t_bits); |
我們知道,對於t_bits來說其描述了NULL的bitmap關係,由於其是與列(屬性)個數有關,因此其長度是一個可變的值,而這也是為什麼t_bits在heaptupleheaderdata中的定義是一個uint8 t_bits[1]這樣一個形式;
在計算完heaptupleheaderdata的長度時候,我們便根據是否存在著null列,來計算相應的資料。
1 2 |
if (hasnull) len += BITMAPLEN(numberOfAttributes); |
以及是否存在著tuple oid資訊。
1 2 |
if (tupleDescriptor->tdhasoid) len += sizeof(Oid); |
再加上padding大小 hoff = len = MAXALIGN(len); /* align user data safely */
最後加上資料的長度:
1 2 |
data_len = heap_compute_data_size(tupleDescriptor, values, isnull); len += data_len; |
從而獲得整個tuple的大小
,在完成對Tuple所需的空間計算之後進行記憶體空間的分配:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* * Allocate and zero the space needed. Note that the tuple body and * HeapTupleData management structure are allocated in one chunk. */ tuple = (HeapTuple) palloc0(HEAPTUPLESIZE + len); //然後,設定t_len, t_self, t_tableoid等資訊,以及heaptupleheaderdata中的相關標誌資訊。 tuple->t_data = td = (HeapTupleHeader) ((char *) tuple + HEAPTUPLESIZE); tuple->t_len = len; ItemPointerSetInvalid(&(tuple->t_self)); tuple->t_tableOid = InvalidOid; HeapTupleHeaderSetDatumLength(td, len); HeapTupleHeaderSetTypeId(td, tupleDescriptor->tdtypeid); HeapTupleHeaderSetTypMod(td, tupleDescriptor->tdtypmod); HeapTupleHeaderSetNatts(td, numberOfAttributes); td->t_hoff = hoff; |
其中,hoff中包括了: 從TupleHeaderData起始位置到,t_bits的位置,然後t_bits由表的列是否為空確定大小; 然後使用者資料是從,t_hoff開始,加上b_bits的偏移,以及tableoid的偏移,開始真正儲存資料的。 由上圖可以得知。
/*
* Bit layouts for varlena headers on big-endian machines:
*
* 00xxxxxx 4-byte length word, aligned, uncompressed data (up to 1G)
* 01xxxxxx 4-byte length word, aligned, *compressed* data (up to 1G)
* 10000000 1-byte length word, unaligned, TOAST pointer
* 1xxxxxxx 1-byte length word, unaligned, uncompressed data (up to 126b)
*
* Bit layouts for varlena headers on little-endian machines:
*
* xxxxxx00 4-byte length word, aligned, uncompressed data (up to 1G)
* xxxxxx10 4-byte length word, aligned, *compressed* data (up to 1G)
* 00000001 1-byte length word, unaligned, TOAST pointer
* xxxxxxx1 1-byte length word, unaligned, uncompressed data (up to 126b)
*
* The “xxx” bits are the length field (which includes itself in all cases).
* In the big-endian case we mask to extract the length, in the little-endian
* case we shift. Note that in both cases the flag bits are in the physically
* first byte. Also, it is not possible for a 1-byte length word to be zero;
* this lets us disambiguate alignment padding bytes from the start of an
* unaligned datum. (We now *require* pad bytes to be filled with zero!)
*
* In TOAST pointers the va_tag field (see varattrib_1b_e) is used to discern
* the specific type and length of the pointer datum.
*/