本記事は、「PostgreSQL19でテンポラルテーブル設計の新時代へ(1)」の続きです。透過的にテーブルをテンポラル化する方法や、テンポラルテーブルの外部キー制約について記載します。
透過的にテーブルをテンポラル化する
前回、pgbench の標準シナリオをテンポラルテーブルに書き換える例を示しました。このとき、テーブル定義のみならず、トランザクション定義の UPDATE文も書き換えしていました。既存アプリケーションが利用するデータベースをテンポラルテーブル化することを考えると、問合せはそのまま使える透過的な実現方法を考えたいところです。
pgbench を透過的にテンポラル化
前回と同じように pgbench で初期データを作ります。今回は後で通常の pgbench と性能比較もするので、小さすぎるデータサイズを避けるためにスケール(-s) 10倍も指定します。初期データサイズが 150MB 程度になります。定義とデータを分けてダンプ出力します。
$ createdb db1
$ pgbench -i -s 10 db1
$ pg_dump --schema-only -f db1.ddl db1
$ pg_dump --data-only -f db1.dat db1
定義の書き換えをします。まずは、sed を使ってテンポラル化する 3つのテーブル名を「t_」を付加したものに置換します。そのうえで、db1_temporal.ddl を編集します。
$ sed -e 's/pgbench_accounts/t_pgbench_accounts/g' \
-e 's/pgbench_branches/t_pgbench_branches/g' \
-e 's/pgbench_tellers/t_pgbench_tellers/g' \
db1.ddl > db1_temporal.ddl
$ vi db1_temporal.ddl
冒頭(SET文の並びの後)に btree_gist 拡張を導入するのは前回同様です。
CREATE EXTENSION IF NOT EXISTS btree_gist WITH SCHEMA public;
また、これも前回同様に、3つのテーブルにタイムスタンプの範囲データ型の列 valid_at を加えて、主キー定義に WITHOUT OVERLAPS の指定で付加します。今回は timestamp with timezone型の範囲型である tstzrange を使うことにします。
CREATE TABLE public.t_pgbench_accounts (
aid integer NOT NULL,
bid integer,
abalance integer,
filler character(84),
valid_at tstzrange DEFAULT tstzrange(CURRENT_TIMESTAMP, NULL)
)
WITH (fillfactor='100');
CREATE TABLE public.t_pgbench_branches (
bid integer NOT NULL,
bbalance integer,
filler character(88),
valid_at tstzrange DEFAULT tstzrange(CURRENT_TIMESTAMP, NULL)
)
WITH (fillfactor='100');
CREATE TABLE public.t_pgbench_tellers (
tid integer NOT NULL,
bid integer,
tbalance integer,
filler character(84),
valid_at tstzrange DEFAULT tstzrange(CURRENT_TIMESTAMP, NULL)
)
WITH (fillfactor='100');
ALTER TABLE ONLY public.t_pgbench_accounts
ADD CONSTRAINT t_pgbench_accounts_pkey PRIMARY KEY (aid, valid_at WITHOUT OVERLAPS);
ALTER TABLE ONLY public.t_pgbench_branches
ADD CONSTRAINT t_pgbench_branches_pkey PRIMARY KEY (bid, valid_at WITHOUT OVERLAPS);
ALTER TABLE ONLY public.t_pgbench_tellers
ADD CONSTRAINT t_pgbench_tellers_pkey PRIMARY KEY (tid, valid_at WITHOUT OVERLAPS);
最後にビューとルールの定義を加えます。何をしているか、pgbench_accountsビューで見てみましょう。
CREATE VIEW public.pgbench_accounts AS
SELECT aid, bid, abalance, filler FROM public.t_pgbench_accounts
WHERE valid_at @> CURRENT_TIMESTAMP;
CREATE RULE rule_upd_accounts AS ON UPDATE TO public.pgbench_accounts
DO INSTEAD UPDATE public.t_pgbench_accounts
FOR PORTION OF valid_at FROM CURRENT_TIMESTAMP TO NULL
SET aid = NEW.aid, bid = NEW.bid, abalance = NEW.abalance, filler = NEW.filler
WHERE aid = OLD.aid;
CREATE RULE rule_del_accounts AS ON DELETE TO public.pgbench_accounts
DO INSTEAD DELETE FROM public.t_pgbench_accounts
FOR PORTION OF valid_at FROM CURRENT_TIMESTAMP TO NULL WHERE aid = OLD.aid;
追加した valid_at 以外の列を列挙して WHERE valid_at @> CURRENT_TIMESTAMP の条件を付加した検索を元のテーブル名(pgbench_accounts)のビューとして定義しています。これで参照は透過的になります。
ビューに対する UPDATE と DELETE のルールを定義しています。テンポラル化したテーブルに対して FOR PORTION OF valid_at FROM CURRENT_TIMESTAMP TO NULL を付加して、それぞれ基底テーブルに UPDATE と DELETE を行うルールになっています。INSERT については、valid_at列にデフォルト値があるのでルール定義がなくとも問題ありません。
pgbench_branchesビュー、pgbench_tellersビューも同じように定義します。
CREATE VIEW public.pgbench_branches AS
SELECT bid, bbalance, filler FROM public.t_pgbench_branches
WHERE valid_at @> CURRENT_TIMESTAMP;
CREATE RULE rule_upd_branches AS ON UPDATE TO public.pgbench_branches
DO INSTEAD UPDATE public.t_pgbench_branches
FOR PORTION OF valid_at FROM CURRENT_TIMESTAMP TO NULL
SET bid = NEW.bid, bbalance = NEW.bbalance, filler = NEW.filler
WHERE bid = OLD.bid;
CREATE RULE rule_del_branches AS ON DELETE TO public.pgbench_branches
DO INSTEAD DELETE FROM public.t_pgbench_branches
FOR PORTION OF valid_at FROM CURRENT_TIMESTAMP TO NULL WHERE bid = OLD.bid;
CREATE VIEW public.pgbench_tellers AS
SELECT tid, bid, tbalance, filler FROM public.t_pgbench_tellers
WHERE valid_at @> CURRENT_TIMESTAMP;
CREATE RULE rule_upd_tellers AS ON UPDATE TO public.pgbench_tellers
DO INSTEAD UPDATE public.t_pgbench_tellers
FOR PORTION OF valid_at FROM CURRENT_TIMESTAMP TO NULL
SET tid = NEW.tid, bid = NEW.bid, tbalance = NEW.tbalance, filler = NEW.filler
WHERE tid = OLD.tid;
CREATE RULE rule_del_tellers AS ON DELETE TO public.pgbench_tellers
DO INSTEAD DELETE FROM public.t_pgbench_tellers
FOR PORTION OF valid_at FROM CURRENT_TIMESTAMP TO NULL WHERE tid = OLD.tid;
これで定義の書き換えはできました。
データのダンプファイルにも書き換えが必要です。テーブル名を「t_」が負荷されたものに置換します。これは COPY文でビューにデータ投入できないという PostgreSQLの制限によるものです。INSERT文でダンプを取ることもできて、そうすればこの書き換えは不要なのですが、COPY文と比べてデータ投入が大幅に遅いので、ここでは COPY文によるダンプを sed で書き換えする手順としています。
$ sed -e 's/pgbench_accounts/t_pgbench_accounts/g' \
-e 's/pgbench_branches/t_pgbench_branches/g' \
-e 's/pgbench_tellers/t_pgbench_tellers/g' \
db1.dat > db1_temporal.dat
データベースを作り直して、書き換えした定義とデータを投入します。
$ dropdb db1
$ createdb db1
$ psql -f db1_temporal.ddl db1
$ psql -f db1_temporal.dat db1
1回 vacuum を実行した後、pgbench のベンチマークを実行してみます。
$ vacuumdb -z db1
vacuumdb: vacuuming database "db1"
$ pgbench -c 50 -T 60 -n db1
pgbench (19beta1)
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 10
query mode: simple
number of clients: 50
number of threads: 1
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 39255
number of failed transactions: 27 (0.069%)
latency average = 76.315 ms (including failures)
initial connection time = 144.328 ms
tps = 654.731839 (without initial connection time)
問題なく、実行できました。
以下は 60秒間の pgbench実行を 5回行った結果と、
その後、pgbench -S -c 50 -T 60 -n db1 と参照のみの pgbench を実行した結果です。
テンポラル化すると更新トランザクションのスループットは3分の1くらいです。実行している更新処理でやることが増えていますので順当な結果です。1行の UPDATE はテンポラルテーブルでは既存行の UPDATE と 新たな行の INSERT が行われます。
参照トランザクションの性能は6割くらい。これはデータ量が増えていることと、Btreeインデックス検索と GiSTインデックス検索の性能差と考えられます。
データの確認
pgbench実行後のテーブル内容を確認してみます。
元のテーブル名(中身はビュー)で検索すると、テンポラル化していないかのような結果が得られます。「t_」で始まる基底テーブルを参照すると、過去時点データが確認できます。
db1=# SELECT aid, bid, abalance FROM pgbench_accounts ORDER BY aid LIMIT 5;
aid | bid | abalance
-----+-----+----------
1 | 1 | 0
2 | 1 | 0
3 | 1 | -2493
4 | 1 | 0
5 | 1 | 0
(5 rows)
db1=# SELECT tid, bid, tbalance FROM pgbench_tellers ORDER BY tid LIMIT 5;
tid | bid | tbalance
-----+-----+----------
1 | 1 | -49250
2 | 1 | -363
3 | 1 | 43287
4 | 1 | 24212
5 | 1 | -64892
(5 rows)
db1=# SELECT bid, bbalance FROM pgbench_branches ORDER BY bid LIMIT 5;
bid | bbalance
-----+----------
1 | -43072
2 | 218964
3 | -181770
4 | -114850
5 | 129253
(5 rows)
db1=# SELECT aid, bid, abalance, valid_at FROM t_pgbench_accounts ORDER BY aid LIMIT 5;
aid | bid | abalance | valid_at
-----+-----+----------+------------------------------------------------------------------
1 | 1 | 0 | ["2026-07-05 10:24:02.76989+09",)
2 | 1 | 0 | ["2026-07-05 10:24:02.76989+09",)
3 | 1 | -2493 | ["2026-07-05 10:47:41.928569+09",)
3 | 1 | 0 | ["2026-07-05 10:24:02.76989+09","2026-07-05 10:47:41.928569+09")
4 | 1 | 0 | ["2026-07-05 10:24:02.76989+09",)
(5 rows)
db1=# SELECT tid, bid, tbalance, valid_at FROM t_pgbench_tellers ORDER BY tid LIMIT 5;
tid | bid | tbalance | valid_at
-----+-----+----------+-------------------------------------------------------------------
1 | 1 | -16270 | ["2026-07-05 10:46:41.359816+09","2026-07-05 10:46:41.495519+09")
1 | 1 | 38092 | ["2026-07-05 10:47:34.143704+09","2026-07-05 10:47:34.365527+09")
1 | 1 | -4873 | ["2026-07-05 10:45:58.724794+09","2026-07-05 10:45:58.769285+09")
1 | 1 | 0 | ["2026-07-05 10:24:42.372927+09","2026-07-05 10:45:58.724794+09")
1 | 1 | -7128 | ["2026-07-05 10:45:58.769285+09","2026-07-05 10:45:58.77503+09")
(5 rows)
db1=# SELECT bid, bbalance, valid_at FROM t_pgbench_branches ORDER BY bid LIMIT 5;
bid | bbalance | valid_at
-----+----------+-------------------------------------------------------------------
1 | -27597 | ["2026-07-05 10:46:47.828591+09","2026-07-05 10:46:47.853358+09")
1 | 698 | ["2026-07-05 10:45:58.62488+09","2026-07-05 10:45:58.626371+09")
1 | 0 | ["2026-07-05 10:24:42.367049+09","2026-07-05 10:45:58.62488+09")
1 | -8691 | ["2026-07-05 10:45:59.817157+09","2026-07-05 10:46:01.852082+09")
1 | -1927 | ["2026-07-05 10:45:58.626371+09","2026-07-05 10:45:58.62905+09")
(5 rows)
さまざまな操作で矛盾は無いか?
pgbench 標準シナリオのトランザクションに現れるUPDATEは行を1件取り出して更新するものでした。
テンポラル化したビューを、複数行を操作したり、行を削除したり、あるいは主キー列を変更したりするときに、おかしなことが起きたりしないでしょうか?
複数行の更新
複数行を更新すると通常のテーブルと同じような結果になります。
db1=# BEGIN;
BEGIN
db1=*# UPDATE pgbench_accounts SET abalance = 10000 WHERE aid <= 5;
UPDATE 5
db1=*# SELECT aid, bid, abalance FROM pgbench_accounts WHERE aid <= 10 ORDER BY aid;
aid | bid | abalance
-----+-----+----------
1 | 1 | 10000
2 | 1 | 10000
3 | 1 | 10000
4 | 1 | 10000
5 | 1 | 10000
6 | 1 | 0
7 | 1 | 0
8 | 1 | 0
9 | 1 | -2126
10 | 1 | 0
(10 rows)
db1=*# SELECT aid, bid, abalance, valid_at FROM t_pgbench_accounts WHERE aid <= 10 ORDER BY aid;
aid | bid | abalance | valid_at
-----+-----+----------+-------------------------------------------------------------------
1 | 1 | 10000 | ["2026-07-05 11:12:32.662822+09",)
1 | 1 | 0 | ["2026-07-05 10:24:02.76989+09","2026-07-05 11:12:32.662822+09")
2 | 1 | 0 | ["2026-07-05 10:24:02.76989+09","2026-07-05 11:12:32.662822+09")
2 | 1 | 10000 | ["2026-07-05 11:12:32.662822+09",)
3 | 1 | -2493 | ["2026-07-05 10:47:41.928569+09","2026-07-05 11:12:32.662822+09")
3 | 1 | 0 | ["2026-07-05 10:24:02.76989+09","2026-07-05 10:47:41.928569+09")
3 | 1 | 10000 | ["2026-07-05 11:12:32.662822+09",)
4 | 1 | 10000 | ["2026-07-05 11:12:32.662822+09",)
4 | 1 | 0 | ["2026-07-05 10:24:02.76989+09","2026-07-05 11:12:32.662822+09")
5 | 1 | 0 | ["2026-07-05 10:24:02.76989+09","2026-07-05 11:12:32.662822+09")
5 | 1 | 10000 | ["2026-07-05 11:12:32.662822+09",)
6 | 1 | 0 | ["2026-07-05 10:24:02.76989+09",)
7 | 1 | 0 | ["2026-07-05 10:24:02.76989+09",)
8 | 1 | 0 | ["2026-07-05 10:24:02.76989+09",)
9 | 1 | -2126 | ["2026-07-05 10:50:26.461806+09",)
9 | 1 | 0 | ["2026-07-05 10:24:02.76989+09","2026-07-05 10:50:26.461806+09")
10 | 1 | 0 | ["2026-07-05 10:24:02.76989+09",)
(17 rows)
db1=*# ABORT ;
ROLLBACK
基底テーブルを参照すると更新した各行に規則通り有効範囲がセットされています。
複数行の削除
同じく複数行を削除してみます。
db1=# BEGIN;
BEGIN
db1=*# DELETE FROM pgbench_accounts WHERE aid < 3;
DELETE 2
db1=*# SELECT aid, bid, abalance FROM pgbench_accounts WHERE aid <= 5 ORDER BY aid;
aid | bid | abalance
-----+-----+----------
3 | 1 | -2493
4 | 1 | 0
5 | 1 | 0
(3 rows)
db1=*# SELECT aid, bid, abalance, valid_at FROM t_pgbench_accounts WHERE aid <= 5 ORDER BY aid;
aid | bid | abalance | valid_at
-----+-----+----------+------------------------------------------------------------------
1 | 1 | 0 | ["2026-07-05 10:24:02.76989+09","2026-07-05 11:22:13.237397+09")
2 | 1 | 0 | ["2026-07-05 10:24:02.76989+09","2026-07-05 11:22:13.237397+09")
3 | 1 | -2493 | ["2026-07-05 10:47:41.928569+09",)
3 | 1 | 0 | ["2026-07-05 10:24:02.76989+09","2026-07-05 10:47:41.928569+09")
4 | 1 | 0 | ["2026-07-05 10:24:02.76989+09",)
5 | 1 | 0 | ["2026-07-05 10:24:02.76989+09",)
(6 rows)
db1=*# ABORT ;
ROLLBACK
これも問題ありません。ビューの上では削除した行は見えなくなり、基底テーブルでは削除した行に有効範囲の終わりがセットされています。
主キー列の書き換え
主キー列を変更する UPDATE を実行するとどうなるでしょうか。
db1=# BEGIN;
BEGIN
db1=*# UPDATE pgbench_accounts SET aid = -1 WHERE aid = 1;
UPDATE 1
db1=*# SELECT aid, bid, abalance FROM pgbench_accounts WHERE aid <= 3 ORDER BY aid;
aid | bid | abalance
-----+-----+----------
-1 | 1 | 0
2 | 1 | 0
3 | 1 | -2493
(3 rows)
db1=*# SELECT aid, bid, abalance, valid_at FROM t_pgbench_accounts WHERE aid <= 3 ORDER BY aid;
aid | bid | abalance | valid_at
-----+-----+----------+------------------------------------------------------------------
-1 | 1 | 0 | ["2026-07-05 11:27:29.401969+09",)
1 | 1 | 0 | ["2026-07-05 10:24:02.76989+09","2026-07-05 11:27:29.401969+09")
2 | 1 | 0 | ["2026-07-05 10:24:02.76989+09",)
3 | 1 | -2493 | ["2026-07-05 10:47:41.928569+09",)
3 | 1 | 0 | ["2026-07-05 10:24:02.76989+09","2026-07-05 10:47:41.928569+09")
(5 rows)
db1=*# ABORT ;
ROLLBACK
ビューのうえでは通常のテーブルで実行したのと同じ結果が得られてます。aid=1 の行が aid=-1 の行になっています。基底テーブル上では、aid=1の行の有効範囲の終わりがセットされ、aid=-1の行が新たに始まっているという形で記録されています。
矛盾はありませんが、「aid=1 の行が aid=-1 の行に変わった」という情報は遺されていないことに注意が必要です。「aid=1の行を削除して、aid以外は同じ内容を持つaid=-1の行を挿入した」という形で履歴が残ります。
テンポラル化と外部キー制約
透過的にテーブル定義をテンポラル化するにあたって、元のテーブル定義に外部キー制約があった場合にはどうすれば良いでしょうか?
テンポラル化したテーブル同士であれば、以下のようにして外部キー制約を加えることができます。参照元、参照先(被参照)ともに「,PERIOD 《範囲をあらわす列》」を指定しています。その有効範囲において参照先テーブルに対応する行が存在するかが検査されるようになります。
ALTER TABLE t_pgbench_accounts ADD CONSTRAINT fk_bid
FOREIGN KEY (bid, PERIOD valid_at)
REFERENCES t_pgbench_branches (bid, PERIOD valid_at);
ALTER TABLE t_pgbench_tellers ADD CONSTRAINT fk_bid
FOREIGN KEY (bid, PERIOD valid_at)
REFERENCES t_pgbench_branches (bid, PERIOD valid_at);
この定義は、ここまで作ってきたデータベースに後から加えると制約違反エラーになります。元の pgbench によるデータ初期化で外部キーを作らないデフォルトの方法をとっていたため、テーブル行の投入順序が、参照元テーブルが先になっていて、参照先テーブルに対応する行が無い時点のタイムスタンプで行が作られてしまっているからです。
以下のように --init-steps=dtgvpf を指定して初期化をすればテーブルに外部キー制約が作られます。dtgvp がデフォルトの指定で、最後の f が外部キー制約の追加です。このデータをダンプすれば、参照先テーブルのデータ投入から先に書き出されます。
$ createdb db2
$ pgbench -s 10 -i --init-steps=dtgvpf db2
$ pg_dump --schema-only -f db2.ddl db2
$ pg_dump --data-only -f db2.dat db2
後は、これまで同様にテンポラル化を行い、db2.ddl を書き換える時に、PERIOD 指定を伴う外部キー制約に書き換えします。
少し困るのは、テンポラル化しているテーブルと、テンポラル化していないテーブルの間で外部キー制約がある場合です。本例でいえば、テンポラル化していない pgbench_historyテーブルと、t_pgbench_accounts、t_pgbench_branches、t_pgbench_tellers テーブルの間に外部キー制約を作るうまい方法がありません。
pgbench_historyテーブルの外部キー制約は無しとすることとして、t_pgbench_accounts と t_pgbench_tellers の外部キー制約を加えた状態で、データの読み戻しと pgbench の実行が可能であることを確認できました。
$ pgbench -c 50 -T 60 -n db2
pgbench (19beta1)
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 10
query mode: simple
number of clients: 50
number of threads: 1
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 8
number of failed transactions: 63 (88.732%)
latency average = 70997.305 ms (including failures)
initial connection time = 96.438 ms
tps = 0.079352 (without initial connection time)
この範囲付き外部キー制約の検査は、かなり負荷の高い処理であるようで、更新トランザクションのスループット性能は著しく低下しました。tps = 650 くらいであったものが、同環境で tps = 0.1 くらいになります。テンポラル化していない場合には、外部キー制約付きの pgbench実行でも、10%くらいの性能ダウンで済みます。
auto_explain を使って実行プランを調べると、一部のテーブル(t_pgbench_branch)の外部キー検査のためのトリガ動作に時間を要していることが★の部分からわかります。
2026-07-06 09:47:08.657 JST [1482] LOG: duration: 0.010 ms plan:
Query Text: SELECT abalance FROM pgbench_accounts WHERE aid = 677745;
--中略--
2026-07-06 09:47:08.657 JST [1482] LOG: duration: 0.097 ms plan:
Query Text: UPDATE pgbench_tellers SET tbalance = tbalance + -1021 WHERE tid = 48;
--中略--
2026-07-06 09:47:08.791 JST [1482] LOG: duration: 133.288 ms plan:
Query Text: UPDATE pgbench_branches SET bbalance = bbalance + -1021 WHERE bid = 3;
Update on public.t_pgbench_branches (cost=0.00..2.39 rows=0 width=0) (actual time=0.025..0.026 rows=0.00 loops=1)
Buffers: shared hit=13
-> Nested Loop (cost=0.00..2.39 rows=1 width=408) (actual time=0.007..0.008 rows=1.00 loops=1)
Output: t_pgbench_branches_1.bid, (t_pgbench_branches_1.bbalance + '-1021'::integer), t_pgbench_branches_1.filler, range_intersect(t_pgbench_branches.valid_at, tstzrange(CURRENT_TIMESTAMP, NULL::timestamp with time zone)), t_pgbench_branches.ctid, t_pgbench_branches_1.ctid
Buffers: shared hit=2
-> Seq Scan on public.t_pgbench_branches (cost=0.00..1.20 rows=1 width=24) (actual time=0.004..0.005 rows=1.00 loops=1)
Output: t_pgbench_branches.valid_at, t_pgbench_branches.ctid, t_pgbench_branches.bid
Filter: ((t_pgbench_branches.bid = 3) AND (t_pgbench_branches.valid_at && tstzrange(CURRENT_TIMESTAMP, NULL::timestamp with time zone)))
Rows Removed by Filter: 28
Buffers: shared hit=1
-> Seq Scan on public.t_pgbench_branches t_pgbench_branches_1 (cost=0.00..1.18 rows=1 width=370) (actual time=0.002..0.002 rows=1.00 loops=1)
Output: t_pgbench_branches_1.bid, t_pgbench_branches_1.bbalance, t_pgbench_branches_1.filler, t_pgbench_branches_1.ctid
Filter: ((t_pgbench_branches_1.bid = 3) AND (t_pgbench_branches_1.valid_at @> CURRENT_TIMESTAMP))
Rows Removed by Filter: 28
Buffers: shared hit=1
★ Trigger RI_ConstraintTrigger_a_28050 for constraint fk_bid: time=133.105 calls=1
Trigger RI_ConstraintTrigger_a_28055 for constraint fk_bid: time=0.156 calls=1
これに対して、CREATE INDEX ON t_pgbench_accounts (bid); あるいは CREATE INDEX ON t_pgbench_accounts USING gist (bid, valid_at); など外部キー検査のためのインデックスを作っても、それほど効果が得られませんでした。
これらのことから、テンポラル化したテーブルへの外部キー制約は、今時点では現実的な現実的な性能での利用が難しそうです。
READ COMMITTED 分離レベルでの問題
また、もう一つ問題がありました。外部キー制約付きのテンポラル化した pgbench では、以下のような制約違反エラーが出ることがありました。これはトランザクション隔離レベルがデフォルトの read committed のときに発生します。
$ pgbench -c 50 -T 60 -n db1
pgbench (19beta1)
pgbench: error: client 14 script 0 aborted in command 5 query 0: ERROR: insert or update on table "t_pgbench_accounts" violates foreign key constraint "fk_bid"
DETAIL: Key (bid, valid_at)=(2, ["2026-07-06 10:41:59.665092+09","2026-07-07 11:50:57.385281+09")) is not present in table "t_pgbench_branches".
pgbench: error: client 8 script 0 aborted in command 7 query 0: ERROR: insert or update on table "t_pgbench_tellers" violates foreign key constraint "fk_bid"
DETAIL: Key (bid, valid_at)=(2, ["2026-07-06 10:42:54.649404+09","2026-07-07 11:50:57.385497+09")) is not present in table "t_pgbench_branches".
default_transaction_isolation = 'repeatable read' とすると、この制約違反発生しなくなりますが、デッドロックエラーや直列化失敗エラーが多数発生します。
PostgreSQL 19beta1 の時点で本記事を書きましたが、READ COMMITTED でテンポラル更新を使う場合には、明示的な行ロックを加える等しないと危険、ということが 19beta2 の段階でドキュメントに加筆されていました。
まとめ/パーティションテーブル
通常のテーブル定義をビューとルールで透過的にテンポラル化する方法を見てきました。アプリケーションからは通常テーブルのように見えるテンポラルテーブルを用意することができました。
一方、外部キー制約については性能の観点で実用には問題がありそうです。
本テーマでもう1つだけ書こうと思います。次回は、パーティションテーブルとの組み合わせについて検討します。

