シェア

はじめに

トランザクションは、複数のステートメントをデータベースが処理するための単一の操作にカプセル化するメカニズムです。個々のステートメントを投入する代わりに、データベースはコマンドのグループをまとまりのあるユニットとして解釈し、操作できます。これは、多くの密接に関連するステートメントの過程でデータセットの整合性を確保するのに役立ちます。

このガイドでは、最初にトランザクションとは何か、そしてトランザクションがなぜ有益なのかについて説明します。その後、PostgreSQL がトランザクションをどのように実装しているか、およびトランザクションを使用する際のさまざまなオプションを見ていきます。

トランザクションとは?

トランザクションは、複数のステートメントをグループ化して分離し、単一の操作として処理する方法です。トランザクションでは、各コマンドをサーバーに送信された順に個別に実行するのではなく、コマンドをまとめてバンドルし、他のリクエストとは別のコンテキストで実行します。

分離はトランザクションの重要な部分です。トランザクション内では、実行されたステートメントはトランザクション自体の内部環境にのみ影響を与えることができます。トランザクション内から、ステートメントはデータを変更でき、結果はすぐに表示されます。外部からは、トランザクションがコミットされるまで変更は行われず、コミットされると、トランザクション内のすべてのアクションが一度に表示されるようになります。

これらの機能は、ACID 準拠を達成するのに役立ちます。原子性(トランザクション内のアクションはすべてコミットされるか、すべてロールバックされるかのいずれか)と分離性(トランザクションの外部では、コミットされるまで何も変更されませんが、内部ではステートメントに結果が生じます)を提供することで実現します。これらは合わせて、データベースが整合性を維持するのを助けます(部分的なデータ変換が発生しないことを保証することによって)。さらに、トランザクションの変更は、不揮発性ストレージにコミットされるまで成功として返されないため、永続性が提供されます。

これらの目標を達成するために、トランザクションは多くの異なる戦略を採用しており、異なるデータベースシステムは異なる方法を使用しています。PostgreSQL は、多版同時実行制御 (MVCC) と呼ばれるシステムを使用しており、データスナップショットを使用して、不要なロックなしでこれらのアクションを実行できます。これらすべてのシステムを合わせると、最新のリレーショナルデータベースの基本的な構成要素の 1 つとなり、クラッシュ耐性のある方法で複雑なデータを安全に処理できるようになります。

整合性障害の種類

人々がトランザクションを使用する理由の 1 つは、データとその処理環境の整合性について特定の保証を得るためです。整合性はさまざまな方法で損なわれる可能性があり、データベースがそれらをどのように防止しようとするかに影響を与えます。

トランザクションの実装に応じて、整合性が損なわれる可能性のある主な方法が 4 つあります。これらのシナリオが発生する可能性に対する許容度が、アプリケーションでトランザクションをどのように使用するかに影響を与えます。

ダーティリード

ダーティリードは、トランザクション内のステートメントが、他の進行中のトランザクションによって書き込まれたデータを読み取ることができる場合に発生します。これは、トランザクションのステートメントがまだコミットされていない場合でも、それらを読み取ることができ、したがって他のトランザクションに影響を与える可能性があることを意味します。

これは、トランザクションが互いに適切に分離されていないため、整合性の重大な侵害と見なされることがよくあります。データベースにコミットされない可能性のあるステートメントが、他のトランザクションの実行に影響を与え、その動作を変更する可能性があります。

ダーティリードを許可するトランザクションは、結果のデータの整合性について合理的な主張をすることはできません。

非リピータブルリード

非リピータブルリードは、トランザクションの外部でのコミットがトランザクション内で見られるデータを変更した場合に発生します。トランザクション内で、同じデータが 2 回読み取られるが、各インスタンスで異なる値が取得される場合、このタイプの問題であると認識できます。

ダーティリードと同様に、非リピータブルリードを許可するトランザクションは、トランザクション間の完全な分離を提供しません。違いは、非リピータブルリードでは、トランザクションに影響を与えるステートメントがトランザクションの外部で実際にコミットされていることです。

ファントムリード

