How Eager Loading Works in ORM (With PHP Examples)

Digital graphic of a database icon with a lightning bolt symbolizing eager loading in ORM with PHP

When I first started using ORMs in PHP, I loved how clean and readable my code became. But I quickly ran into performance problems—things were getting sluggish, and I couldn’t figure out why. That’s when I discovered the magic (and danger) of lazy loading, and why eager loading is a tool every developer needs in their back pocket.

What is Eager Loading?

Eager loading is the practice of loading related data at the same time as the main data query, typically using a JOIN. Instead of fetching each related record one at a time (which leads to the dreaded N+1 problem), we ask for everything up front in a single efficient query.

Sample Database Schema

Users Table:

CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(255)
);

Posts Table:

CREATE TABLE posts (
    id INT PRIMARY KEY,
    user_id INT,
    title VARCHAR(255),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

A Common Example (Without Eager Loading)

Let’s say you’re pulling users and each one has a post.

$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');

// Step 1: Get all users
$stmt = $pdo->query("SELECT * FROM users");
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Step 2: For each user, fetch their posts (one query per user!)
foreach ($users as $user) {
    echo "User: {$user['name']}\n";

    $stmt = $pdo->prepare("SELECT * FROM posts WHERE user_id = ?");
    $stmt->execute([$user['id']]);
    $posts = $stmt->fetchAll(PDO::FETCH_ASSOC);

    foreach ($posts as $post) {
        echo "- {$post['title']}\n";
    }
}

This looks fine—but under the hood, this results in 1 query to fetch all users and 1 query per user to fetch the posts. So if you have 100 users, that’s 101 queries. Yikes.

With Eager Loading

$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');

// Step 1: Get all users
$stmt = $pdo->query("SELECT * FROM users");
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Step 2: Get all posts in one query
$stmt = $pdo->query("SELECT * FROM posts");
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Step 3: Group posts by user_id
$postsByUser = [];
foreach ($posts as $post) {
    $postsByUser[$post['user_id']][] = $post;
}

// Step 4: Display users and their posts
foreach ($users as $user) {
    echo "User: {$user['name']}\n";
    $userPosts = $postsByUser[$user['id']] ?? [];

    foreach ($userPosts as $post) {
        echo "- {$post['title']}\n";
    }
}

Now we’re down to 2 queries total—one for users, one for posts. Much better.

But, I know it still looks clunky having to group them in PHP so here’s another example – this time just using one query.

Eager Loading via JOIN

$stmt = $pdo->query("
    SELECT users.id AS user_id, users.name AS user_name,
           posts.id AS post_id, posts.title AS post_title
    FROM users
    LEFT JOIN posts ON posts.user_id = users.id
    ORDER BY users.id
");

$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Group and display
$currentUserId = null;

foreach ($rows as $row) {
    if ($row['user_id'] !== $currentUserId) {
        $currentUserId = $row['user_id'];
        echo "User: {$row['user_name']}\n";
    }

    if ($row['post_id']) {
        echo "- {$row['post_title']}\n";
    }
}

Cool huh?

Common Use-Cases for Eager Loading

  • Displaying lists with related data (e.g., users and roles, orders and products)
  • APIs that return nested JSON (e.g., user → posts → comments)
  • Dashboards that show summarized data across tables
  • Admin panels where you paginate through relational data
  • Search results with filters based on related models

Basically, if you’re planning to show related data alongside your main data every time, go eager.

When Not to Use Eager Loading

Eager loading isn’t always a free win. Avoid it when:

  • You don’t need the related data right away
  • You’re dealing with huge relationships and don’t want to balloon your query memory
  • You conditionally access the relationship (e.g., only sometimes showing the related data)
  • You’re fetching something with deeply nested relations that are rarely needed

Premature eager loading can lead to bigger, slower queries than necessary—especially when relationships are large or optional.

+ Pro Tip

  • Paginate your queries. Only load what you need. You don’t want to load all 500 users with 500 posts each right if you only need the 10 most recent posts of the 10 most active users. Capische?

Wrap Up

Understanding when and how to use eager loading can be the difference between a fast app and one that grinds to a halt under load. The trick is balance: eager load only what you know you’ll need, and keep an eye on your query logs to avoid surprises.

I recommend profiling your pages and learning what your ORM is actually doing under the hood. Once you’ve seen the N+1 monster in action, you’ll never forget to go eager again.