【実務・中級編】 CLOGとサブトランザクション – PostgreSQL

みんな、元気にしてるかい? PostgreSQLの深淵に挑む連載、今回はちょっとコアな部分、CLOG(Commit Log)とサブトランザクションについて掘り下げていこうと思う。

普段何気なく `BEGIN` して `COMMIT` してるトランザクション、その裏側で一体何が起こっているのか、考えたことあるかい? そして、`SAVEPOINT` を使って部分的にロールバックできる仕組みって、どうやって実現されてるんだろう?

データベースエンジニアとして、これらの仕組みを理解しておくことは、トラブルシューティングの際や、より堅牢なアプリケーションを設計する上で、めちゃくちゃ強力な武器になるんだ。教科書的な説明だけじゃなくて、僕自身の経験も踏まえながら、実務で役立つ視点も交えて解説していくから、最後までついてきてくれよな!

—

CLOG(Commit Log)って、結局何者?

まずはCLOGから。名前の通り「コミットログ」なんだけど、これがPostgreSQLのトランザクション管理の心臓部の一つなんだ。

CLOGの役割と重要性

CLOGの最も重要な役割は、各トランザクションのコミット状態(IN_PROGRESS, COMMITTED, ABORTED)を記録・管理することだ。

ちょっと想像してみてほしいんだけど、PostgreSQLにはMVCC(Multi-Version Concurrency Control)という仕組みがあるよね。これは、あるトランザクションがデータを変更している最中でも、他のトランザクションからは変更前のデータが見える、っていう優れものだ。このMVCCを実現するためには、特定のデータ行が見えるべきかどうかを判断する基準が必要になる。その判断基準の一つが、まさにこのCLOGなんだ。

例えば、あるトランザクションが「X」というIDでデータを更新したとする。別のトランザクションがそのデータを見ようとしたとき、「あれ? トランザクションXはまだコミットされてないから、このデータは見えないな」とか、「トランザクションXはコミット済みだから、このデータが見えるな」といった判断をする。この「コミットされているかどうか」の情報を教えてくれるのが、CLOGなんだ。

`pg_xact` ディレクトリの秘密

CLOGの実体は、PostgreSQLのデータディレクトリ内の `pg_xact` (PostgreSQL 9.6以前は `pg_clog`) というディレクトリにあるファイル群だ。これらのファイルは、トランザクションID(XID)をキーとして、そのトランザクションの状態をビットマップ形式で保持している。

各トランザクションIDに対して、たった2ビットの情報があれば、以下の3つの状態を表現できるよね。

  • 00: IN_PROGRESS (進行中)
  • 01: COMMITTED (コミット済み)
  • 10: ABORTED (アボート済み)
  • (11: 未使用、または特定の文脈で使われることもある)

つまり、非常にコンパクトな形で膨大なトランザクションの状態を管理しているんだ。これがディスクI/Oを抑え、高速なトランザクション状態の参照を可能にしているってわけだ。

なぜCLOGを知っておくべきか?

「そんな細かいこと、知らなくても普段PostgreSQL使えるじゃん?」って思うかもしれない。確かにそうだ。だけど、一歩踏み込んで、こんな時に役立つんだ。

1. 障害調査: 例えば、データベースがクラッシュして復旧する際、CLOGの情報は非常に重要になる。WAL(Write-Ahead Log)と連携して、どのトランザクションがコミット済みで、どれがロールバックされるべきかを判断するのに使われるんだ。
2. パフォーマンスチューニングのヒント: 長時間のトランザクションが頻繁に発生すると、CLOGが肥大化しやすくなる。CLOGが大きくなると、`VACUUM` の処理に影響が出たり、ディスクI/Oが増えたりする可能性がある。CLOGの概念を知っていると、「あれ、最近データベース重いな。長時間のトランザクションないかな?」って疑うきっかけになる。
3. VACUUMの理解: `VACUUM` は、不要になったタプル(行)を削除するだけでなく、CLOGの古いエントリをクリーンアップする役割も持っている。この知識があると、なぜ `VACUUM` が重要なのか、より深く理解できるはずだ。

ぶっちゃけ、普段CLOGファイルを直接いじることはまずない。でも、その存在と役割を頭に入れておくことは、PostgreSQLを「ブラックボックス」ではなく「透明な箱」として扱う上で、本当に価値があるんだ。

—

サブトランザクション、影の立役者

さて、CLOGの話でトランザクションの基本状態がわかったところで、次は「サブトランザクション」について見ていこう。これは実務で複雑なビジネスロジックを扱う際に、めちゃくちゃ役立つテクニックなんだ。

サブトランザクションとは何か?

