PostgreSQLでTRUNCATEをROLLBACKした際の内部の挙動を追いかけてみた
好きなRDBはPostgreSQLのCX事業本部@大阪の岩田です。
PostgreSQLの内部アーキテクチャについて少し調べてみたので内容をご紹介します。
環境
今回利用した環境です。
- OS: AmazonLinux2 (ami-0af1df87db7b650f4)
- PostgreSQL: 11.5-5
PostgreSQLの内部アーキテクチャ概要
初めて知った時はビックリしたのですが、PostgreSQLはDML以外にDDLもROLLBACKすることが可能です。つまりトランザクション内でTRUNCATEやDROP TABLEといったSQLを発行した場合もROLLBACKが可能です。これはPostgreSQL固有の内部アーキテクチャによって実現されています。
PostgreSQLはOracleやMySQLといったRDBとは違い、追記型アーキテクチャを採用しています。ざっくり説明すると追記型アーキテクチャでは行データの更新を行った際に、元データを更新するのではなく、元データに削除済みフラグを付け新たに更新後のデータを追加するような動作をします。10列中1列だけ更新するようなケースでも更新対象外の列まで含んだ10列分のデータを追加するので、更新性能が出づらいというデメリットはありますが、ファイル内に複数バージョンの行データが存在するので、読み取り一貫性の実現が容易というメリットもあります。
PostgreSQLの行データは実際のユーザーデータ(各列のデータ)に加えて以下の構造を持つHeapTupleHeaderDataというヘッダ情報等から構成されています。
| フィールド | 型 | 長さ | 説明 |
|---|---|---|---|
| t_xmin | TransactionId | 4バイト | 挿入XIDスタンプ |
| t_xmax | TransactionId | 4バイト | 削除XIDスタンプ |
| t_cid | CommandId | 4バイト | 挿入、削除の両方または片方のCIDスタンプ(t_xvacと共有) |
| t_xvac | TransactionId | 4バイト | 行バージョンを移すVACUUM操作用XID |
| t_ctid | ItemPointerData | 6バイト | この行または最新バージョンの行の現在のTID |
| t_infomask2 | uint16 | 2バイト | 属性の数と各種フラグビット |
| t_infomask | uint16 | 2バイト | 様々なフラグビット |
| t_hoff | uint8 | 1バイト | ユーザデータに対するオフセット |
PostgreSQL 11.5文書 68.6. データベースページのレイアウト
このヘッダ情報のt_xminやt_xmaxを参照することで、どのトランザクションに対してどのバージョンの行データを見せるべきなのか?ということが判断できる訳です。※XID = トランザクションID
また、PostgreSQLは基本的に1リレーションにつき1ファイルを利用してデータを管理します(TOASTテーブルなどの例外もあります)。例えばOracleの場合は1つ以上のデータファイルから構成された表領域に複数のテーブルやインデックスが格納されますが、PostgreSQLの場合はテーブルやインデックスといったDB上のオブジェクト1つにつきOS上の1ファイルが対応付きます。PostgreSQLでTRUNCATEを実行すると、対象テーブル用の物理ファイルを新しく作成し、以後は新しい物理ファイルを参照するようになります。古い物理ファイルはTRUNCATEがを実行したトランザクションがコミットされるまで(実際はコミットされてからもしばらく)残り続けるため、ROLLBACKが可能になるという訳です。
やってみる
実際にTRUNCATEをROLLBACKした際にPostgreSQLの内部データがどのように遷移するか確認してみましょう。
環境構築
まずは検証環境の準備としてAmazon Linux上にPostgresSQLの環境を用意します。後ほどpageinspectを利用するので、PostgreSQL本体に加えて拡張モジュールもインストールしておきます。
1 2 | $ sudo amazon-linux-extras enable postgresql11$ sudo yum install postgresql-server postgresql-contrib |
PostgreSQLがインストールできたらDBクラスタを作成します。
1 2 3 | $ su - postgres$ initdb$ pg_ctl start |
検証用に適当なテーブルが欲しいので、pgbenchを使ってテーブルを作成します。
1 2 | $ psql -c "create database pgbench;"$ pgbench -i pgbench |
準備できたらFull Vacuumをかけてゴミを掃除しておきます。
1 | $ vacuumdb --full -d pgbench |
実際に挙動を追いかけてみる
実際にトランザクション内でテーブルをtruncateした際に、PostgreSQLのシステムカタログや物理ファイルがどのように更新されるかを追いかけてみます。今回はpgbenchで作成した pgbench_branchesテーブルを利用して動作を見ていきます。
まずはpageinspectが利用できるようにEXTENSIONを作成します。pageinspectはPostgreSQLの拡張モジュールでpageinspectを利用することで物理ファイル内のページから色々な情報を覗きみることができます。
1 2 | pgbench=# CREATE EXTENSION pageinspect;CREATE EXTENSION |
まずpgbench_branchesテーブルのOIDを確認します。
1 2 3 4 5 | pgbench=# select oid,relfilenode from pg_class where relname = 'pgbench_branches'; oid | relfilenode-------+------------- 19763 | 19780(1 row) |
pgbench_branchesテーブルのOIDは19763で、現在は19780という物理ファイルにテーブルのデータを格納していることが分かりました。続いてシステムカタログpg_classの物理ファイルに書き込まれている pgbench_branchesに関するタプルの情報を確認します。heap_page_itemsを利用することで前述のHeapTupleHeaderDataのデータが確認できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | pgbench=# SELECT * FROM heap_page_items(get_raw_page('pg_class', 0)) WHERE t_oid = 19763;-[ RECORD 1 ]-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------lp | 3lp_off | 7584lp_flags | 1lp_len | 213t_xmin | 1266t_xmax | 0t_field3 | 2t_ctid | (0,3)t_infomask2 | 33t_infomask | 11019t_hoff | 32t_bits | 1111111111111111111111111111110100000000t_oid | 19763t_data | \x706762656e63685f6272616e6368657300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000098080000354d0000000000000a00000000000000444d000000000000010000000000803f0000000000000000010070720300000000000000000001640000000000000000f2040000010000005301000000000000001900000001000000010000004800000066696c6c666163746f723d3130300000 |
t_minが1266、t_xmax が0となっています。トランザクションIDが1266以後のトランザクションから見たpgbench_branchesテーブルのメタデータは上記タプルの情報になることが分かります。
検証用にpsqlのセッションをもう1つ立ち上げて(以後セッションAとします)トランザクションを開始します。
1 2 3 4 5 6 7 | pgbench=# begin;BEGINpgbench=# select txid_current(); txid_current-------------- 1344(1 row) |
もう片方のセッションからトランザクションを開始し(以後セッションBとします)pgbench_branches テーブルをTRUNCATEしてみます。
1 2 3 4 | pgbench=# begin;BEGINpgbench=# truncate table pgbench_branches;TRUNCATE TABLE |
再度システムカタログpg_classの物理ファイルの中身を確認してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | pgbench=# SELECT * FROM heap_page_items(get_raw_page('pg_class', 0)) WHERE t_oid = 19763;-[ RECORD 1 ]-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------lp | 3lp_off | 7584lp_flags | 1lp_len | 213t_xmin | 1266t_xmax | 1345t_field3 | 0t_ctid | (0,12)t_infomask2 | 33t_infomask | 8971t_hoff | 32t_bits | 1111111111111111111111111111110100000000t_oid | 19763t_data | \x706762656e63685f6272616e6368657300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000098080000354d0000000000000a00000000000000444d000000000000010000000000803f0000000000000000010070720300000000000000000001640000000000000000f2040000010000005301000000000000001900000001000000010000004800000066696c6c666163746f723d3130300000-[ RECORD 2 ]-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------lp | 12lp_off | 560lp_flags | 1lp_len | 213t_xmin | 1345t_xmax | 0t_field3 | 0t_ctid | (0,12)t_infomask2 | 33t_infomask | 10251t_hoff | 32t_bits | 1111111111111111111111111111110100000000t_oid | 19763t_data | \x706762656e63685f6272616e6368657300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000098080000354d0000000000000a00000000000000e84e0000000000000000000000000000000000000000000001007072030000000000000000000164000000000000000040050000010000005301000000000000001900000001000000010000004800000066696c6c666163746f723d3130300000 |
今度は2レコード抽出されました。1レコード目は t_min が1266、t_maxが1345なので、トランザクションIDが1266 ~ 1345の範囲内から参照されるタプルです。2レコード目はt_minが1345、t_maxが0なのでトランザクションIDが1345以後のトランザクションから参照されるタプルです。セッションBから、現在のトランザクションIDを確認してみましょう。
1 2 3 | pgbench=# select txid_current();-[ RECORD 1 ]+-----txid_current | 1345 |
トランザクションIDは1345なので、セッションBから見たpgbench_branchesテーブルのメタデータはpg_classの物理ファイルに格納されたpgbench_branchesに関するタプルの中で、2つ目のタプルの情報を参照することが分かります。セッションBからpg_classの情報をSELECTしてみます。
1 2 3 4 5 | pgbench=# select oid,relfilenode from pg_class where relname = 'pgbench_branches'; oid | relfilenode-------+------------- 19763 | 20200(1 row) |
TRUNCATE実行前からrelfilenodeの値が変わっていることが分かります。セッションAからもpg_classの情報をSELECTしてみます。
1 2 3 4 5 | pgbench=# select oid,relfilenode from pg_class where relname = 'pgbench_branches'; oid | relfilenode-------+------------- 19763 | 19780(1 row) |
こちらのセッションからみるとrelfilenodeは19780のままです。
セッションBをROLLBACKしてTRUNCATEを無かったことにします。
1 2 | pgbench=# rollback;ROLLBACK |
Vacuumを実行して削除フラグの立ったpg_class内のタプルを回収後に再度pg_classの物理ファイルの中身を確認してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | pgbench=# vacuum;VACUUMSELECT * FROM heap_page_items(get_raw_page('pg_class', 0)) WHERE t_oid = 19763;pgbench=# SELECT * FROM heap_page_items(get_raw_page('pg_class', 0)) WHERE t_oid = 19763;-[ RECORD 1 ]-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------lp | 40lp_off | 768lp_flags | 1lp_len | 213t_xmin | 1427t_xmax | 0t_field3 | 2t_ctid | (0,40)t_infomask2 | 33t_infomask | 11019t_hoff | 32t_bits | 1111111111111111111111111111110100000000t_oid | 19763t_data | \x706762656e63685f6272616e6368657300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000098080000354d0000000000000a000000000000007a50000000000000010000000000803f000000000000000001007072030000000000000000000164000000000000000093050000010000005301000000000000001900000001000000010000004800000066696c6c666163746f723d3130300000 |
pgbench_branchesテーブルに関する情報が1タプルに減っていることが分かります。再度セッションBからpg_classの情報をSELECTしてみます。
1 2 3 4 5 | pgbench=# select oid,relfilenode from pg_class where relname = 'pgbench_branches'; oid | relfilenode-------+------------- 19763 | 19780(1 row) |
relfilenodeが19780に戻りました。これでセッションBからpgbench_branchesテーブルを参照した際も物理ファイル19780を参照することになり、TRUNCATE実行前のpgbench_branchesの中身が見れるようになったことが分かります。
まとめ
実際に物理ファイルの中身がどのように更新されていくか確認することで、PostgreSQLの挙動をより深く理解することができました。また色々と調べてみようと思います。