小野マトペの業務日誌(アニメ制作してない篇)

はてなダイアリーの閉鎖をうけ、旧ブログ http://d.hatena.ne.jp/ono_matope/ から移行しました。続きは→ http://matope.hatenablog.com/

MySQLで50個のIDからレコードを取得したいときに、プレースホルダで50回叩くのとINで一回で取るの、どっちが速いか。

今日はふぁぼったーのフロントエンドのDB処理まわりをリファクタリングしました。この辺りも、もう1年半も開発・拡張を続けている部分なのでかなり汚くなっており、今後のためにできるだけソースを短くし、保守性の高いコードに書き直すなどの作業です。
そのなかで、以前から気になっていた点があったので、せっかくなので検証してみました。

前提

ふぁぼったーでは、発言をデータベースから取得する際、最初に発言とそのユーザーをSTATUSテーブルとUSERテーブルからSELECTして、そこで得た発言のIDを用いて、右下に出る各発言のふぁぼりユーザーをFAVORITEテーブルとUSERテーブルから別途SELECTしています。二つに分けたのはその方が速かったからですが、今回検証したのは、後半のふぁぼりユーザー取得部分の書き方です。


FAVORITEテーブルから複数の既知のステータスIDのレコードをSELECTしてきたい訳ですが、大体以下の二つの取得方法が考えられます。

A. プレースホルダつきプリペアドステートメントを用いて複数回叩く。

$favSQL="SELECT SQL_NO_CACHE * from FAVORITE,USER where FAVORITE.id=? and FAVORITE.user_id=USER.id;";

というSQLをprepareし、FAVORITE.idのプレースホルダにステータスIDをつっこみつつステータスの数だけexecuteする。

B. IN句に全てのIDをつっこんで一回で叩く。

$favSQL="SELECT SQL_NO_CACHE * from FAVORITE,USER where FAVOIRTE.id IN (".implode(',',$id_array).") and FAVORITE.user_id=USER.id;";

SQLを一発叩く。でもどのレコードがどのステータスIDに対応するのかという対照性はレコードの中身を見ないと分からないので、自力で各ステータスにつっこむという一手間を加える必要がある。

比較

人間が書きやすいのは多分Aなのですが、パフォーマンスの面ではどの程度差が出るのか気になったので、ベンチマークを取ってみました。

条件

  1. 2008/11/22の人気ふぁぼられページでテスト
  2. SQLキャッシュは無効
  3. プレースホルダを利用する関係上、SQL単体での比較は難しい。よって、各方式でふぁぼりユーザーを取得する関数を作成し、関数全体の実行時間を比較
A方式.
SELECT SQL_NO_CACHE f.id as status_id,screen_name,profile_image_url from FAVORITE f,USER u where f.id=? and f.user_id=u.id;

/*以下のプレースホルダで逐次実行。
1424797395, 1429571494, 1425533647, 1424770030, 1424798850, 1429457857, 1427719403, 1424996482, 1427697348, 1425186009, 1425883861, 1425883445, 1424981533, 1424930159, 1430825300, 1431058634, 1425179905, 1424813902, 1430527920, 1425768797, 1424796665, 1430748456, 1424828035, 1430514385, 1430474389, 1427714952, 1424987539, 1424839614, 1431577764, 1427783898, 1425414359, 1427950164, 1425191835, 1424916362, 1430541056, 1429964773, 1428692993, 1427342901, 1425160690, 1425081143, 1430872949, 1425871198, 1424959392, 1430738299, 1430707134, 1430345316, 1430150316, 1427859488, 1427784934, 1427701659, 1424995649 
*/
B方式.

以下のSQL文で実行

SELECT SQL_NO_CACHE f.id as status_id,screen_name,profile_image_url from FAVORITE f,USER u where f.id IN ( 1424797395, 1429571494, 1425533647, 1424770030, 1424798850, 1429457857, 1427719403, 1424996482, 1427697348, 1425186009, 1425883861, 1425883445, 1424981533, 1424930159, 1430825300, 1431058634, 1425179905, 1424813902, 1430527920, 1425768797, 1424796665, 1430748456, 1424828035, 1430514385, 1430474389, 1427714952, 1424987539, 1424839614, 1431577764, 1427783898, 1425414359, 1427950164, 1425191835, 1424916362, 1430541056, 1429964773, 1428692993, 1427342901, 1425160690, 1425081143, 1430872949, 1425871198, 1424959392, 1430738299, 1430707134, 1430345316, 1430150316, 1427859488, 1427784934, 1427701659, 1424995649 ) and f.user_id=u.id;

結果

参考:ページあたりのfav数の少ない「新着fav」ページでテストした結果

結論

2倍から6倍ほどIN句の方が高速でした。なんとなく予想はしていましたが、思った以上に一回のクエリで処理できるというアドバンテージは大きいみたいですね。
出来るだけ高速にしたい局面では、複数の既知のIDからレコードを取得する際にはIN句を使った方がよさそうです。

参考

実際に使った関数を置いておきます。

350   private function getStatusesFavotters(){
351     $favSQL="SELECT SQL_NO_CACHE f.id as status_id,screen_name,profile_image_url from FAVORITE f,USER u where f.id=? and f.user_id=u.id;";
352     preprint($favSQL);
353     $favStmt=$this->pdo->prepare($favSQL);
354     foreach ($this->statuses as $key => $rev) {
355       $favotters = array();
356       $favStmt->execute( array( $rev['status_id'] ) );
357       
358       while($favotter=$favStmt->fetch(PDO::FETCH_ASSOC)){
359         $url=$favotter['profile_image_url'];
360         $favotter['profile_image_url_mini']=preg_replace("/(bigger|normal).(jpg|jpeg|gif|png)/i","mini.$2",$url,1);
361         array_push($favotters,$favotter);
362       }
363       $this->statuses[$key]['favotters'] = $favotters;
364         
365     }   
366   }     
367   private function getStatusesFavotters2(){
368     $sids = array();
369     $favotters = array();
370     foreach($this->statuses as $st){
371       array_push($sids,$st['status_id']);
372       $favotters[$st['status_id']] = array();
373     }   
374         
375     $favSQL="SELECT SQL_NO_CACHE f.id as status_id,screen_name,profile_image_url from FAVORITE f,USER u where f.id IN (".implode(',',$sids).") and f.user_id=u.id;";
376     preprint($favSQL);
377     $favStmt = $this->pdo->query($favSQL);
378     while($favotter = $favStmt->fetch(PDO::FETCH_ASSOC)){
379       $url=$favotter['profile_image_url'];
380       $favotter['profile_image_url_mini']=preg_replace("/(bigger|normal).(jpg|jpeg|gif|png)/i","mini.$2",$url,1);
381       array_push($favotters[ $favotter['status_id'] ],$favotter);
382     }
383     foreach( $this->statuses as $key => $rev ){
384       $this->statuses[$key]['favotters'] = $favotters[ $rev['status_id'] ];
385     } 
386   }