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.
Python code
54 linesimport 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
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
- Use ORMs like SQLAlchemy or Django with `selectinload()` or `joinedload()` for eager loading without raw SQL
- 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
More from Database scaling & optimization
- Approximate Count with HyperLogLog in Python medium
- B-Tree Insert and In-Order Traversal in Python hard
- Broadcast a Small Reference Table in Python easy
- Build a Full Text Search Index in Python medium
- Build a Partial Index Mock in Python for Database Filtering easy
- Composite index leftmost prefix in Python medium
Keep learning
Related tutorials and quizzes for this topic.