SELECT FOR UPDATE を使用する

このページでは、Spanner で FOR UPDATE 句を使用する方法について説明します。

SELECT クエリを使用してテーブルをスキャンする場合は、FOR UPDATE 句を追加して、行と列の粒度レベル(セルレベル)で排他ロックを有効にします。ロックは、読み取り / 書き込みトランザクションが存続している間、保持されます。この間、FOR UPDATE 句により、現在のトランザクションが完了するまで、他のトランザクションがロックされたセルを変更できなくなります。詳細については、GoogleSQLPostgreSQLFOR 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 11 <= AlbumId < 5MarketingBudget セルをロックします。

-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 5
FOR UPDATE;

AlbumId = 1MarketingBudget の読み取りを試行している 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 13 <= AlbumId < 5MarketingBudget セルをロックしているためです。これは 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 = 1SingerId セルと 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 < 5MarketingBudget セルをロックします。

-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 5
FOR UPDATE;

Transaction 2Albums テーブルの更新を試みると、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 < 10MarketingBudget セルをロックします。

-- 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 句を指定できます。

次のステップ