Skip to content
wannacode

ORM and the N+1 problem

In short: An ORM maps table rows to objects and writes the SQL for you, and the N+1 problem is when it runs one extra query for every row instead of a single query.

Problem

The slow orders page

The “Orders” page in the admin panel shows the order number, the date and the customer’s name. The shop has 60 orders, and the page is slow to open. The database log shows 61 queries for one page view. Where do they come from, when the code has no loop with SQL in it?

The idea

ORM: a class instead of the table

An ORM (object-relational mapping) is a library that links a class in the program to a table in the database. You describe the class Order and which table it is, and the ORM writes the SQL for you when you ask for the orders:

@Entity
@Table(name = "orders")
class Order {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
@Column(name = "ordered_at")
private LocalDate orderedAt;
}
List<Order> orders = em.createQuery("SELECT o FROM Order o ORDER BY o.id", Order.class)
.getResultList();

These are Hibernate/JPA in Java, Entity Framework Core in C#, SQLAlchemy in Python and Prisma in JavaScript. You gain less repetitive code, objects instead of rows, and the same program for different databases. You pay with SQL you no longer see: the ORM decides how many queries to send.

More precisely, if you are curious

The ORM examples are not run in the site's checks, because their packages are not there. A real program also needs some configuration for them: the database address, the Customer class and the context (EntityManager, DbContext, Session, PrismaClient).

See it

N+1

Here is the same thing without an ORM, but with the same mistake: first the orders, then the customer of each. The program counts the queries it sends:

try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) {
setup(conn);
int queries = 0;
List<int[]> orders = new ArrayList<>();
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT id, customer_id FROM orders ORDER BY id")) {
queries++;
while (rs.next()) {
orders.add(new int[] { rs.getInt("id"), rs.getInt("customer_id") });
}
}
for (int[] order : orders) {
try (PreparedStatement ps = conn.prepareStatement("SELECT name FROM customers WHERE id = ?")) {
ps.setInt(1, order[1]);
queries++;
try (ResultSet rs = ps.executeQuery()) {
rs.next();
System.out.println("order " + order[0] + ": " + rs.getString("name"));
}
}
}
System.out.println("queries: " + queries);
}
Output
order 1: Radost Hristova
order 2: Georgi Petrov
order 3: Maria Ivanova
order 4: Ivana Koleva
order 5: Radost Hristova
order 6: Maria Ivanova
queries: 7

Six orders, seven queries: one for the orders (1) and one for the customer of each (N). With 60 orders it is 61. With an ORM the same happens, only the loop is invisible: o.getCustomer().getName() looks like reading a field, but it is really a query.

for (Order o : orders) {
System.out.println(o.getId() + ": " + o.getCustomer().getName()); // a query for every customer
}
// The fix: the customers come in the same query.
em.createQuery("SELECT o FROM Order o JOIN FETCH o.customer ORDER BY o.id", Order.class)

The idea

One query

The database can join the orders and the customers itself and return everything at once:

try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) {
setup(conn);
String sql = """
SELECT o.id, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
ORDER BY o.id
""";
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) {
System.out.println("order " + rs.getInt("id") + ": " + rs.getString("name"));
}
}
System.out.println("queries: 1");
}
Output
order 1: Radost Hristova
order 2: Georgi Petrov
order 3: Maria Ivanova
order 4: Ivana Koleva
order 5: Radost Hristova
order 6: Maria Ivanova
queries: 1

The same rows with one query. For the shop’s page, this is all it takes:

SQLdatabase: shop

The difference is not in the SQL of each query but in the trip to the database. Every query is a round trip over the network, usually under a millisecond inside one data centre, and tens of milliseconds if the database is far away. 61 of those are noticeable, 6,001 bring the page to a halt.

Try it

The latest orders

The dashboard shows the latest 5 orders: the number, the date, the customer’s name and email, the newest first (with the same date, the higher number on top). With one query.

SQLdatabase: shop

Exercise

How many orders each customer has

The “Customers” page shows every customer with the number of their orders (column orders), in alphabetical order. Without N+1, that is, without a query for each customer, and without leaving out the customers who have no orders.

SQLdatabase: shop

Test

Check yourself

1. A page shows 50 orders with the customer’s name. The program reads the orders with one query, and then the customer of each with a separate one. How many queries is that?
2. Why does N+1 not show while you are writing the program?
3. What fixes N+1 in an ORM?
4. An ORM writes the SQL for you. Do you still need to know SQL?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·