Postgresのトランザクション内でTRUNCATEをする際の注意点

トランザクション内で排他ロックが発生してしまい、該当処理が終わるまでの間参照なども不可能になる事態になってしまったのでメモ。

・原因となったコード(サンプル)

    ActiveRecord::Base.transaction do
      ActiveRecord::Base.connection.execute("TRUNCATE TABLE #{tables} RESTART IDENTITY;")
      ~~~~~~~~~何らかの時間がかかる処理~~~~~~~~~
    end

postgresはTRUNCATEする際に最も強いロック権限(ACCESS EXCLUSIVE)を獲得するが、トランザクション内だとその権限を保持したままになるため後続処理がロック待機状態になってしまう模様。

TRUNCATEは操作対象の各テーブルに対するACCESS EXCLUSIVEを獲得します。 これは、この他のそのテーブルに対する同時操作をすべてブロックします。 テーブルへの同時アクセスが必要ならば、代わりに DELETEコマンドを使用しなければなりません。

www.postgresql.jp

stackoverflow.com

対処法

①TRUNCATEでなくDELETEを使う

メリット
・排他ロックがかからず、安全に削除処理を実行できる
・処理が途中でコケた場合でもロールバック可能

デメリット
・削除に時間がかかる
・デッドタプルが大量に発生するため、データ量や1日の実行回数によってはvacuumが追いつかなくなる

    ActiveRecord::Base.transaction do
      SomeModel.delete_all
      # プライマリキーのシーケンスをリセットする
      ActiveRecord::Base.connection.reset_pk_sequence!('table')

      ~~~~~~~~~何らかの処理~~~~~~~~~
    end

②TRUNCATEは個別のトランザクションで行う

メリット
・削除処理が一瞬で終わる

デメリット
・一度TRUNCATEが完了してしまうと、削除したレコードのロールバックは不可能になってしまう

サンプルコード

    # トランザクション1(TRUNCATEのみ 一瞬で終わる)
    ActiveRecord::Base.transaction do
      ActiveRecord::Base.connection.execute("TRUNCATE TABLE #{table} RESTART IDENTITY;")
    end
    
    # トランザクション2(本来やりたい処理)
    ActiveRecord::Base.transaction do  
      ~~~~~~~~~何らかの処理~~~~~~~~~
    end