ぱと隊長日誌

ブログ運用もエンジニアとしての生き方も模索中

PostgreSQL は更新処理を ROLLBACK してもテーブルファイルに追記される

概要

PostgreSQL はテーブルに対する更新処理 (INSERT / UPDATE / DELETE) を行うと、テーブルファイルに追記されます(追記型アーキテクチャ)。これは最終的に COMMIT した場合に限らず、ROLLBACK された場合でも同様となります。

本エントリではこの挙動を検証します。

なお、追記型アーキテクチャについて知りたい方は下記の記事を参照ください。
PostgreSQL Deep Dive: PostgreSQLのストレージアーキテクチャ(基本編)

検証

検証環境

  • PostgreSQL 12.1
    • contrib モジュールをインストールしました。
    • postgresql.conf で "autovacuum = off" に設定しました。

ROLLBACK とテーブルファイルサイズ

各ステップでテーブルファイルサイズがどのように変化するかを確認します。

テスト用のテーブルを作成し、テーブルファイルのパスを調べます。

testdb=# CREATE TABLE tab1(c1 integer);

testdb=# SELECT pg_relation_filepath('tab1');
 pg_relation_filepath
----------------------
 base/16384/25445
(1 row)

テーブルファイルのサイズを調べます。

$ cd /usr/local/pgsql/data/base/16384/

$ ls -l 25445*
-rw-------. 1 postgres postgres 0  6月  6 10:23 25445

作成直後は0バイトであることがわかります。

テーブルに1行追加します。

testdb=# BEGIN;

testdb=# INSERT INTO tab1(c1) VALUES (1);

testdb=# CHECKPOINT;

この時点でのテーブルファイルのサイズとハッシュ値を調べます。

$ ls -l 25445*
-rw-------. 1 postgres postgres 8192  6月  6 10:24 25445

$ md5sum 25445
6a5133d3254b46397a1cd33b184e8248  25445

COMMIT も ROLLBACK もしていませんが、テーブルファイルのサイズが8192バイトになりました。

ROLLBACK を実行します。

testdb=# ROLLBACK;

testdb=# CHECKPOINT;

testdb=# SELECT COUNT(*) FROM tab1;
 count
-------
     0
(1 row)
$ ls -l 25445*
-rw-------. 1 postgres postgres 8192  6月  6 10:24 25445

$ md5sum 25445
6a5133d3254b46397a1cd33b184e8248  25445

ROLLBACK したのでテーブルは0行ですが、テーブルファイルのサイズは変わらず8192バイトあります。また、ROLLBACK前後でファイルのハッシュ値が一致したことから、更新されていないことが分かります。

VACCUM を実行します。

testdb=# VACUUM tab1;
$ ls -l 25445*
-rw-------. 1 postgres postgres     0  6月  6 10:27 25445
-rw-------. 1 postgres postgres 16384  6月  6 10:27 25445_fsm
-rw-------. 1 postgres postgres     0  6月  6 10:27 25445_vm

テーブルファイルのサイズが0バイトに戻りました。

ROLLBACK とテーブルファイルの中身

ROLLBACK 前後でテーブルファイルの中身(ページヘッダとタプルの情報)を比較しました。比較には pageinspect モジュールを利用しました。

ページヘッダとタプルのデータ構造は以下の記事を参照ください。
The Internals of PostgreSQL
1.3. Internal Layout of a Heap Table File
5.2. Tuple Structure

testdb=# SELECT * FROM page_header(get_raw_page('tab1', 0));
ERROR:  block number 0 is out of range for relation "tab1"

testdb=# SELECT * FROM heap_page_items(get_raw_page('tab1', 0));
ERROR:  block number 0 is out of range for relation "tab1"

testdb=# BEGIN;
BEGIN
testdb=# INSERT INTO tab1(c1) VALUES (1);
INSERT 0 1

testdb=# SELECT * FROM page_header(get_raw_page('tab1', 0));
    lsn     | checksum | flags | lower | upper | special | pagesize | version | prune_xid
------------+----------+-------+-------+-------+---------+----------+---------+-----------
 2/692608B8 |        0 |     0 |    28 |  8160 |    8192 |     8192 |       4 |         0
(1 row)

testdb=# SELECT * FROM heap_page_items(get_raw_page('tab1', 0));
 lp | lp_off | lp_flags | lp_len |  t_xmin  | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid |   t_data
----+--------+----------+--------+----------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
  1 |   8160 |        1 |     28 | 26881546 |      0 |        0 | (0,1)  |           1 |       2048 |     24 |        |       | \x01000000
(1 row)

testdb=# ROLLBACK;
ROLLBACK

testdb=# SELECT * FROM page_header(get_raw_page('tab1', 0));
    lsn     | checksum | flags | lower | upper | special | pagesize | version | prune_xid
------------+----------+-------+-------+-------+---------+----------+---------+-----------
 2/692608B8 |        0 |     0 |    28 |  8160 |    8192 |     8192 |       4 |         0
(1 row)

testdb=# SELECT * FROM heap_page_items(get_raw_page('tab1', 0));
 lp | lp_off | lp_flags | lp_len |  t_xmin  | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid |   t_data
----+--------+----------+--------+----------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
  1 |   8160 |        1 |     28 | 26881546 |      0 |        0 | (0,1)  |           1 |       2048 |     24 |        |       | \x01000000
(1 row)

ROLLBACK 前後でページヘッダとタプルの情報に差異は無いことが分かりました。

なお、この後に PostgreSQL を再起動しても、ページヘッダとタプルの情報に変化はありませんでした。このことからも進行中のトランザクションの更新結果は揮発性の情報として管理されるのでなく、テーブルファイルに書き込まれたと推測できます。

Twitterでは @tatsuo_ishii さんからも裏付けるコメントをいただきました。


なお、コメント後半の「直観に反している」はご指摘の通り「Oracle的な常識」からの話でした(※本記事ではなく、私のTwitter投稿内容に対するコメントです)。

まとめ

PostgreSQL のトランザクション & MVCC & スナップショットの仕組み
この記事の説明によれば、PostgreSQL は ROLLBACK してもトランザクションの状態を Abort に更新するのみであり、テーブルファイルには更新処理の結果が残っています。今回の検証結果はこれを裏付けるものとなりました。

テーブルファイル内の不要なデータは VACUUM を実行するまで削除されません。これは処理性能劣化の原因となり得ます。

最終的に ROLLBACK するからと安易にテーブルを更新することは、例えテストであっても慎重になった方が良いでしょう。

更新情報

2020/06/06

【ROLLBACK とテーブルファイルサイズ】

更新結果がバッファのみに存在する可能性を考慮し、INSERT と ROLLBACK の後に CHECKPOINT を実行する手順に変更しました。また、テーブルファイルのハッシュ値比較を追加しました。

【ROLLBACK とテーブルファイルの中身】

以下の内容を追記しました。

  • ページヘッダとタプルのデータ構造についての参考記事。
  • ROLLBACK 後に PostgreSQL を再起動して、ページヘッダとタプルを比較した結果と考察。
  • Twitter での @tatsuo_ishii さんからのコメント引用。