LoginSignup
3
2

バルクアップデートとは

複数のレコードを一度に更新する操作のこと。

  • bulk(大部分/一括)
  • update(更新)

どんな時にバルクアップデートをするのか

  • パフォーマンスの向上・負荷削減
    • 大量のデータを一度に更新するため、個々のレコードを1つずつ更新するよりも遥かに高速
    • DBへのアクセスは比較的高コストなため、一度のクエリでまとめて更新した方が効率的
    • DBへのアクセス回数を減少させるため、サーバーのリソースを節約
  • トランザクションの最適化
    • 複数のレコードを1つのトランザクションで処理をすることが出来る
    • エラーが発生した場合にはトランザクション全体をロールバックすることが出来る
    • トランザクション全体を制御するため、データの整合性を保つことが出来る
      • 更新がすべて成功するか、すべて失敗するかのいずれか
  • サーバーやDBへの負荷が低減され、処理が高速化される
  • データの整合性を保つことが出来る

サンプルコード

#update!の場合

posts = Post.where(category_id: 1)
posts.update!(contents: '更新', updated_system: 'system')

数百件程度であれば、後述する#import!よりも#update!が適切。

#update!は、条件に合致するレコードを一括で更新する場合にも適している。

ActiveRecordオブジェクトを使用しレコードを更新するため、コールバックやバリデーションが実行される。

一度に大量のレコードを更新する場合、DBの負荷が高まる可能性があるため、適切なタイミングで使うようにする。

#update_allの場合

posts = Post.where(category_id: 1)
posts.update_all(updated_at: Time.zone.now, updated_system: 'system')

条件に合致する全てのレコードを一度に更新する。

数百件程度の一括更新であれば問題なし。

updated_atは明示的に記載してあげる必要がある。

単一のテーブルのみ対象。
関連するモデルや関連するテーブルを同時に更新することは出来ない。

#update_allは即座にDBを更新するため、一度実行すると変更が即時に反映される。
元のデータが失われる可能性があるため注意。

コールバックやバリデーションが実行されない。
#update_allはActiveRecordオブジェクトのコールバックやバリデーションをスキップする。つまり、DBに直接クエリを送信して更新されるため、値がデータベースの制約を満たしているかどうかを事前に検証しない。

#import!の場合

※ Rails6では#insert_all #insert_all! #upsert_allが標準機能としてActiveRecordに追加された(よってactiverecord-importはそれ以前のバージョン向け)

Gemfile
gem 'activerecord-import'
terminal
bundle install
bulk_update_example.rb
class BulkUpdateExample
  def execute
    # カテゴリーとジャンルのIDを取得(サンプルコード用の適当なID)
    @category_id = 1
    @genre_id = 1
    @system = 'sample_system'

    # 1. 10万件のデータを取得
    posts = Post.where(category_id: @category_id).limit(100_000)

    # 2. 配列がメモリを圧迫するため、1000件ずつに分割して実行
    posts.each_slice(1_000) { |target_posts| bulk_update_posts(target_posts) }
  end

  def bulk_update_posts(posts)
    ActiveRecord::Base.transaction do
      # 各レコードに対して同値を設定
      posts.each do |post|
        post.assign_attributes(genre_id: @genre_id, updated_system: @system, updated_at: Time.now)
      end

      # バルクアップデート実行
      Post.import!(posts, on_duplicate_key_update: [:genre_id, :updated_system])

      puts "バルクアップデート成功: #{posts.size}件のレコードが更新されました"
    end
  rescue => e
    # バルクアップデートが失敗した場合のエラーハンドリング
    puts "エラーが発生しました: #{e.message}"
    puts e.backtrace.join("\n")

    # トランザクションをロールバック
    raise ActiveRecord::Rollback
  end
end

BulkUpdateExample.new.execute

#import!は、大量のデータを一括で登録または更新する場合に適している。

on_duplicate_key_updateオプションは、データをDBに追加または更新しようとした際、一意制約(主キーやユニークキー制約)に違反した場合に、特定のカラムを更新するために使用される。それにより、指定したカラムが更新されるようになる。

数千〜数万件以上といった規模のデータ量であれば、#updateよりも#import!を使った方が効率的に処理出来る場合が多い。

オプションをもっと細かく設定することも可能

sample.rb
posts = [
  { id: 1, title: 'Update Title 1', contents: 'Update Contents 1', delete_flg: false, created_at: Time.now, updated_at: Time.now },
  { id: 2, title: 'Update Title 2', contents: 'Update Contents 2', delete_flg: false, created_at: Time.now, updated_at: Time.now }
]

options = {
  on_duplicate_key_update: {
    columns: [:title, :contents],
    conflict_target: [:id],
    condition: "posts.delete_flg = FALSE"
  }
}

Post.import! posts, options
  • columnsオプションには、更新するカラムが指定されている
  • conflict_targetオプションには、重複の判定に使うカラムを指定
    • 既に同じidがデータベース上に存在する場合に更新を行うための設定
  • conditionオプションには、更新の条件を指定
    • ここでは、delete_flgfalseの場合に更新を行う条件を指定
  • setオプションを使用すれば、特定の値を設定できる(上記では不使用)
    • 例)set: { title: "Updated Title", contents: "Updated Contents" },
  • 最後に、#importを呼び出し、postsの中身をDBへインポートし、重複の際に指定されたカラムを更新

要約

postsに含まれるデータをDBに追加しようとしている。
もしも既に同じidが存在し、かつdelete_flgfalseの場合にのみ、
titlecontentsを更新する。
※例えばid: 2がDBに存在しない場合は、新しいレコードとして登録する。

batch_sizeオプションで制御することも可能

以下はバルクインサートの例(バルクアップデートも同様)

# Gemfileに以下を追加し、bundle installを実行する
# gem 'activerecord-import'

class BulkInsertExample
  def execute
    # 1万件のデータを生成
    data = []
    current_time = Time.zone.now

    10_000.times do |i|
      # created_at と updated_at を明示的に指定する(通常のActiveRecordのコールバックやデフォルト値の機能が働かないため)
      data << { item_name: "Item #{i}", created_at: current_time, updated_at: current_time }
    end

    # バルクインサートを実行
    Item.import! data, batch_size: 1_000
  end
end

BulkInsertExample.new.execute

batch_sizeは1000に設定されているため、1000件ずつデータが分割されて処理される。
これにより、メモリの消費を抑えつつ効率的にバルクインサートが実行される。
最終的には1万件のレコードがデータベースに追加される。

バルクアップデートの注意点

  • 一意制約を確認する
    • 重複した値がある場合、更新が失敗する可能性があるため
  • 大量のデータを操作するため、適切なタイミングで実行する
    • DBへの負荷が低い時間帯を選ぶ等
      • (例)アクセスが少ない深夜帯に実行
  • トランザクションの範囲を検討する
    • トランザクションが長時間続くと、DBのロックやパフォーマンス低下といった影響を与える可能性があるため
  • 適切なインデックスが設定されていることを確認する
    • 適切なインデックスが存在しない場合、処理が遅くなる可能性がある
3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2