バルクアップデートとは
複数のレコードを一括で更新する操作のこと。
どんな時にバルクアップデートをするのか
-
パフォーマンスの向上・負荷削減
- 大量のデータを一度に更新する為、レコードを1つずつ更新するよりも遥かに高速
- DBへのアクセスは比較的高コストな為、一度のクエリでまとめて更新した方が効率的
- DBへのアクセス回数を減少させるため、サーバーのリソースを節約
-
トランザクションの最適化
- 複数のレコードを1つのトランザクションで処理をすることが出来る
- エラーが発生した場合にはトランザクション全体をロールバックすることが出来る
- トランザクション全体を制御するため、データの整合性を保つことが出来る
- 更新がすべて成功するか、すべて失敗するかのいずれか
- サーバーや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
はそれ以前のバージョン向け)
gem 'activerecord-import'
bundle install
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に追加または更新しようとした際、一意制約(主キーやユニークキー制約)に違反した場合に、特定のカラムを更新するために使用される。それにより、指定したカラムが更新されるようになる。
①Insert実行
②既にDBに存在する場合、一意制約に引っかかる
③Update実行
INSERT INTO "posts" (
"id", "genre_id", "updated_system", "updated_at"
)
VALUES
(
1, 1, 'sample_system', '2024-07-10 15:41:07.727030'
)
ON CONFLICT (id) DO
UPDATE
SET
"genre_id" = EXCLUDED."genre_id",
"updated_system" = EXCLUDED."updated_system",
"updated_at" = EXCLUDED."updated_at"
RETURNING "id"
UPSERT(ON CONFLICT句)
ON CONFLICT句にDO UPDATE句を指定すると、テーブルに挿入したい行がまだ存在しない(制約に違反する行が存在しない)場合はDO NOTHING句と同様に通常の挿入処理となります。ただし、テーブルに挿入したい行がすでに存在する(制約に違反する行がすでに存在する)場合は対象行への挿入処理が更新処理として実行されるようになります。
オプションをもっと細かく設定することも可能
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_flg
がfalse
の場合に更新を行う条件を指定
- ここでは、
-
setオプションを使用すれば、特定の値を設定できる(上記では不使用)
- 例)
set: { title: "Updated Title", contents: "Updated Contents" },
- 例)
- 最後に、
#import
を呼び出し、postsの中身をDBへインポートし、重複の際に指定されたカラムを更新
要約
postsに含まれるデータをDBに追加しようとしている。
もしも既に同じid
が存在し、かつdelete_flg
がfalseの場合にのみ、
title
とcontents
を更新する。
※例えば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への負荷が低い時間帯を選ぶ等
- トランザクションの範囲を検討する
- トランザクションが長時間続くと、DBのロックやパフォーマンス低下といった影響を与える可能性があるため
- 適切なインデックスが設定されていることを確認する
- 適切なインデックスが存在しない場合、処理が遅くなる可能性がある