hasManyのcontainに頼るか否か

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
河津七滝ループ橋。実に行きたくなる橋である

この記事を三行にまとめると

containを使うよりも自分で二回findをした方が良いかもしれません
配列を作成するために二重ループをやってます
1,000万回のと11,000回じゃあ速度に差が出るのも無理ない
CakePHP2の話なのですが、hasManyで紐づくデータを一緒に取ってくる場合、データ量がすごーく多い場合はcontainを使うよりも自分で二回findをした方が良いかもしれません。

例えばpostsというテーブルとtagsというテーブルがhasManyでアソシエーションしている場合。

$params = array(
  'conditions' => array('id' => array(1,2,3)),
  'contain' => array('Tag'),
);
$posts = $this->Post->find('all', $params);

この場合は以下のようなSQLが発行されています。

//postsデータ
SELECT * FROM posts WHERE id IN (1,2,3)

//tagsデータ
SELECT * FORM tags WHERE post_id IN (1,2,3)

実際はCakeさんがもう少しいい感じにSQL文を作ってくれますが、すごいざっくり書くとこんなような処理が行われています。

これと同じSQLをcontainを使わずに発行する場合。

//postsデータを取得
$posts = $this->Post->find('all');

//tagsデータを取得
foreach($post as $post) {
  $ids[] = $post['Post']['id'];
}
$params = array(
  'conditions' => array('post_id' => $ids),
);
$tags = $this->Tag->find('all', $params);

僕が試した限りだと、postsデータが1000件で紐づくtagsデータが10,000件くらいだった場合に、containを使うのを止めたことで画面の表示に10秒近くかかってたのが1秒くらいまで縮まりました。

containを使った場合、それぞれのpostsデータにtagsデータを紐づけた配列を返してくれますからね。その配列を作成するための処理に時間がかかるんでしょう。

//$posts
Array(
  [0] => Array(
    [Post] => array(
      [id] => 1
      [title] => タイトル1
      [text] => 本文1
    )

    [Tag] => array(
      [0] => Array(
        [id] => 1
        [post_id] => 1
        [name] => タグ1
      )

      [1] => Array(
        [id] => 2
        [post_id] => 1
        [name] => タグ2
      )
    )
  )
  [1] => Array(
    [Post] => array(
      [id] => 2
      [title] => タイトル2
      [text] => 本文2
    )

    [Tag] => array(
      [0] => Array(
        [id] => 3
        [post_id] => 2
        [name] => タグ3
      )

      [1] => Array(
        [id] => 4
        [post_id] => 2
        [name] => タグ4
      )
    )
  )
)

ざっとソースを見た限りではこの配列を作成するために二重ループをやってます。

foreach($posts as $key => $post) {
  foreach($tags as $tag) {
    if($post['Post']['id'] == $tag['Tag']['post_id']) {
      $posts[$key]['Tag'][] = $tag['Tag'];
    }
  }
}

何かこんな感じのこと。これだとpostsデータが1,000件でtagsデータが10,000件あった場合、単純計算して1,000×10,000回のループ処理が発生しますからね。そりゃあ時間がかかるっつー話ですよ。

containを止めた場合はpostsデータとtagsデータが独立した状態になるので、紐づける処理も自分で書かなきゃならない。

//$posts
Array(
  [0] => Array(
    [Post] => array(
      [id] => 1
      [title] => タイトル1
      [text] => 本文1
    )
  )
  [1] => Array(
    [Post] => array(
      [id] => 2
      [title] => タイトル2
      [text] => 本文2
    )
  )
)

//$tags
Array(
  [0] => Array(
    [Tag] => array(
      [id] => 1
      [post_id] => 1
      [name] => タグ1
    )
  )
  [1] => Array(
    [Tag] => array(
      [id] => 2
      [post_id] => 1
      [name] => タグ2
    )
  )
  [2] => Array(
    [Tag] => array(
      [id] => 3
      [post_id] => 2
      [name] => タグ3
    )
  )
  [3] => Array(
    [Tag] => array(
      [id] => 4
      [post_id] => 2
      [name] => タグ4
    )
  )
)

でも自分でループ処理を書けばcontainの時と同じ配列を1,000+10,000回のループで作ることもできる。

foreach($tags as $tag) {
  $assoc[$tag['Tag']['post_id']][] = $tag['Tag'];
}

foreach($posts as $key => $post) {
  $posts[$key]['Tag'] = $assoc[$post['Post']['id']];
}

例えばこんな感じ。1,000万回のと11,000回じゃあ速度に差が出るのも無理ないですよね。



CakePHP3の方では検証してないので、もしかしたら3系ではこの辺が良い感じに改善されているかもしれません。でも、もしも2系でhasManyを使ってるところの処理がどーにも重たいなあと思ったら、containをやめてみるのも一つの解決策だと思います。

それか自分で処理を改造して、hasManyをcontainする時に二重ループにならないようにしてしまうという手段もありかもですね。二重ループじゃなくすることによる弊害はたぶんないと思うので。
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください