systemdesign database/databaseengineering
It’s a common performance issue in application that interact with databases (ORMs).
Problem
Example:
- A list of Authors.
- Each author has books.
erDiagram
AUTHORS ||--o{ BOOKS : writes
AUTHORS {
int id PK
string name
string email
}
BOOKS {
int id PK
string title
int author_id FK
date published_at
}If we want to fetch author and their books,
authors := db.FindAllAuthors() // 1 query
for _, author := range authors {
books := db.FindBooksByAuthorID(author.ID) // N queries (1 per author)
}-
1 query to get all authors.
-
Then for each author, do 1 query to get their books.
-
Total Queries = N + 1 for N authors.
-
Inefficient: Each database call has overhead (network, parsing, execution).
-
Slow: If you have 1000 authors, that’s 1001 queries.
-
Scales poorly: Latency and load increase rapidly with data size.
Solutions
Eager Loading (Preloading, Fetch Join)
Load related data at the same time as the main query.
SELECT authors.*, books.*
FROM authors
LEFT JOIN books ON authors.id = books.author_id;ORMs provide prebuilt method for Eager loading.

// User represents a user in the system.
// gorm.Model includes fields: ID, CreatedAt, UpdatedAt, DeletedAt.
type User struct {
gorm.Model
Name string // User's name
Email string // User's email address
Addresses []Address // One-to-many relationship: a user has many addresses
}
// Address represents a physical address associated with a user.
type Address struct {
gorm.Model
UserID uint // Foreign key referencing User.ID
Street string // Street name of the address
City string // City of the address
}
// getAllUserWithAddress retrieves all users from the database
// and preloads their associated addresses to avoid the N+1 query problem.
func getAllUserWithAddress(db *gorm.DB) ([]User, error) {
var users []User
// Use Preload to fetch related addresses in a single optimized query
if err := db.Preload("Addresses").Find(&users).Error; err != nil {
return nil, err // Return error if query fails
}
return users, nil // Return the list of users with their addresses
}with eager loading,
SELECT * FROM users;
SELECT * FROM addresses WHERE user_id IN (1, 2, 3, 4);without eager loading,
SELECT * FROM users;
SELECT * FROM addresses WHERE user_id = 1;
SELECT * FROM addresses WHERE user_id = 2;
SELECT * FROM addresses WHERE user_id = 3;
SELECT * FROM addresses WHERE user_id = 4; Batch Loading
This technique is similar to eager loading, but queries are performed in batches.
Manual Join Queries
SELECT a.id, a.name, b.title
FROM authors a
JOIN books b ON a.id = b.author_id;Dataloader
Dataloader is a utility often used in GraphQL APIs that batched and cached database or API requests per request lifecycle. It used to prevent issues like the N+1 problem.