サブトランザクションとは、一言で言えば「トランザクションの中のトランザクション」だ。SQLの `SAVEPOINT` コマンドを使って開始され、その `SAVEPOINT` まで部分的にロールバックできる機能を提供する。

例えば、こんな状況を想像してみてほしい。

「ユーザーが商品を注文する」という一連の処理の中で、
1. 注文ヘッダの登録
2. 注文明細の登録(複数行)
3. 在庫の引き当て
4. 支払い処理
というステップがあるとする。

もし明細の登録中にエラーが発生したら、注文ヘッダまで全部ロールバックして、最初からやり直し? いやいや、それは非効率だよね。明細の登録だけをやり直したい、あるいはエラーになった明細だけをスキップして、他の明細やヘッダはそのままコミットしたい、なんてケースもあるだろう。

ここでサブトランザクションの出番だ。

具体的なSQLコード例

BEGIN; — メイントランザクション開始

— 1. 注文ヘッダの登録
INSERT INTO orders (user_id, total_amount) VALUES (1, 0) RETURNING id INTO main_order_id;
— main_order_id を取得

— 2. 注文明細の登録(複数行を想定)
— ここからサブトランザクションの出番!
SAVEPOINT item_insert_point;

BEGIN; — これは不要、SAVEPOINTがサブトランザクションを開始する
— INSERT INTO order_items …
— もしここでエラーが発生したら…
ROLLBACK TO SAVEPOINT item_insert_point;
— あるいは、エラー処理をして、再度試行したり、別の処理に進んだり

— 別の明細の登録
SAVEPOINT another_item_point;
INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (main_order_id, 101, 2, 100);
— もしここでエラーが発生したら…
ROLLBACK TO SAVEPOINT another_item_point;

— エラーがなければ、次の処理へ
— 3. 在庫の引き当て
UPDATE products SET stock = stock – 2 WHERE id = 101;

— 4. 支払い処理 (外部システム連携など)
— ここも失敗する可能性があるなら、さらにSAVEPOINTを置くこともできる

COMMIT; — メイントランザクションのコミット

このように `SAVEPOINT` を使うことで、トランザクション内の特定のポイントまで処理を巻き戻すことができるんだ。これは、複雑なビジネスロジックを、より堅牢に、かつ柔軟に実装するための強力なツールになる。

CLOGとサブトランザクションの「つながり」

さて、ここが今回の記事のミソの一つだ。サブトランザクションも独自のトランザクションID(XID)を持つんだけど、じゃあ、その状態もCLOGに記録されるのか?

答えは「直接は記録されない」なんだ。

どういうことかというと、サブトランザクションのコミット状態は、親となるメイントランザクションのコミット状態に依存するんだ。

PostgreSQLは、`pg_xact` ディレクトリにある `pg_subtrans` というファイルを使って、トランザクションIDとその親トランザクションIDのマッピングを管理している。

  • サブトランザクションが開始されると、新しいXIDが割り当てられ、そのXIDと親XIDの関連が `pg_subtrans` に記録される。
  • このとき、サブトランザクション自身のCLOGエントリは `IN_PROGRESS` とはマークされない。
  • `ROLLBACK TO SAVEPOINT` が実行されると、そのサブトランザクション以降に行われた変更はロールバックされる。この際も、CLOGの状態は変化しない(親トランザクションのCLOGエントリは `IN_PROGRESS` のまま)。
  • 親トランザクションが `COMMIT` されると、その親XIDのCLOGエントリが `COMMITTED` になる。この時、`pg_subtrans` を参照し、その親XIDを持つ全ての子サブトランザクションも `COMMITTED` とみなされる。
  • 親トランザクションが `ABORT` されると、その親XIDのCLOGエントリが `ABORTED` になる。同様に、`pg_subtrans` を参照し、全ての子サブトランザクションも `ABORTED` とみなされる。

つまり、サブトランザクションは、CLOG上では親トランザクションの「一部」として扱われるんだ。独立した存在ではなく、親に寄り添ってその運命を共にする、そんなイメージだね。

サブトランザクションのメリット・デメリット

メリット

  • 柔軟なエラーハンドリング: トランザクション全体を巻き戻すことなく、部分的なエラーから回復できる。
  • 複雑な処理の安全な実行: 複数のステップからなる処理で、各ステップを個別に検証し、失敗した場合に特定のポイントまで戻れる。
  • リトライ処理の効率化: 特定の処理が失敗した場合、その部分だけを再実行するロジックを組み込みやすい。