ファントムリードは、トランザクション内でクエリが 2 回実行されたときに、2 回目に返される行が異なる場合に発生する、非リピータブルリードの特定のタイプです。

たとえば、トランザクション内のクエリが最初に実行されたときに 4 行を返し、2 回目に 5 行を返す場合、これはファントムリードです。ファントムリードは、クエリを満たす行数を変更するトランザクションの外部でのコミットによって発生します。

シリアライゼーション異常

シリアライゼーション異常は、同時にコミットされた複数のトランザクションの結果が、それらが 1 つずつコミットされた場合とは異なる結果になる場合に発生します。これは、トランザクションが競合を解決せずに、それぞれが同じテーブルまたはデータを変更する 2 つのコミットを許可する場合にいつでも発生する可能性があります。

シリアライゼーション異常は、初期のトランザクションタイプが理解していなかった特別なタイプの問題です。これは、初期のトランザクションがロックで実装されていたためです。ロックでは、別のトランザクションが同じデータを読み取ったり変更したりしている場合、続行できませんでした。

トランザクション分離レベル

トランザクションは「万能」ソリューションではありません。さまざまなシナリオでは、パフォーマンスと保護の間で異なるトレードオフが必要です。幸いなことに、PostgreSQL では、必要なトランザクション分離のタイプを指定できます。

ほとんどのデータベースシステムで提供される分離レベルには、以下が含まれます。

リードアンコミット

リードアンコミットは、データ整合性と分離性の維持について最も少ない保証を提供する分離レベルです。read uncommitted を使用するトランザクションは、複数のステートメントを一度にコミットしたり、間違いが発生した場合にステートメントをロールバックしたりする機能など、トランザクションに頻繁に関連付けられる特定の機能を備えていますが、整合性が損なわれる可能性のある多数の状況を許可しています。

read uncommitted 分離レベルで構成されたトランザクションは、以下を許可します。

  • ダーティリード
  • 非リピータブルリード
  • ファントムリード
  • シリアライゼーション異常

このレベルの分離は、実際には PostgreSQL には実装されていません。PostgreSQL は分離レベル名を認識しますが、内部的には実際にはサポートされておらず、「リードコミット」(下記参照)が代わりに使用されます。

リードコミット

リードコミットは、ダーティリードから具体的に保護する分離レベルです。トランザクションが read committed レベルの整合性を使用する場合、コミットされていないデータがトランザクションの内部コンテキストに影響を与えることはありません。これにより、コミットされていないデータがトランザクションに影響を与えないようにすることで、基本的なレベルの整合性が提供されます。

read committedread uncommitted よりも優れた保護を提供しますが、すべてのタイプの非整合性から保護するわけではありません。これらの問題は依然として発生する可能性があります。

  • 非リピータブルリード
  • ファントムリード
  • シリアライゼーション異常

PostgreSQL は、他の分離レベルが指定されていない場合、デフォルトで read committed レベルを使用します。

リピータブルリード

リピータブルリード分離レベルは、read committed によって提供される保証に基づいて構築されています。以前と同様にダーティリードを回避しますが、非リピータブルリードも防止します。

これは、トランザクションの外部でコミットされた変更が、トランザクション内で読み取られたデータに影響を与えないことを意味します。トランザクションの開始時に実行されたクエリは、トランザクション内のステートメントによって直接引き起こされない限り、トランザクションの終了時に異なる結果になることはありません。

repeatable read 分離レベルの標準的な定義では、ダーティリードと非リピータブルリードのみが防止される必要がありますが、PostgreSQL はこのレベルでもファントムリードを防止します。これは、トランザクションの外部でのコミットが、クエリを満たす行数を変更できないことを意味します。

トランザクション内で見られるデータの状態がデータベース内の最新データと異なる可能性があるため、2 つのデータセットを調整できない場合、トランザクションはコミットに失敗する可能性があります。このため、この分離レベルの欠点の 1 つは、コミット時にシリアライゼーションエラーが発生した場合に、トランザクションを再試行する必要がある場合があることです。

