販売ページのクエリチューニング その2
はじめに
概要
ecforceはエンドユーザへの販売の仕組み、注文や顧客の管理を提供するシステムです。
今回はエンドユーザ向けの販売ページに関する速度改善についてまとめました。
経緯
ここ数年、エンドユーザ向けの販売ページの機能追加によって、販売ページの描画が遅くなりました。
それによって以下の問題が発生しました。
- クライアントによっては広告を打って、販売ページにアクセスが集中するケースがあり、サーバが負荷に耐えられず、購入できないエンドユーザが発生した
- 一部クライアントからクレームが上がってしまった
- インフラチームの稼働負荷が上がってしまった
上記を解消するために、半年間かけて販売ページのパフォーマンスを改善しました。
原因特定
分析ツールを使って原因を探ったところ、以下の箇所で不要なSQLが発行されていることが確認できました。
- 対象の販売ページで利用可能な支払い方法の一覧を取得し、画面に展開する
- 対象の販売ページの設定値を取得し、画面に展開する
今回は 対象の販売ページの設定値を取得し、画面に展開する
の内容を記載します。
本題
まずこの機能における前提を記載します。
- 広告URL単位で販売ページを作成することができる
- 広告URLごとにフォームに表示する項目を制御することができ、個別設定がないものはマスターデータの設定に従う
- ecforceで設定可能なフォーム設定は約50-60ほど存在する
- 広告URLのテーブルは
urls
(model:Url
) - マスターデータのテーブルは
lp_form_settings
(model:LpFormSetting
) - 個別設定の中間テーブルは
lp_form_settings_urls
(model:LpFormSettingUrl
)
元のコード
以下はフォームに表示する項目を制御するメソッドです。
広告URLごとの個別設定を優先し、なければマスターデータの設定に従うという仕様を満たすために、以下のようなメソッドが設定値の個数分存在していました。
def show_required_check?
lp_form_setting = LpFormSetting.required_check
case lp_form_settings_urls.find_by(lp_form_setting: lp_form_setting).try(:value)
when 1
true
when 0
false
else
lp_form_setting.value == 1
end
end
def show_login_form?
lp_form_setting = LpFormSetting.login_form
case lp_form_settings_urls.find_by(lp_form_setting: lp_form_setting).try(:value)
when 1
true
when 0
false
else
lp_form_setting.value == 1
end
end
def show_product_name?
lp_form_setting = LpFormSetting.product_name
case lp_form_settings_urls.find_by(lp_form_setting: lp_form_setting).try(:value)
when 1
true
when 0
false
else
lp_form_setting.value == 1
end
end
@url.show_required_check?
@url.show_login_form?
@url.show_product_name?
問題点
これらのコードには以下のような問題点がありました。
- 画面が描画される度に、マスターデータの参照と中間レコードの参照が、設定値の個数分だけクエリが発行される
- 設定値の個数分だけ、このようなメソッドの定義が必要になる
改善後のコード
上記を解消するためにコードを改善し、最終的には以下のようになりました。
def personal_lp_form_settings_hash
url_lp_form_settings_hash =
lp_form_settings_urls.joins(:lp_form_setting)
.pluck('`lp_form_settings`.`query`', :value, :value_text)
.map do |query, personal_value, personal_value_text|
[query, personal_value.nil? ? personal_value_text : !personal_value.zero?]
end.to_h
global_lp_form_settings_hash =
LpFormSetting.pluck(:query, :value, :value_text).map do |query, global_value, global_value_text|
[query, global_value.nil? ? global_value_text : !global_value.zero?]
end.to_h
global_lp_form_settings_hash.merge(url_lp_form_settings_hash)
end
@lp_form_settings = @url.personal_lp_form_settings_hash
@lp_form_settings['required_check']
@lp_form_settings['login_form']
@lp_form_settings['product_name']
.
.
.
@lp_form_settings['customer_term_text']
改善のポイント
コードを改善するにあたり、以下のような点を意識しました。
- pluckでの参照の仕方
- 個別設定とマスターデータで同じ形のハッシュを生成し、個別設定で後勝ちさせる
pluck での参照の仕方
pluckは以下のように joinした先の列
も指定することができます。
これによって、内部結合させつつ必要な列だけ取得して配列に展開することができました。
lp_form_settings_urls.joins(:lp_form_setting).pluck('`lp_form_settings`.`query`', :value, :value_text)
個別設定とマスターデータで同じ形のハッシュを生成し、個別設定で後勝ちさせる
Rubyのハッシュには merge というメソッドがあります。
個別設定とマスターデータそれぞれハッシュを生成し、同じ形にした上で merge
を使います。
上記 merge
メソッドの仕様により、
- 個別設定があれば個別設定の値が優先される
- 個別設定がなければ、該当のkey-valueがないため、マスターデータの値が優先される
といった形でシステムの仕様を満たすことができます。
global_lp_form_settings_hash.merge(url_lp_form_settings_hash)
まとめ
販売ページのフォーム設定の描画において以下を実施したことで、SQLの発行回数が 80回 → 6回
ほどに改善されました。
- pluck を使って内部結合した上で必要な値だけ取得
- マスターデータと個別設定において、同じ形のハッシュを生成し、
merge
メソッドを利用
SUPER STUDIOの採用について
SUPER STUDIOでは、エンジニアを採用しています。
少しでも興味がありましたら、以下をご覧ください。
下記の記事は、SUPER STUDIOのキックオフイベントで表彰されたエンジニアのインタビュー記事です。
SUPER STUDIOのエンジニア組織をより理解できる内容となっておりますので、ご一読ください。
Discussion