デメリット

  • オーバーヘッド: サブトランザクションを多用すると、`SAVEPOINT` の設定や `ROLLBACK TO SAVEPOINT` の処理に若干のオーバーヘッドが生じる可能性がある。特にネストが深くなると影響が出やすい。
  • 複雑さ: ロジックが複雑になりがち。どの `SAVEPOINT` に戻るべきか、どの部分がコミットされるべきかをしっかり設計しないと、かえってバグの温床になることも。
  • リソース消費: アクティブな `SAVEPOINT` が多いと、一時的に消費するメモリやディスクリソースが増えることがある。

—

CLOGとサブトランザクションを実務でどう活かすか

ここまでCLOGとサブトランザクションの仕組みを見てきたけど、じゃあこれを日々の業務でどう活かしていくか?

長時間トランザクションとの付き合い方

CLOGの観点から言うと、長時間のトランザクションは避けるべき、というのが僕からのアドバイスだ。

長時間トランザクションが続くと、そのトランザクションIDが `IN_PROGRESS` のままずっとCLOGに残ることになる。これが原因で、`VACUUM` が古いトランザクションIDを参照するタプルをクリーンアップできなくなり、テーブルの肥大化や、最悪の場合トランザクションIDの周回問題(Transaction ID wraparound)を引き起こす可能性がある。

もしどうしても長時間かかる処理が必要な場合は、細かくトランザクションを区切るか、サブトランザクションを駆使して、できるだけ影響範囲を限定することを検討してほしい。

複雑なバッチ処理やAPI設計での活用

僕は以前、ECサイトの注文処理でサブトランザクションを多用したことがある。支払い処理など、外部連携が入る部分は失敗する可能性が高い。もし失敗しても、注文ヘッダは残しておいて、支払いだけリトライするような設計にしたかったんだ。

BEGIN;
— 注文ヘッダの登録
INSERT INTO orders …;

SAVEPOINT payment_attempt;
— 外部支払いAPIを呼び出し
BEGIN; — このBEGINは不要だが、処理ブロックとして明示
— ここで例外が発生する可能性
SELECT do_external_payment_api_call(…);
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO SAVEPOINT payment_attempt;
— 支払い失敗ログを記録
INSERT INTO payment_logs (order_id, status, error_message) VALUES (…, ‘FAILED’, SQLERRM);
— 支払い失敗時の代替処理や、ユーザーへの通知など
— この時点ではまだメイントランザクションは有効
— アプリケーション側で支払い失敗フラグを立てておくなど
END;

— 注文明細の登録など、他の処理
INSERT INTO order_items …;

COMMIT;

このように、部分的な失敗を許容し、全体としてはコミットしたい、というシナリオでサブトランザクションは真価を発揮する。ただし、`ROLLBACK TO SAVEPOINT` が発生した場合に、アプリケーション側でどういう状態遷移をさせるか、きちんと設計しておくことが重要だ。

トランザクションIDの監視

CLOGやサブトランザクションの理解を深めると、`pg_stat_activity` や `pg_locks` で表示されるトランザクションID(XID)の見方も変わってくるはずだ。

  • `SELECT pid, usename, application_name, state, xact_start, query FROM pg_stat_activity WHERE state = ‘active’;`
  • `SELECT FROM pg_locks;`

これらの情報から、どのトランザクションが長時間実行されているか、どのトランザクションが他のトランザクションをブロックしているか、といったことを読み解く際に、CLOGが管理する「トランザクションの状態」という概念が頭にあると、より深く分析できるようになる。

—

まとめ

今日はPostgreSQLのコアな部分、CLOGとサブトランザクションについて話してきた。

  • CLOG は、各トランザクションのコミット状態(IN_PROGRESS, COMMITTED, ABORTED)を管理する、PostgreSQLのMVCCを支える重要な仕組みだ。`pg_xact` ディレクトリにその実体があり、非常にコンパクトに状態を記録している。
  • サブトランザクション は、`SAVEPOINT` を使ってトランザクション内部で部分的なロールバックを可能にする機能だ。複雑なビジネスロジックやエラーハンドリングに非常に有効。
  • サブトランザクション自体はCLOGに直接状態を持たず、親トランザクションのCLOG状態と `pg_subtrans` を介してその運命を決定される。

これらの知識は、普段の開発ではあまり意識しないかもしれない。だけど、一歩踏み込んでデータベースの「なぜ?」を考える時、そしてトラブルシューティングやパフォーマンス改善に取り組む時に、必ず君の力になるはずだ。

PostgreSQLの奥深さは、こういう細部にこそ宿っている。これからも一緒に、この素晴らしいデータベースを深く探求していこうじゃないか!

じゃあ、また次の記事で会おう!

コメント

タイトルとURLをコピーしました