Zen Cartハイエンド版 で、商品を10万件データベースに登録したときのパフォーマンスをチェックしてみたところ、「かんたんカテゴリ管理」のトップ階層を表示させるのに時間がかかっていることが判明した。
そのため、遅くなっている原因を追求し、時間が掛かっている部分を改良(チューニング)することで速度向上を行った。
「かんたんカテゴリ管理」とは、Zen Cartハイエンド版で提供されている管理者向けのカテゴリ・商品管理のアドオン・モジュールである。かんたんカテゴリ管理を使うと、カテゴリの新規登録や階層の操作、カテゴリへの商品の追加などをZen Cartよりわかりやすいインターフェイスで行うことができる。
以下に10回あたり(httpサーバ負荷なし)の調査結果を示す。
◆Zen Cartハイエンド かんたんカテゴリ管理 01 42.23 02 42.72 03 42.25 04 42.02 05 38.25 06 39.34 07 40.88 08 42.17 09 36.65 10 42.59 -------- 平均40.91
平均で40.91秒かかっているのが分かる。連続して同じページを表示させているが、DBのキャッシュの効果も出ていないように見られる。
なお比較として、ハイエンド版ではない通常のZen Cartの管理画面「カテゴリ・商品の管理」についても計測したので付記しておく。
◆Zen Cart カテゴリ・商品の管理 01 43.78 02 43.32 03 43.81 04 43.74 05 43.63 06 39.01 07 43.52 08 40.88 09 36.37 10 43.27 -------- 平均42.11
両方とも時間がかかっているが、ハイエンド版の「かんたんカテゴリ管理」の方がやや応答速度が早い。これは、ハイエンド版「かんたんカテゴリ管理」の方が一度に取得しているパラメータが少ないことなどが理由に考えられる。
URL呼び出しから遅い部分を取り出して調べることにする。
かんたんカテゴリ管理の呼び出しは以下のURLで行われる。
https://www.example.com/zencart/admin/addon_modules_admin.php?module=easy_admin_products/categories
URLから、
addon_modules
の名称が、
easy_admin_products
ということが分かる。つまり、対象のモジュールは以下の階層にある。
インストールdir/includes/addon_modules/easy_admin_products/
このディレクトリの中で表示に時間がかかる部分を調べていく。調査には色々な方法があると思うが、今回はprintfデバッグを行った。
printfデバッグとは、通常のデバッガにあるブレークポイントなどの代わりにprint文などのデバッグ用のコードを直接埋め込んでいく最も原始的なデバッグ方法の1つである。パフォーマンスの悪い部分を探すため、print文のほかにもPHPのデフォルト関数であるmicrotime()関数を使って実行時間をチェックしていく。
しばらく試行錯誤を繰り返すと、上記階層のinclude/配下にある
easy_admin_products_category_model.php
というファイル中の
convert_categories_result()
という関数に実行時間が掛かっていることが分かった。さらに対象を絞っていくと、
$categories_values->fields['is_link']
= (zen_get_products_to_categories($categories_id, true, 'products_active') == 'true');
という呼び出しに40秒近く掛かっていることが分かった。前述の統計からすると、41秒の負荷のうちのほとんどがこの1行に集中していることになる。
この行は何をしてるかというと、最終的に$categories_values->fields['is_link']に真偽値が入るのだが、これの判定にzen_get_products_to_categories()という関数の呼び出しを行っていることが分かる。
zen_get_products_to_categories()が定義されている行を調べると、
インストールDir/admin/includes/functions/general.php
にその記述がある。ソースコードを解析していくと、$category_id以下(選択されているカテゴリ以下)の全ての商品IDを抽出し、親カテゴリが2つ以上あるものを探して真偽値を返している。
つまりこれはリンク商品を探す関数である。Zen Cartでは、2つ以上の親カテゴリに属する商品はリンク商品として扱われる。
今回の場合、商品数が10万レコード存在するため、商品IDのチェックもトップカテゴリでは10万回行われる。いくら高速なMySQLの呼び出しであっても、10万回の問い合わせにはそれなりの時間が掛かる。この部分がボトルネックであることはほぼ確実であるため、この関数の書き換えを行なう。
なお、このモジュールで呼び出されているSQL問い合わせの数は実に100,336回にも及んでいた。
試行錯誤の末、10万回超の問い合わせを180回程度にまとめることができた。リンク商品のIDを今まで1つずつ10万回行っていた点を改良し、1つのSQL問い合わせでリンク商品の一覧を取得するようにした。
まず、child_categories()というメソッドを新たに作り、自分自身を含む下層カテゴリを全て洗い出す。この関数では効率化のために再帰が使われている。
また、母集合として、カテゴリと商品の関連テーブルを商品テーブルを使用し、JOINしておく。複数のカテゴリに含まれる商品は同じ商品IDが複数回現れるはずである。
母集合をIN()句を使って調べようとしているカテゴリか、その子孫カテゴリに属するものに絞る。
そして商品IDをgroup by句とcount()文を使って出現回数を調べる。2回以上出現するものがリンク商品である。
こうしてSQLクエリの呼び出しを減らした結果、以下のようにかなりのパフォーマンスが得られた。
100,336クエリを発行している(商品数+カテゴリ数+α?)。 ↓ 182クエリに削減(内包カテゴリを再帰で取得(複数クエリ)、リンク商品の有無を1クエリで問い合わせ)
◆削減後のかんたんカテゴリ管理 01 1.23 02 1.02 03 1.30 04 1.03 05 1.01 06 1.00 07 1.15 08 0.96 09 1.04 10 1.00 ------ 平均1.07
ネックとなっていた40秒かかるリンク商品検索部分が高速化されたため、商品数が10万あるような場合でも問題ないパフォーマンスが出るようになったことがわかる。