Active Recordデータ処理のアンチパターンについて整理されたスライドを見つけたので、学習のためにまとめました。
今回読んだスライドがこちらです。
3つの事例と6つのアンチパターンを紹介していただいているので、1つずつ見て整理して行きます。
前提事項
・User has many Postsな関係のUserモデル&Postモデル
・User10万件、Post50万件のデータが登録されている
・データベースの最適化は考慮しない
スキーマ定義
create_table "posts", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.bigint "user_id, null: false
t.string "title"
t.text "content"
t.integer "like_count", default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_posts_on_user_id"
end
create_table "users", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.string "email", null: false
t.string "name", null: false
t.integer "point", default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
end
add_foreign_key "posts", "users"
事例1 全ユーザの中から、2017以降の登録ユーザへ100ポイント付与する
アンチパターン
User.all.each do |user|
if user.created_at >= Date.new(2017)
user.point += 100
user.save
end
end
All Each Pattern
Model.allで、テーブルの全レコードを取得した後に、その結果をeachでループさせること。
問題点 | 解決策 |
---|---|
全件取得でメモリが逼迫。そもそも全件取得する必要あるのか? | 取得件数をフィルター(all→where) |
ループ回数が増えるのでCPUリソースも消費 | 少しずつUserを取得してメモリフレンドリーな処理に (each→fnd_each) |
改善後ソースコード
User.where("created_at>=?", Date.new(2017)) # 取得件数のフィルタリング
.find_each do |user| # 少しずつレコードを取得する
user.point += 100
user.save
end
N+1 Update Queries Pattern
問題点 | 解決策 |
---|---|
1回Select+N回Updateのクエリが走る | 複数レコードを一括で更新する(update→update_all) |
Nの数が多くなるほどパフォーマンスが悪化する | update_allはバリデーションやコールバックが実行しないので、適切な処理を加えた上で使う必要がある。 |
改善後ソースコード
User.where("created_at>=?", Date.new(2017))
.update_all("point = point + 100")
まとめ
・全件取得はなるべく避け、事前にフィルタリングできないか検討する
・ループ処理はCPUリソースを消費するので、find_eachメソッドなどで少しずつ処理する
・update_allを使用することで、クエリの発行回数を削減する