Precena Tech Book
コーポレートサイト採用サイト
  • はじめに
  • ソフトウェア開発
    • 開発環境構築
      • Homebrew
        • Homebrew用語の意味
      • ngrok
        • ngrokの導入
        • ngrokのアップグレード(v2 to v3)
      • Slack
        • Slackの/remind コマンドの形式
        • 対面での相談を気軽にするためのSlack設定
      • AWS CLI
      • Ruby
      • Scala
      • Prettier
      • zsh
        • zsh-completion
      • Mac
        • M1 Macでの開発環境構築(rosetta 無し)
    • バックエンド
      • OpenAPI
        • OpenAPI 定義ファイル分割のすゝめ
      • Ruby on Rails
        • ActiveRecordのfind_or_initialize_byメソッドにブロックを渡したときの挙動
        • Railsのアプリケーションサーバーのプロセス数とスレッド数の設定方法
        • Railsを6.1系から7.0系へアップグレードした時に調査したこと
        • schema.rbで差分が発生する事例とその復旧について
        • tmux + overmind を利用して、複数システムを1コマンドで起動できるよう設定する
        • Rails Migrationチートシート
        • GithubのプライベートリポジトリをGemfileで参照する方法
        • ActiveSupportのto_jsonメソッドの注意点
        • 危険なJSON出力を禁止するRuboCopカスタムルールの作成方法
      • Scala
        • Validated を直列に処理したい
      • DB
        • PostgreSQLにおける、削除行に対するロック獲得時の挙動
    • フロントエンド
      • React
        • Storybookを利用したビジュアルリグレッションテスト
  • インフラ開発
    • AWS
      • IAM
        • スイッチロールの設定手順
        • AWS CLIでのスイッチロールの設定手順
        • AWS Vaultを使ったスイッチロール設定手順
        • Github ActionsでIAMロールを利用してAWSリソースを操作する
      • ECS
      • SES
        • AWS SESメールボックスシミュレーターにて、カスタムヘッダや添付ファイル付きのテストEメールを送信する
      • CloudWatch
        • Amazon SNS + Slack Workflowを使って、CloudWatch Alarmの通知をSlackチャンネルへ投稿する
      • Lambda
        • lambrollでAWS Lambda関数をデプロイしたときのTips
    • Heroku
      • HerokuのStackの設定
      • Heroku Postgresの運用でよく使うコマンド集
  • セキュリティ
    • Web
      • Same Origin PolicyとCORS
      • 脆弱性診断 2社同時依頼実施記録
  • Mail
    • SPF、DKIM、DMARCを使用した迷惑メール対策
  • データ分析
    • データ分析プロセス
  • SaaS
    • Zendesk
      • 問い合わせフォームの項目をサービスごとに出し分け、各サービス担当者に自動で振り分けてメールで通知する
  • イベント
    • RubyKaigi
      • RubyKaigi 2023 に現地参加しました
    • EMConf
      • EMConfJP2025_参加レポート
  • やってみた
    • IoT
      • Raspberry Pi + PaSoRi + Python で、勤怠打刻マシンを作ってみた
  • Precena Tech Book 管理
    • コンテンツ執筆時のルール
  • 関連リンク
    • プレセナエンジニア公式Twitter
GitBook提供
このページ内
  • 環境
  • TLDR
  • 現象の説明
  • 比較実験
  • MySQLの場合
  • PostgreSQLでトランザクション分離レベルをrepeatable readとした場合
  • MySQLでトランザクション分離レベルをread committedとした場合
  • select for updateによるロック獲得の場合
  • 終わりに

役に立ちましたか?

PDFとしてエクスポート
  1. ソフトウェア開発
  2. バックエンド
  3. DB

PostgreSQLにおける、削除行に対するロック獲得時の挙動

直感とは異なる挙動が観察されたため、記事として残します

環境

  • RDB

    • PostgreSQL 13.3

    • トランザクション分離レベル read committed (デフォルト)

  • 実験環境

    • IntelliJ IDEA 2024.3 (Ultimate Edition)

TLDR

PostgreSQLにおいて、トランザクション内で同一キーの行を delete - insert する場合、同時に実行される他のトランザクションからは当該の行が参照できない場合があります。

現象の説明

次のようなSQLを考えます。

SQL1
begin;

-- delete-insertによってデータを更新する
delete from users where id = 1;  -- id が p_key
insert into users (id, name) values (1, 'jane doe');

commit;

実験のためIntellijを使い、2つのセッション分のクエリコンソールを開きます。 これら2つをそれぞれセッション1、セッション2とします。

  1. セッション1で5行目まで実行する。この時 delete によってロックが獲得される

  2. セッション2で4行目まで実行する。するとセッション1で行がロックされているため、待ち状態になる

  3. セッション1で7行目の commit まで実行する。これによりセッション1で獲得されていたロックが解放される

  4. セッション1のロックが解放されたことでセッション2の delete が実行される

この時、4 の結果セッション2では delete が空振りし、insert を実行するもすでに id = 1 の行が存在するため、 [23505] ERROR: duplicate key value violates unique constraint "users_pkey" となります。

比較実験

MySQLの場合

MySQL 8.0.28 で同様の実験を行ったところ、エラーは発生しませんでした。データの更新結果はcommitを後に実行している、セッション2の結果が保存されます。 MySQLのトランザクション分離レベルは repeatable read(デフォルト) です。

PostgreSQLでトランザクション分離レベルをrepeatable readとした場合

ひょっとしたら分離レベルに依存した挙動なのでは?ということでPostgreSQLにて分離レベルをrepeatable readとして実験を行いました。 結果前述4のステップで、以下のようなエラーが返されました。

[40001] ERROR: could not serialize access due to concurrent update

MySQLでトランザクション分離レベルをread committedとした場合

MySQLではrepeatable readの場合と挙動は変わらず、1行返却されました。

select for updateによるロック獲得の場合

PostgreSQLにおいて、delete 文ではなくselect for updateでロックを獲得した場合の挙動についても確認しました。

SQL2
begin;

-- for update により、行ロックを取得する
select * from test where id = 1 for update;

-- delete-insertによってデータを更新する
delete from test where id = 1;
insert into test (id, name) values (1, 'jane doe');

commit;

この場合、セッション2は select for update の行で待ち状態となり、セッション1のcommit後、セッション2の select for update が実行され 0行 の返却となりました。

終わりに

今回、別の問題を調査していく中でたまたまこの現象に遭遇しました。 個人的には全く想定していない挙動でした。

改めてこちらの記事はPostgreSQLでの挙動になります。 前述の通りMySQLでは異なる挙動となりましたので、RDBMSの実装に依存するようです。

ご興味があれば、ぜひお手持ちの環境でも試してみてください。

前へDB次へフロントエンド

最終更新 5 か月前

役に立ちましたか?

これはPostgreSQLの仕様によるもので、 リピータブルリードトランザクションでは、トランザクションが開始された後に別のトランザクションによって更新されたデータは変更またはロックすることができないため とあります。

PostgreSQLでは削除データはすぐには物理削除されませんが、この辺りが関係しているような気もします。

https://www.postgresql.jp/docs/9.4/transaction-iso.html
https://www.postgresql.jp/docs/9.4/sql-vacuum.html