How to Eager Load with JOIN to Reduce N+1 Queries in Python

Demonstrates eager loading with a SQL JOIN to reduce N+1 query patterns down to a single database call when fetching related data.

Medium Python 3.9+ Aug 9, 2026 Database scaling & optimization 16 views 0 copies

Python code

54 lines
Python 3.9+
import sqlite3


def eager_load_join_reduce(mock_db_path=":memory:"):
    """Demonstrate eager loading where joins reduce query count from N+1 to 1."""
    conn = sqlite3.connect(mock_db_path)
    cursor = conn.cursor()
    cursor.executescript(
        """
        CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT);
        CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author_id INTEGER);
        INSERT INTO authors VALUES (1, 'Tolkien'), (2, 'Asimov');
        INSERT INTO books VALUES
            (1, 'The Hobbit', 1),
            (2, 'Foundation', 2),
            (3, 'I, Robot', 2);
        """
    )
    conn.commit()

    # Lazy loading (N+1): per author, query the DB
    cursor.execute("SELECT id, name FROM authors")
    authors = cursor.fetchall()
    lazy_queries = 1  # authors query
    lazy_books = {}
    for author_id, _ in authors:
        cursor.execute("SELECT title FROM books WHERE author_id=?", (author_id,))
        lazy_books[author_id] = [title for (title,) in cursor.fetchall()]
        lazy_queries += 1

    # Eager loaded join: single query fetches nested data
    cursor.execute(
        """
        SELECT a.id, a.name, b.title
        FROM authors a
        LEFT JOIN books b ON b.author_id = a.id
        ORDER BY a.id
        """
    )
    rows = cursor.fetchall()
    eager_queries = 1
    eager_books = {}
    for author_id, name, title in rows:
        eager_books.setdefault(author_id, {"name": name, "books": []})["books"].append(title)

    conn.close()

    print(f"Lazy queries: {lazy_queries} -> authors: {lazy_books}")
    print(f"Eager queries: {eager_queries} -> authors: {eager_books}")
    assert lazy_queries > eager_queries, "Eager should use fewer queries"


if __name__ == "__main__":
    eager_load_join_reduce()

Output

stdout
Lazy queries: 3 -> authors: {1: ['The Hobbit'], 2: ['Foundation', 'I, Robot']}
Eager queries: 1 -> authors: {1: {'name': 'Tolkien', 'books': ['The Hobbit']}, 2: {'name': 'Asimov', 'books': ['Foundation', 'I, Robot']}}

How it works

The lazy approach issues one query per author plus the initial authors query, creating the classic N+1 problem where 2 authors produce 3 total queries. The eager approach uses a single LEFT JOIN that retrieves all nested data in one round trip, reducing the query count to exactly 1. The setdefault idiom in the loop groups joined rows into a nested dictionary structure, mapping each author to their books. The assertion verifies that lazy queries always exceed eager queries, proving the performance benefit.

Common mistakes

  • Forgetting that INNER JOIN drops authors with no books — use LEFT JOIN to preserve all parent rows
  • Not handling NULL titles when authors have no books after the LEFT JOIN
  • Assuming eager loading always helps — it can over-fetch large result sets if the join produces massive duplication

Variations

  1. Use ORMs like SQLAlchemy or Django with `selectinload()` or `joinedload()` for eager loading without raw SQL
  2. Batch queries with `WHERE id IN (...)` across author IDs as an alternative to JOIN

Real-world use cases

  • Rendering a blog list page where each post needs its comments — avoids hundreds of queries per page view.
  • Building an API endpoint that returns users with their orders, reducing database round trips in one request.
  • Generating dashboard reports that group sales by region and need product details fetched together.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Database scaling & optimization

Related tutorials and quizzes for this topic.