このページでは、Spanner で FOR UPDATE
句を使用する方法について説明します。
SELECT
クエリを使用してテーブルをスキャンする場合は、FOR UPDATE
句を追加して、行と列の粒度レベル(セルレベル)で排他ロックを有効にします。ロックは、読み取り / 書き込みトランザクションが存続している間、保持されます。この間、FOR UPDATE
句により、現在のトランザクションが完了するまで、他のトランザクションがロックされたセルを変更できなくなります。詳細については、GoogleSQL と PostgreSQL の FOR UPDATE
リファレンス ガイドをご覧ください。
FOR UPDATE
句を使用する理由
分離レベルが厳しくないデータベースでは、同時実行トランザクションがデータの読み取りとトランザクションの commit の間でデータを更新しないように、FOR UPDATE
句が必要になる場合があります。Spanner は常にシリアル化可能性を適用するため、トランザクション内でアクセスされたデータが commit 時に古くない場合にのみ、トランザクションが正常に commit されることが保証されます。したがって、Spanner でトランザクションの正確性を確保するために FOR UPDATE
句は必要ありません。
ただし、書き込みの競合が多いユースケース(複数のトランザクションが同じデータを同時に読み書きしている場合など)では、同時実行トランザクションにより中断が増加する可能性があります。これは、複数のトランザクションが同時に共有ロックを取得し、排他的ロックへのアップグレードを試み、トランザクションがデッドロックを引き起こすためです。その場合、Spanner は 1 つを除くすべてのトランザクションを中止します。詳細については、ロックをご覧ください。
FOR UPDATE
句を使用するトランザクションは排他ロックを取得して実行を続行しますが、他のトランザクションはロックの順番を待機します。競合するトランザクションは一度に 1 つしか実行できないため、Spanner ではスループットが制限される可能性があります。ただし、Spanner は 1 つのトランザクションの処理のみを進めるため、トランザクションの中止と再試行にかかる時間が短くなります。
したがって、同時書き込みリクエストのシナリオで中断されたトランザクションの数を減らすことが重要である場合は、FOR UPDATE
句を使用して、全体的な中断数を減らし、ワークロードの実行効率を高めることができます。
LOCK_SCANNED_RANGES
ヒントとの比較
FOR UPDATE
句は、LOCK_SCANNED_RANGES=exclusive
ヒントと同様の機能を果たします。
次のような主な違いが 2 つあります。
LOCK_SCANNED_RANGES
ヒントを使用すると、トランザクションはステートメント全体のスキャン範囲に対する排他ロックを取得します。サブクエリで排他ロックを取得することはできません。ロックヒントを使用すると、必要以上にロックが取得され、ワークロードでのロック競合につながる可能性があります。次の例は、ロックヒントの使用方法を示しています。@{lock_scanned_ranges=exclusive} SELECT s.SingerId, s.FullName FROM Singers AS s JOIN (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000) AS a ON a.SingerId = s.SingerId;
一方、次の例に示すように、サブクエリで
FOR UPDATE
句を使用できます。SELECT s.SingerId, s.FullName FROM Singers AS s JOIN (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000) FOR UPDATE AS a ON a.SingerId = s.SingerId;
DML ステートメントでは
LOCK_SCANNED_RANGES
ヒントを使用できますが、SELECT
ステートメントではFOR UPDATE
句のみを使用できます。
ロックのセマンティクス
同時書き込みリクエストと、デッドロックの結果としてトランザクションが中断されるコストを削減するため、Spanner は可能であればセルレベルでデータをロックします。FOR UPDATE
句を使用すると、Spanner は SELECT
クエリによってスキャンされる特定のセルをロックします。
次の例では、SingerId = 1
行と AlbumId = 1
行の MarketingBudget
セルが Albums
テーブルで排他的にロックされているため、このトランザクションが commit またはロールバックされるまで、同時実行トランザクションがそのセルを変更できなくなります。ただし、同時実行トランザクションは、その行の AlbumTitle
セルを更新できます。
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId = 1
FOR UPDATE;
ロックされたデータの読み取りで同時実行トランザクションがブロックされる可能性がある
1 つのトランザクションがスキャン範囲の排他ロックを取得すると、同時実行トランザクションによってそのデータの読み取りがブロックされる可能性があります。Spanner はシリアル化可能性を適用するため、トランザクションの存続期間内に別のトランザクションによって変更されないことが保証されている場合にのみ、データを読み取ることができます。すでにロックされているデータを読み取ろうとする同時実行トランザクションは、ロックを保持しているトランザクションが commit またはロールバックされるまで、待機しなければならない場合があります。
次の例では、Transaction 1
は 1 <= AlbumId < 5
の MarketingBudget
セルをロックします。
-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 5
FOR UPDATE;
AlbumId = 1
の MarketingBudget
の読み取りを試行している Transaction 2
は、Transaction 1
が commit されるかロールバックされるまでブロックされます。
-- Transaction 2
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId = 1;
-- Blocked by Transaction 1
同様に、FOR UPDATE
でスキャン範囲をロックしようとするトランザクションは、重複するスキャン範囲をロックする同時実行トランザクションによってブロックされます。
次の例の Transaction 3
もブロックされます。これは、Transaction 1
が 3 <= AlbumId < 5
の MarketingBudget
セルをロックしているためです。これは Transaction 3
と重複するスキャン範囲です。
-- Transaction 3
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 3 and AlbumId < 10
FOR UPDATE;
-- Blocked by Transaction 1
インデックスを読み込む
スキャンされた範囲をロックしたクエリがベーステーブル内の行をロックしても、同時実行トランザクションがインデックスから読み取る場合、同時実行読み取りがブロックされないことがあります。
次の Transaction 1
は、SingerId = 1
の SingerId
セルと SingerInfo
セルをロックします。
-- Transaction 1
SELECT SingerId, SingerInfo
FROM Singers
WHERE SingerId = 1
FOR UPDATE;
読み取り専用の Transaction 2
は、インデックス テーブルをクエリするため、Transaction 1
で取得されたロックによってブロックされません。
-- Transaction 2
SELECT SingerId FROM Singers;
同時実行トランザクションで、すでにロックされているデータに対する DML オペレーションがブロックされない
1 つのトランザクションが排他的ロックのヒントを使用してセル範囲のロックを確保した場合、ロックされたセルでデータを読み取らずに書き込みを実行しようとする同時実行トランザクションは処理を続行できます。ロックを保持しているトランザクションが commit またはロールバックされるまで、トランザクションは commit でブロックされます。
次の Transaction 1
は、1 <= AlbumId < 5
の MarketingBudget
セルをロックします。
-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 5
FOR UPDATE;
Transaction 2
が Albums
テーブルの更新を試みると、Transaction 1
が commit またはロールバックされるまで更新がブロックされます。
-- Transaction 2
UPDATE Albums
SET MarketingBudget = 200000
WHERE SingerId = 1 and AlbumId = 1;
> Query OK, 1 rows affected
COMMIT;
-- Blocked by Transaction 1
スキャン範囲がロックされると、既存の行とギャップがロックされる
1 つのトランザクションがスキャン範囲の排他ロックを取得すると、同時実行トランザクションはその範囲内のギャップにデータを挿入できません。
次の Transaction 1
は、1 <= AlbumId < 10
の MarketingBudget
セルをロックします。
-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 10
FOR UPDATE;
Transaction 2
がまだ存在しない AlbumId = 9
の行を挿入しようとすると、Transaction 1
が commit またはロールバックされるまで、その行の挿入はブロックされます。
-- Transaction 2
INSERT INTO Albums (SingerId, AlbumId, AlbumTitle, MarketingBudget)
VALUES (1, 9, "Hello hello!", 10000);
> Query OK, 1 rows affected
COMMIT;
-- Blocked by Transaction 1
ロックの取得に関する注意事項
説明されているロック セマンティクスは一般的なガイダンスを提供しますが、Spanner が FOR UPDATE
句を使用するトランザクションを実行するときにロックがどのように取得されるかを保証するものではありません。Spanner のクエリ最適化メカニズムも、取得されるロックに影響する可能性があります。この句により、現在のトランザクションが完了するまで、他のトランザクションがロックされたセルを変更できなくなります。
クエリのセマンティクス
このセクションでは、FOR UPDATE
句を使用する場合のクエリのセマンティクスについて説明します。
WITH ステートメントで使用する
WITH
ステートメントの外側レベルのクエリで FOR UPDATE
を指定するときに、FOR UPDATE
句は WITH
ステートメントのロックを取得しません。
次のクエリでは、ロックする意図が共通テーブル式(CTE)クエリに伝播されないため、Singers
テーブルでロックは取得されません。
WITH s AS (SELECT SingerId, SingerInfo FROM Singers WHERE SingerID > 5)
SELECT * FROM s
FOR UPDATE;
CTE クエリで FOR UPDATE
句が指定されている場合、CTE クエリのスキャン範囲でロックが取得されます。
次の例では、SingerId > 5
がロックされている行の SingerId
セルと SingerInfo
セルがロックされています。
WITH s AS
(SELECT SingerId, SingerInfo FROM Singers WHERE SingerId > 5 FOR UPDATE)
SELECT * FROM s;
サブクエリで使用する
FOR UPDATE
句は、1 つ以上のサブクエリを含む外側レベルのクエリで使用できます。ロックは、最上位クエリとサブクエリ内で取得されます(式サブクエリを除く)。
次のクエリは、SingerId > 5.
の SingerId
セルと SingerInfo
セルをロックします。
(SELECT SingerId, SingerInfo FROM Singers WHERE SingerId > 5) AS t
FOR UPDATE;
次のクエリは式サブクエリ内にあるため、Albums
テーブルのセルはロックされません。式サブクエリから返された行の SingerId
セルと SingerInfo
セルはロックされます。
SELECT SingerId, SingerInfo
FROM Singers
WHERE SingerId = (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000)
FOR UPDATE;
ビューのクエリに使用する
次の例に示すように、FOR UPDATE
句を使用してビューをクエリできます。
CREATE VIEW SingerBio AS SELECT SingerId, FullName, SingerInfo FROM Singers;
SELECT * FROM SingerBio WHERE SingerId = 5 FOR UPDATE;
ビューを定義するときに FOR UPDATE
句は使用できません。
サポートされていないユースケース
次の FOR UPDATE
のユースケースはサポートされていません。
- Spanner の外部でコードを実行するための相互排除のメカニズムとして: Spanner のロックを使用して、Spanner の外部のリソースへの排他的アクセスを確保しないでください。トランザクションが Spanner によって中断される場合があります。たとえば、トランザクションが再試行された場合、アプリケーション コードによって明示的に試行されたか、Spanner JDBC ドライバなどのクライアント コードによって暗黙に試行されたかにかかわらず、トランザクションの試行が実際に行われている間にロックが実施されたことのみが保証されます。
LOCK_SCANNED_RANGES
ヒントと組み合わせる: 同じクエリでFOR UPDATE
句とLOCK_SCANNED_RANGES
ヒントの両方を使用することはできません。使用すると、Spanner からエラーが返されます。- 全文検索クエリ: 全文検索インデックスを使用するクエリで
FOR UPDATE
句を使用できません。 - 読み取り専用トランザクション:
FOR UPDATE
句は、読み取り / 書き込みトランザクション内で実行されるクエリでのみ有効です。 - DDL ステートメント内: DDL ステートメント内のクエリで
FOR UPDATE
句を使用できません。これらのクエリは、後で実行するために保存されます。たとえば、ビューを定義するときにFOR UPDATE
句を使用できません。ロックが必要な場合は、ビューをクエリするときにFOR UPDATE
句を指定できます。
次のステップ
- GoogleSQL と PostgreSQL で
FOR UPDATE
句を使用する方法を学習する。 LOCK_SCANNED_RANGES
ヒントについて学習する。- Spanner のロックについて学習する。
- Spanner のシリアル化可能性について学習する。