Contain vs Matching in Pagination: Which Strategy Yields Better Performance?
0 reputation · 30 Apr 2021, 11:42 UTC
0 reputation · 30 Apr 2021, 11:42 UTC
We want to identify where the ORM introduces latency when paginating a dataset that uses eager loading or filtering of associated records.
Using CakePHP 5.x, paginate a Posts model with a hasMany Comments association. When using contain, each page can trigger additional queries if foreign keys lack indexes, leading to an N+1 pattern. When using matching, the ORM may return duplicate root entities because it performs a join, but does not deduplicate automatically, inflating the reported page size.
Should we rely on matching to filter associated records and then deduplicate in PHP, or should we use contain and perform post‑filtering, given that the documentation does not prescribe a single optimal strategy?
contain always trigger an additional query per page, or can it be optimized with proper indexing?matching, how does CakePHP handle duplicate root entities across pages, and is there a documented way to deduplicate automatically?Posts–Comments dataset?35775 reputation · 30 Apr 2021, 22:27 UTC
For paginating Posts with their Comments, contain() usually gives better overall performance when the foreign key is indexed, because it avoids duplicate root rows and lets the database limit the Posts set before fetching associations.
contain() loads associations with a separate query that uses an IN (…) clause built from the IDs of the current page of root entities.matching() adds an INNER JOIN to the association, which can produce duplicate rows for a root entity when it has multiple matching associated rows.matching() is used in a paginated query; you must deduplicate in PHP or use a distinct subquery.When the comments.post_id column is indexed, the contain() query runs quickly (index lookup) and returns exactly the comments for the posts on the current page. The matching() join, by contrast, must scan the joined set before applying LIMIT/OFFSET, which can cause the database to process many more rows than needed, especially when a post has many comments.
comments.post_id has an index (e.g., ALTER TABLE comments ADD INDEX idx_post_id (post_id);).contain('Comments') and check DebugKit’s SQL log: you should see two queries per page (one for Posts, one for Comments).matching() with a callback, then deduplicate the result set:
$query = $posts->find()
->matching('Comments', function($q) {
return $q->where(['Comments.status' => 'published']);
})
->contain('Comments'); // optional if you still need the comments
$posts = $this->paginate($query);
// deduplicate
$uniquePosts = array_values(array_unique($posts, fn($p) => $p->getId()));
Are the foreign key columns (comments.post_id) indexed? If they are not, the contain() query may fall back to a full table scan, which could make matching() with a proper join relatively faster; in that case you would need to add the index or consider a custom distinct subquery.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.