この記事を三行にまとめると
containを使うよりも自分で二回findをした方が良いかもしれません配列を作成するために二重ループをやってます
1,000万回のと11,000回じゃあ速度に差が出るのも無理ない
CakePHP2の話なのですが、hasManyで紐づくデータを一緒に取ってくる場合、データ量がすごーく多い場合はcontainを使うよりも自分で二回findをした方が良いかもしれません。
例えばpostsというテーブルとtagsというテーブルがhasManyでアソシエーションしている場合。
この場合は以下のようなSQLが発行されています。
実際はCakeさんがもう少しいい感じにSQL文を作ってくれますが、すごいざっくり書くとこんなような処理が行われています。
これと同じSQLをcontainを使わずに発行する場合。
僕が試した限りだと、postsデータが1000件で紐づくtagsデータが10,000件くらいだった場合に、containを使うのを止めたことで画面の表示に10秒近くかかってたのが1秒くらいまで縮まりました。
containを使った場合、それぞれのpostsデータにtagsデータを紐づけた配列を返してくれますからね。その配列を作成するための処理に時間がかかるんでしょう。
ざっとソースを見た限りではこの配列を作成するために二重ループをやってます。
何かこんな感じのこと。これだとpostsデータが1,000件でtagsデータが10,000件あった場合、単純計算して1,000×10,000回のループ処理が発生しますからね。そりゃあ時間がかかるっつー話ですよ。
containを止めた場合はpostsデータとtagsデータが独立した状態になるので、紐づける処理も自分で書かなきゃならない。
でも自分でループ処理を書けばcontainの時と同じ配列を1,000+10,000回のループで作ることもできる。
例えばこんな感じ。1,000万回のと11,000回じゃあ速度に差が出るのも無理ないですよね。
CakePHP3の方では検証してないので、もしかしたら3系ではこの辺が良い感じに改善されているかもしれません。でも、もしも2系でhasManyを使ってるところの処理がどーにも重たいなあと思ったら、containをやめてみるのも一つの解決策だと思います。
それか自分で処理を改造して、hasManyをcontainする時に二重ループにならないようにしてしまうという手段もありかもですね。二重ループじゃなくすることによる弊害はたぶんないと思うので。
例えば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する時に二重ループにならないようにしてしまうという手段もありかもですね。二重ループじゃなくすることによる弊害はたぶんないと思うので。