🎉

販売ページのクエリチューニング その2

2024/02/02に公開

はじめに

概要

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では、エンジニアを採用しています。
少しでも興味がありましたら、以下をご覧ください。
https://hrmos.co/pages/superstudio/jobs/0000400
https://hrmos.co/pages/superstudio/jobs/0000404
https://hrmos.co/pages/superstudio/jobs/0010025
https://hrmos.co/pages/superstudio/jobs/0000407

下記の記事は、SUPER STUDIOのキックオフイベントで表彰されたエンジニアのインタビュー記事です。
SUPER STUDIOのエンジニア組織をより理解できる内容となっておりますので、ご一読ください。
https://www.wantedly.com/companies/super-studio/post_articles/497997
https://www.wantedly.com/companies/super-studio/post_articles/487617

SUPER STUDIOテックブログ

Discussion