PostgreSQL の repeatable read 分離レベルは、ほとんどのタイプの整合性問題をブロックしますが、シリアライゼーション異常は依然として発生する可能性があります。

シリアライザブル

シリアライザブル分離レベルは、最高レベルの分離と整合性を提供します。repeatable read レベルが行うすべてのシナリオを防止すると同時に、シリアライゼーション異常の可能性も排除します。

シリアライザブル分離は、同時トランザクションが 1 つずつ実行されたかのようにコミットされることを保証します。シリアライゼーション異常が発生する可能性のあるシナリオが発生した場合、データセットに非整合性を導入する代わりに、トランザクションの 1 つでシリアライゼーションエラーが発生します。

トランザクションの定義

PostgreSQL がトランザクションで使用できるさまざまな分離レベルについて説明したので、トランザクションを定義する方法を示しましょう。

PostgreSQL では、明示的にマークされたトランザクションのすべてのステートメントは、実際には独自の単一ステートメントトランザクションで実行されます。トランザクションブロックを明示的に開始するには、BEGIN または START TRANSACTION コマンド(同義です)を使用できます。トランザクションをコミットするには、COMMIT コマンドを発行します。

したがって、トランザクションの基本的な構文は次のようになります。

BEGIN;
statements
COMMIT;

より具体的な例として、ある口座から別の口座に 1000 ドルを振り込もうとしていると想像してください。お金は常に 2 つの口座のいずれかにあり、両方にあることは決してないようにしたいと考えています。

この振込をカプセル化する 2 つのステートメントを、次のようなトランザクションでラップできます。

BEGIN;
UPDATE accounts
SET balance = balance - 1000
WHERE id = 1;
UPDATE accounts
SET balance = balance + 1000
WHERE id = 2;
COMMIT;

ここでは、id = 2 の口座に 1000 ドルを入金せずに、id = 1 の口座から 1000 ドルを引き出すことはありません。これらの 2 つのステートメントはトランザクションで順番に実行されますが、コミットされるため、基になるデータセットで同時に実行されます。

トランザクションのロールバック

トランザクション内では、ステートメントのすべてまたはNoneがデータベースにコミットされます。トランザクション内で行われたステートメントと変更をデータベースに適用する代わりに放棄することは、「トランザクションのロールバック」として知られています。

トランザクションは、自動または手動でロールバックできます。PostgreSQL は、トランザクション内のステートメントの 1 つがエラーになった場合、トランザクションを自動的にロールバックします。また、選択された分離レベルで許可されていない場合にシリアライゼーションエラーが発生した場合も、トランザクションをロールバックします。

現在のトランザクション中に与えられたステートメントを手動でロールバックするには、ROLLBACK コマンドを使用できます。これにより、トランザクション内のすべてのステートメントがキャンセルされ、本質的にトランザクションの開始時に時計が戻ります。

たとえば、以前に使用していた同じ銀行口座の例を使用していると仮定して、UPDATE ステートメントを発行した後で、誤って間違った金額を振り込んだり、間違った口座を使用したりしたことが判明した場合、変更をコミットする代わりにロールバックできます。

BEGIN;
UPDATE accounts
SET balance = balance - 1500
WHERE id = 1;
UPDATE accounts
SET balance = balance + 1500
WHERE id = 3; -- Wrong account number here! Must rollback
/* Gets us back to where we were before the transaction started */
ROLLBACK;

ROLLBACK を実行すると、1500 ドルは id = 1 の口座に残ります。

ロールバック時のセーブポイントの使用

デフォルトでは、ROLLBACK コマンドは、BEGIN または START TRANSACTION コマンドが最初に呼び出された時点までトランザクションをリセットします。しかし、トランザクション内の一部のステートメントのみを元に戻したい場合はどうでしょうか?

ROLLBACK コマンドを発行するときにロールバックする任意の場所を指定することはできませんが、トランザクション全体で設定した「セーブポイント」にはロールバックできます。トランザクション内の場所を SAVEPOINT コマンドで事前にマークし、ロールバックする必要がある場合は、それらの特定の場所を参照できます。

