はじめに
トランザクションは、複数のステートメントをデータベースが処理する単一の操作としてカプセル化するメカニズムです。個別のステートメントを投入する代わりに、データベースはコマンドのグループをまとまりのある単位として解釈し、実行することができます。これにより、密接に関連する多くのステートメントの過程で、データセットの一貫性を確保できます。
このガイドでは、まずトランザクションとは何か、そしてなぜそれが有益なのかについて説明します。その後、PostgreSQLがトランザクションをどのように実装しているか、そしてそれらを使用する際の様々なオプションを見ていきます。
トランザクションとは?
トランザクションは、複数のステートメントを単一の操作として処理するためにグループ化および分離する方法です。各コマンドがサーバーに送信されるたびに個別に実行するのではなく、トランザクションでは、コマンドがまとめてバンドルされ、他のリクエストとは別のコンテキストで実行されます。
分離はトランザクションの重要な要素です。トランザクション内では、実行されたステートメントはトランザクション自体の環境のみに影響を与えます。トランザクションの内部からは、ステートメントはデータを変更でき、その結果はすぐに表示されます。外部からは、トランザクションがコミットされるまで変更は行われず、コミットされた時点でトランザクション内のすべてのアクションが一度に可視化されます。
これらの機能は、原子性(トランザクション内のアクションはすべてコミットされるか、すべてロールバックされるかのいずれかである)および分離性(トランザクションの外部ではコミットされるまで何も変更されず、内部ではステートメントが結果をもたらす)を提供することで、データベースがACID準拠を達成するのに役立ちます。これらが相まって、データベースが一貫性を維持するのに役立ちます(部分的なデータ変換が発生しないことを保証することで)。さらに、トランザクション内の変更は、不揮発性ストレージにコミットされるまで成功として返されず、これが永続性を提供します。
これらの目標を達成するために、トランザクションはさまざまな戦略を採用しており、異なるデータベースシステムは異なるメソッドを使用しています。PostgreSQLは多版型同時実行制御 (MVCC) と呼ばれるシステムを使用しており、これによりデータベースはデータスナップショットを使用して不要なロックなしでこれらのアクションを実行できます。これらすべてを合わせると、これらのシステムは現代のリレーショナルデータベースの基本的な構成要素の1つを形成し、クラッシュ耐性のある方法で複雑なデータを安全に処理することを可能にします。
一貫性の失敗の種類
人々がトランザクションを使用する理由の1つは、データの整合性やデータが処理される環境について特定の保証を得るためです。整合性はさまざまな方法で損なわれる可能性があり、それがデータベースがそれらを防ぐ方法に影響を与えます。
トランザクションの実装方法によって、不整合が発生する主な方法は4つあります。これらのシナリオが発生する可能性のある状況に対する許容度が、アプリケーションでトランザクションを使用する方法に影響します。
ダーティリード
ダーティリードは、トランザクション内のステートメントが、他の進行中のトランザクションによって書き込まれたデータを読み取ることができる場合に発生します。これは、トランザクションのステートメントがまだコミットされていないにもかかわらず、読み取ることができ、他のトランザクションに影響を与える可能性があることを意味します。
トランザクションが互いに適切に分離されていないため、これはしばしば深刻な一貫性の侵害と見なされます。データベースにコミットされない可能性のあるステートメントが、他のトランザクションの実行に影響を与え、その動作を変更する可能性があります。
ダーティリードを許可するトランザクションは、結果データの整合性について合理的な主張をすることはできません。
再現不能リード
再現不能リードは、トランザクション外部からのコミットによって、トランザクション内で参照されるデータが変更される場合に発生します。この種の問題は、トランザクション内で同じデータが2回読み取られたときに、各インスタンスで異なる値が取得される場合に認識できます。
ダーティリードと同様に、再現不能リードを許可するトランザクションは、トランザクション間の完全な分離を提供しません。違いは、再現不能リードの場合、トランザクションに影響を与えるステートメントが実際にトランザクションの外部でコミットされていることです。
ファントムリード
A ファントムリードは、トランザクション内でクエリが2回実行されたときに、返される行が異なる場合に発生する特定のタイプの再現不能リードです。
例えば、トランザクション内のクエリが最初に実行されたときに4行を返し、2回目に5行を返した場合、これはファントムリードです。ファントムリードは、トランザクション外部からのコミットがクエリを満たす行数を変更することによって発生します。
直列化異常
直列化異常は、複数のトランザクションが同時にコミットされた結果が、それらが一つずつコミットされた場合とは異なる結果になる場合に発生します。これは、トランザクションが競合を解決せずに同じテーブルまたはデータをそれぞれ変更する2つのコミットを許可するたびに発生する可能性があります。
直列化異常は、初期のトランザクションでは理解されていなかった特殊な問題です。これは、初期のトランザクションがロックによって実装されており、別のトランザクションが同じデータを読み取ったり変更したりしている場合、処理を続行できなかったためです。
トランザクション分離レベル
トランザクションは「万能」な解決策ではありません。さまざまなシナリオでは、パフォーマンスと保護の間で異なるトレードオフが必要です。幸いなことに、PostgreSQLは必要なトランザクション分離のタイプを指定できます。
ほとんどのデータベースシステムが提供する分離レベルは以下のとおりです。
読み取り未コミット
読み取り未コミットは、データの整合性と分離を維持するための保証が最も少ない分離レベルです。read uncommitted
を使用するトランザクションは、複数のステートメントを一度にコミットする機能や、間違いが発生した場合にステートメントをロールバックする機能など、トランザクションによく関連付けられる特定の機能を備えていますが、整合性が破られる可能性のある多くの状況を許可します。
read uncommitted
分離レベルで設定されたトランザクションは、以下を許可します。
- ダーティリード
- 再現不能リード
- ファントムリード
- 直列化異常
この分離レベルは、実際にはPostgreSQLでは実装されていません。PostgreSQLは分離レベル名を認識していますが、内部的には実際にはサポートされておらず、「読み取りコミット済み (read committed)」(後述)が代わりに使用されます。
読み取りコミット済み
読み取りコミット済みは、ダーティリードから具体的に保護する分離レベルです。トランザクションがread committed
レベルの一貫性を使用する場合、未コミットのデータがトランザクションの内部コンテキストに影響を与えることはありません。これにより、未コミットのデータがトランザクションに影響を与えることがないようにすることで、基本的な一貫性レベルが提供されます。
read committed
はread uncommitted
よりも優れた保護を提供しますが、すべての種類の不整合から保護するわけではありません。これらの問題は依然として発生する可能性があります。
- 再現不能リード
- ファントムリード
- 直列化異常
他の分離レベルが指定されていない場合、PostgreSQLはデフォルトでread committed
レベルを使用します。
再現可能リード
再現可能リード分離レベルは、read committed
によって提供される保証を基盤としています。以前と同様にダーティリードを回避し、さらに再現不能リードも防止します。
これは、トランザクション外でコミットされた変更が、トランザクション内で読み取られるデータに影響を与えることはないことを意味します。トランザクションの開始時に実行されたクエリは、トランザクション内のステートメントによって直接引き起こされない限り、トランザクションの終了時に異なる結果になることはありません。
repeatable read
分離レベルの標準的な定義では、ダーティリードと再現不能リードの防止のみが求められていますが、PostgreSQLはこのレベルでファントムリードも防止します。これは、トランザクション外部からのコミットがクエリを満たす行数を変更できないことを意味します。
トランザクション内で参照されるデータの状態がデータベースの最新データと異なる可能性があるため、2つのデータセットを調整できない場合、コミット時にトランザクションが失敗する可能性があります。このため、この分離レベルの欠点の1つは、コミット時に直列化の失敗があった場合、トランザクションを再試行する必要があるかもしれないことです。
PostgreSQLのrepeatable read
分離レベルは、ほとんどのタイプの一貫性の問題をブロックしますが、直列化異常は依然として発生する可能性があります。
直列化可能
直列化可能分離レベルは、最高レベルの分離と一貫性を提供します。repeatable read
レベルが行うすべてのシナリオを防ぎ、同時に直列化異常の可能性も排除します。
直列化可能分離は、並行するトランザクションがまるで一つずつ実行されたかのようにコミットされることを保証します。直列化異常が導入される可能性のあるシナリオが発生した場合、データセットに不整合を導入する代わりに、いずれかのトランザクションで直列化の失敗が発生します。
トランザクションの定義
PostgreSQLがトランザクションで使用できるさまざまな分離レベルについて説明したので、トランザクションの定義方法を見ていきましょう。
PostgreSQLでは、明示的にマークされたトランザクション外部のすべてのステートメントは、実際にはそれ自身の単一ステートメントトランザクションとして実行されます。トランザクションブロックを明示的に開始するには、BEGIN
またはSTART TRANSACTION
コマンド(これらは同義です)のいずれかを使用できます。トランザクションをコミットするには、COMMIT
コマンドを発行します。
したがって、トランザクションの基本的な構文は次のようになります。
BEGIN;statementsCOMMIT;
より具体的な例として、ある口座から別の口座へ1000ドルを送金しようとしていると想像してください。私たちは、そのお金が常に2つの口座のいずれか一方にあり、両方にあることは決してないことを確実にしたいと考えています。
この送金をカプセル化する2つのステートメントを、次のようなトランザクションにまとめることができます。
BEGIN;UPDATE accountsSET balance = balance - 1000WHERE id = 1;UPDATE accountsSET balance = balance + 1000WHERE id = 2;COMMIT;
ここでは、id = 1
の口座から1000ドルが引き出されることは、同時にid = 2
の口座に1000ドルが預け入れられない限りありません。これらの2つのステートメントはトランザクション内で順次実行されますが、同時にコミットされ、それによって基となるデータセット上で同時に実行されます。
トランザクションのロールバック
トランザクション内では、すべてのステートメントがデータベースにコミットされるか、何もコミットされないかのいずれかです。トランザクション内で行われたステートメントや変更をデータベースに適用する代わりに放棄することを、トランザクションの「ロールバック」と呼びます。
トランザクションは、自動的または手動でロールバックできます。PostgreSQLは、内部のステートメントのいずれかがエラーになった場合、トランザクションを自動的にロールバックします。また、選択された分離レベルが許可しない場合、直列化エラーが発生する可能性があればトランザクションをロールバックします。
現在のトランザクション中に与えられたステートメントを手動でロールバックするには、ROLLBACK
コマンドを使用できます。これにより、トランザクション内のすべてのステートメントがキャンセルされ、実質的にトランザクションの開始時点に時間を巻き戻します。
例えば、以前使用した銀行口座の例を使い続けると仮定して、UPDATE
ステートメントを発行した後に、誤って間違った金額を送金してしまったり、間違った口座を使用してしまったりしたことが判明した場合、変更をコミットする代わりにロールバックすることができます。
BEGIN;UPDATE accountsSET balance = balance - 1500WHERE id = 1;UPDATE accountsSET balance = balance + 1500WHERE 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 accountsSET balance = balance - 1500WHERE id = 1;/* Set a save point that we can return to */SAVEPOINT save_1;UPDATE accountsSET balance = balance + 1500WHERE 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 accountsSET balance = balance + 1500WHERE id = 4;COMMIT;
ここでは、これまでのトランザクションで行ったすべての作業を失うことなく、犯した間違いから回復できます。ロールバック後、正しいステートメントを使用して計画通りにトランザクションを続行します。
トランザクションの分離レベルの設定
トランザクションに必要な分離レベルを設定するには、START TRANSACTION
またはBEGIN
コマンドにISOLATION LEVEL
句を追加します。基本的な構文は次のようになります。
BEGIN ISOLATION LEVEL <isolation_level>;statementsCOMMIT;
<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 accountsSET balance = balance - 1500WHERE id = 1;UPDATE accountsSET balance = balance + 1500WHERE 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 accountsSET balance = balance - 1000WHERE id = 2;UPDATE accountsSET balance = balance + 1000WHERE id = 3;COMMIT;
トランザクションの連結は、新しい機能という点ではあまり提供しませんが、同じ種類の操作に焦点を当てながら自然な境界でデータをコミットするのに役立ちます。
結論
トランザクションは万能薬ではありません。さまざまな分離レベルには多くのトレードオフがあり、保護する必要がある一貫性の種類を理解するには考察と計画が必要です。これは、基盤となるデータが大幅に変化し、他の並行トランザクションとの競合の可能性が増加するような、長時間実行されるトランザクションでは特に顕著です。
そうは言っても、トランザクションのメカニズムは多くの柔軟性と強力な機能を提供します。相互に関連する並行操作を実行している間でもACID保証が維持されることを大きく助けます。複雑で安全な操作を実行するために、いつ、どのようにトランザクションを適切に使用するかを知ることは非常に貴重です。
JavaScriptまたはTypeScriptを使用している場合、Prismaを使用してPostgreSQLデータベースを管理できます。トランザクションAPIを使用するすべての操作は、PostgreSQLサーバーのデフォルトの分離レベルを使用します。対話的にトランザクションを使用する代わりに、Prismaはネストされた書き込みやバルク操作またはバッチ操作を介してトランザクションの動作を提供します。詳しくはPrismaのトランザクションガイドを参照してください。