ログ日記

作業ログと日記とメモ

DBから取得したデータを一度に沢山表示する場合の高速化

Webサーバが一台の場合はPHPが処理するよりもデータベースで処理した方が早い。
あと絶対的な時間。例えばPHPで計算して一ページ表示するのに1秒かかりDBで計算して0.1秒かかる場合は、Webサーバが複数あってもどっちにロジックを持って行くかは考えもの。PHPで集計処理を行うとしても、結局DBにselect文を発行する回数は増えるわけだし。
まぁ同時アクセスの数だなぁ。新規のネットショップ程度なら一秒間に何十人もアクセスしてくることは無いと思うのでDB側で処理するのがいいかと。
ただAjaxを使ってる場合は、一人が一ページ表示中に何度もリクエストを送ることになるのでそれも考慮しないといけない。


というわけで、うちの場合は同時アクセスが少ないかつ重い処理を行うのでDB側で計算するように修正。


主な変更項目。

取り敢えずeAcceleratorとかAPCとか入れる。
サーバ設定のみで早くなるので簡単で便利。

  • foreachを減らす

今作ってるやつで一番重いページが、DBのテーブルを10個程度joinしつつgroup byで集計もする。
例えば商品が発送待ちより前の状態を含む注文を取得するコードをPHPで書くと

<?php
$waitedOrders = array();
$orders = $orderDao->get(array('userId' => $userId));
foreach ($orders as $order){
    $orderDetails = $orderDetailDao->get(array('orderId' => $order->orderId));

    $waited = true;
    foreach ($orderDetails as $orderDetail){
        if ($orderDetail->status >= Status::WAIT /* WAIT = 10 */){
            $waited = false;
            break;
        }
    }
    if ($waited){
        $waitedOrders[] = $order;
    }
}

var_dump($waitedOrder);

こんな感じになる。
foreachをarray_filter()とかにしても処理速度はだいたい同じ。


テーブルのリレーションが複雑だとPHPで処理する場合はforeachがやたら出てくる。
単純なforeachならいいけども、ここではormを使っているので内部では更にforeachがかかる。
上記のコードで$ordersが10件くらいだと大したことないけど100件、1000件となってくるとだいぶ重い。
数秒間CPUがmaxになる。


これはやはりDB側で group by order_id を使って max(status) < 10 とかにしたい。
SQL直打ちじゃなくてormを使いたいのでビューで作成してオブジェクトで取得するようにする。
これで処理時間が半分くらいになった。

  • 圧縮転送を行う

これは大量のデータを表示する場合はかなり早くなる。
重いページで ob_start("ob_gzhandler") を使うのもいいけれど、どうせならindex.phpとか全体をコントロールする部分で ini_set('zlib.output_compression', true) とやっておくといい。
これもアクセラレータと同じく一行で済むので取り敢えず初めに書いておく。

IEのエンジンはEUC-JPが重い。よってUTF-8を使う。
EUC-JPを使っている場合、実はこれが一番高速化の効果があった。
サーバからの転送が完了しているのにブラウザのレンダリングで時間がかかっていると結果的に表示速度が遅くなる。既にEUC-JPで作っている場合でも、根こそぎ変更する価値はある。



面倒なのはやっぱりO/Rマッパーを使っている部分。
私の場合は自作のormを使っているのでどこに何回foreachを使っているのかが分かるのだが、オープンソースのものを使っている場合は意識が行かないかもしれない。
お勧めはイテレータイテレータとPDOのbindColumn()を使うと、$db->fetch()を直書きするより若干遅い程度で済む。



# 追記
闇雲にforeachを探しても仕方がないので、xdebugとkcachegrindは必須。Callee MapとCall Graphが素晴らしい。