これらのセーブポイントを使用すると、中間ロールバックポイントを作成できます。次に、必要に応じて、現在位置からセーブポイントまでの間に行われたステートメントを元に戻し、トランザクションの作業を続行できます。

セーブポイントを指定するには、SAVEPOINT コマンドの後にセーブポイントの名前を発行します。

SAVEPOINT save_1;

そのセーブポイントにロールバックするには、ROLLBACK TO コマンドを使用します。

ROLLBACK TO save_1;

これまで使用してきた口座中心の例を続けましょう。

BEGIN;
UPDATE accounts
SET balance = balance - 1500
WHERE id = 1;
/* Set a save point that we can return to */
SAVEPOINT save_1;
UPDATE accounts
SET balance = balance + 1500
WHERE id = 3; -- Wrong account number here! We can rollback to the save point though!
/* Gets us back to the state of the transaction at `save_1` */
ROLLBACK TO save_1;
/* Continue the transaction with the correct account number */
UPDATE accounts
SET balance = balance + 1500
WHERE id = 4;
COMMIT;

ここでは、トランザクションでこれまでに行ったすべての作業を失うことなく、犯した間違いから回復できます。ロールバック後、正しいステートメントを使用して、計画どおりにトランザクションを続行します。

トランザクションの分離レベルの設定

トランザクションに必要な分離レベルを設定するには、ISOLATION LEVEL 句を START TRANSACTION または BEGIN コマンドに追加できます。基本的な構文は次のようになります。

BEGIN ISOLATION LEVEL <isolation_level>;
statements
COMMIT;

<isolation_level> は、次のいずれかになります(詳細は前述)。

  • READ UNCOMMITTED (このレベルは PostgreSQL に実装されていないため、READ COMMITTED になります)
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

SET TRANSACTION コマンドは、トランザクション開始後に分離レベルを設定するためにも使用できます。ただし、クエリまたはデータ変更コマンドが実行される前にのみ SET TRANSACTION を使用できるため、柔軟性は向上しません。

トランザクションのチェーン

順番に実行する必要がある複数のトランザクションがある場合は、COMMIT AND CHAIN コマンドを使用して、それらを任意にチェーンできます。

COMMIT AND CHAIN コマンドは、現在のトランザクションを完了し、ステートメントをコミットします。コミットが処理された後、すぐに新しいトランザクションを開きます。これにより、別のステートメントのセットをトランザクションにグループ化できます。

ステートメントは、COMMIT; BEGIN を発行した場合とまったく同じように機能します。

BEGIN;
UPDATE accounts
SET balance = balance - 1500
WHERE id = 1;
UPDATE accounts
SET balance = balance + 1500
WHERE id = 2;
/* Commit the data and start a new transaction that will take into account the committed from the last transaction */
COMMIT AND CHAIN;
UPDATE accounts
SET balance = balance - 1000
WHERE id = 2;
UPDATE accounts
SET balance = balance + 1000
WHERE id = 3;
COMMIT;

トランザクションのチェーンは、新しい機能という点ではあまり提供しませんが、同じタイプの操作に焦点を当て続けながら、自然な境界でデータをコミットするのに役立ちます。

結論

トランザクションは万能薬ではありません。さまざまな分離レベルに伴う多くのトレードオフがあり、保護する必要のある整合性のタイプを理解するには、思考と計画が必要です。これは、基になるデータが大幅に変更され、他の同時トランザクションとの競合の可能性が高まる長時間実行トランザクションでは特に当てはまります。

そうは言っても、トランザクションの仕組みは非常に柔軟性と強力さを提供します。相互に関連する同時実行の操作を実行している場合でも、ACID保証が維持されるように大きく貢献します。複雑で安全な操作を実行するためにトランザクションを適切にいつ、どのように使用するかを知ることは非常に重要です。

著者について
Justin Ellingwood

Justin Ellingwood

Justinは2013年からデータベース、Linux、インフラストラクチャ、および開発者ツールについて執筆しています。彼は現在、妻と2羽のウサギと共にベルリンに住んでいます。彼は普段三人称で書く必要がないため、関係者全員にとってありがたいことです。