バイナリダンプで学ぶPostgreSQL

この記事はPostgreSQL Advent Calendar 2019の6日目の記事です。

「PostgreSQLってどういう風にデータを保存しているんだろう?」ってよく思いますよね。たとえばr1というテーブルを作った場合、そのファイルがどこにできるかは、pg_database.oidとpg_class.relfilenameをみることでわかります。

user=> SELECT * FROM r1;
i |  t  
---+-----
1 | One
2 | Two
(2 rows)

user=> show data_directory;
     data_directory       
---------------------------
/Users/user/pgsql/11/data
(1 row)

user=> SELECT oid, datname FROM pg_database WHERE datname = 'user';
 oid  | datname 
-------+---------
16384 | user
(1 row)

user=> SELECT relname, relfilenode FROM pg_class WHERE relname = 'r1';
relname | relfilenode 
---------+-------------
r1      |       16434
(1 row)

ファイルは「database_directory + "/base/" + pg_database.oid + "/" + pg_class.relfilenode」にあります。

ページレイアウト

では実際にファイルの中身を見てみましょう。

$ cd /Users/user/pgsql/11/data/base/16384

$ hexdump -C 16434
00000000  00 00 00 00 68 37 30 02  00 00 00 00 20 00 c0 1f  |....h70..... ...|
00000010  00 20 04 20 00 00 00 00  e0 9f 40 00 c0 9f 40 00  |. . ......@...@.|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00001fc0  59 02 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |Y...............|
00001fd0  02 00 02 00 02 09 18 00  02 00 00 00 09 54 77 6f  |.............Two|
00001fe0  59 02 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |Y...............|
00001ff0  01 00 02 00 02 09 18 00  01 00 00 00 09 4f 6e 65  |.............One|
00002000

8kbyte(0 〜 0x00001fff)のファイルの最後のあたりにレコードが格納されていそうにみえますね。

新たにレコードを追加してみましょう。テーブルにINSERTなどを行ってもすぐにはファイルに反映されませんが、CHECKPOINTコマンドを実行するとファイルに反映されます。

--
user=> INSERT INTO r1 VALUES(3, 'Three');
INSERT 0 1
user=> CHECKPOINT;
CHECKPOINT
$ hexdump -C 16434
00000000  00 00 00 00 50 3a 30 02  00 00 00 00 24 00 98 1f  |....P:0.....$...|
00000010  00 20 04 20 00 00 00 00  e0 9f 40 00 c0 9f 40 00  |. . ......@...@.|
00000020  98 9f 44 00 00 00 00 00  00 00 00 00 00 00 00 00  |..D.............|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00001f90  00 00 00 00 00 00 00 00  5a 02 00 00 00 00 00 00  |........Z.......|
00001fa0  00 00 00 00 00 00 00 00  03 00 02 00 02 08 18 00  |................|
00001fb0  03 00 00 00 0d 54 68 72  65 65 00 00 00 00 00 00  |.....Three......|
00001fc0  59 02 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |Y...............|
00001fd0  02 00 02 00 02 09 18 00  02 00 00 00 09 54 77 6f  |.............Two|
00001fe0  59 02 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |Y...............|
00001ff0  01 00 02 00 02 09 18 00  01 00 00 00 09 4f 6e 65  |.............One|
00002000

hexdumpの結果で`*`となっている行は、中身が空なので表示が省略されている部分です。つまりこのファイルは8kbのページの中で、上のちょっとと下のちょっとだけを使っていることになります。これがPostgreSQLの内部構造を勉強したことがある人は一度は見たことがあるページレイアウトの絵です。

スクリーンショット 2019-12-02 15.37.01

このページレイアウトの詳細については以前から公式のドキュメントに説明がありましたが、最近は図も入るようになったようです。

https://www.postgresql.org/docs/12/storage-page-layout.html#STORAGE-PAGE-LAYOUT-FIGURE

可変長レコードの保存方法

ところでなぜこのような構造になっているのでしょう?

固定長のレコードであれば、そのレコード長 * n バイト目にアクセスすればいきなりレコードにアクセスすることができます。

しかしPostgreSQLのレコードはすべて可変長です。可変長のレコードを保存するためには、それぞれのレコードの長さを知るために、たとえばレコードの終端をNULLで区切るとか、あるいは各レコードの先頭に長さを格納するいわゆるPascal文字列のような方法があります。

スクリーンショット 2019-12-05 18.55.35

(説明のために文字列にしましたが、実際に格納されている可変長データはレコード全体です)

もしこのような方法を採用したとすると、1000レコード目を取り出したい時は1レコード目から1000回順に辿る必要があるため、固定長の任意のレコードにアクセスするときと比べて遅くなってしまいます。

実際にはPostgreSQLは、1つのレコードを保存したとき、

1. ページの下側にその可変長データそのものを保存し、
2. そのデータをページ内のどこに保存したかをページの上側に保存します

2.は固定長のデータであるため、任意の場所のレコードに一度でアクセスでき、そこに格納された情報から1.にアクセスすることができます。

いかがでしたか?

PostgreSQLの下側のストレージ周りに興味があるかたは、公式ドキュメントの「第68章 データベースの物理的な格納」もご参照ください。



この記事が気に入ったらサポートをしてみませんか?