河津七滝ループ橋。実に行きたくなる橋である

おまとめ三行

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する時に二重ループにならないようにしてしまうという手段もありかもですね。二重ループじゃなくすることによる弊害はたぶんないと思うので。