ProcessWire 3.x: Querying Related Pages Without the N+1 Penalty
ProcessWire makes relational content easy, but looping through PageArrays can trigger N+1 queries. This post shows how ->preload() and ->paginate() keep your templates fast and predictable.
16 Jun 2026, 00:28 UTC

Problem: N+1 Queries When Looping Related Pages
In ProcessWire 3.x, the $pages API makes it easy to fetch related content. But when you loop through a PageArray and access field data, each access can trigger a separate database query. This pattern kills performance on pages with many items: the first page loads one query, and every subsequent field access adds another.
Thesis: Preload and Pagination Fix the Flow
The solution isnt to avoid relational queries, but to control when and how field data loads. ProcessWire provides ->preload() and ->paginate() to bring the data you need into a single round-trip.
1. Selector-based Relational Queries
ProcessWire selectors mirror SQL WHERE clauses but operate on page properties and fields. To fetch articles tagged with a specific topic:
$related = $pages->find("template=article, tags=tech");This returns a PageArray that is lazy-loaded. Field values are fetched on first access.
2. The Preload Pattern
If you need multiple fields from each page in a loop, call ->preload() on the PageArray before iterating. This bundles the field requests into one query per field:
$items = $pages->find("template=article, related=123")->preload("title, body, date");
foreach ($items as $item) {
// Field data already in memory; no extra queries
echo $item->title;
}Where: Any ProcessWire template file (site/templates/) or PHP context with the $pages API available.
Permissions: Read access to the page tree; no superuser rights needed for read-only queries.
Expected check: Run the loop before and after ->preload() and compare $wire->page->db->getNumQueries(); the count should drop after preloading.
Risk: ->preload() fetches every requested field, even ones you might not use in the current loop. Large fields (e.g., body text) add payload weight. Only preload the fields you actually render.
3. Pagination That Works
For listing pages on the frontend, use ->paginate() to set limits and navigate pages automatically:
$articles = $pages->find("template=article")->paginate(10);
echo $articles->getTotalPages(); // 27
echo $articles->getPageNum(); // 1The method adjusts offsets internally, so you can render next and prev links without manual math. ProcessWire also provides ->getNextPage() and ->getPrevPage() for direct Page objects.
Where: In a template file rendered by ProcessWire, after the $pages API is bootstrapped.
Permissions: Frontend visitors see paginated lists; editors need appropriate template access to modify query conditions.
Expected check: Call $articles->getTotalPages() and verify it matches the number of results divided by the limit (rounded up). Call $articles->getPageNum() to confirm the current page index.
Risk: If you manually set an offset beyond the total result set, ->find() returns an empty PageArray and pagination links may break. Always rely on the ->paginate() return values rather than computing offsets by hand.
Trade-off: Preload Loads All Requested Fields
->preload() fetches every requested field, even ones you might not use in the current loop. If a field is large (e.g., a body text column), it adds payload weight. Weigh the convenience against the extra data transferred.
Practical check: After preloading, inspect the generated SQL (enable ProcessWire debug logging or use $pages->getUnformatted()) to see exactly which columns are pulled. Adjust the field list if you only need a subset.
Actionable Closing
- Identify the fields you actually need in the loop.
- Call ->preload() with those field names before the foreach.
- Use ->paginate() for any listing that needs navigation, and test getTotalPages and getPageNum to confirm offset handling.
- Register a hook in site/ready.php if you want to customize Page::save behavior, and verify it logs via the process log.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.