Yes, that's right.
Because it causes a massive spike in network round-trips and drastically slows down your application. Each database query requires a separate round-trip.
An N+1 query happens when your application fetches a collection of records and then issues an additional query for each record to load related data. What starts as a single database query can quickly turn into hundreds or thousands of queries as the dataset grows.
Let's say we want to fetch all appointments along with their patients.
const appointments = await appointmentRepository.find();
for (const appointment of appointments) {
appointment.patient = await patientRepository.findOne({
where: { id: appointment.patientId },
});
}
At first glance, nothing looks wrong. We fetch all appointments and then load the patient for each appointment.
The problem becomes obvious when we look at the queries being executed.
SELECT * FROM appointments;
SELECT * FROM patients WHERE id = 1;
SELECT * FROM patients WHERE id = 2;
SELECT * FROM patients WHERE id = 3;
...
If there are 500 appointments, we've just executed:
1 query for appointments
500 queries for patients
Total: 501 queries
The endpoint still works, but the database is doing 500 times more work than necessary.
A better approach is to fetch everything in a single query.
const appointments = await appointmentRepository
.createQueryBuilder("appointment")
.leftJoinAndSelect("appointment.patient", "patient")
.getMany();
Which translates roughly to:
SELECT
a.*,
p.*
FROM appointments a
LEFT JOIN patients p
ON p.id = a.patient_id;
Now we're executing a single query regardless of whether there are 10 appointments or 10,000.
That's the difference between code that works and code that scales.
How to Fix N+1 Queries
Using a JOIN is usually the simplest solution, but it's not the only one.
1. Fetch Related Data with Joins
This is the approach we used above.
const appointments = await appointmentRepository
.createQueryBuilder("appointment")
.leftJoinAndSelect("appointment.patient", "patient")
.getMany();
Instead of asking the database for each patient individually, we ask for everything in a single query.
2. Batch Related Records
Sometimes joining multiple tables isn't ideal, especially when the relationships become complex. In those cases, batching can be a good alternative.
SELECT * FROM appointments;
SELECT * FROM patients
WHERE id IN (1, 2, 3, 4, 5);
Now we're making:
1 query for appointments
1 query for patients
instead of:
1 query for appointments
500 queries for patients
The database still receives only a small number of requests regardless of how many appointments are returned.
3. Use ORM Relation Loading
Most modern ORMs already provide ways to load related data efficiently.
const appointments = await appointmentRepository.find({
relations: {
patient: true,
},
});
This allows the ORM to fetch the required relations up front instead of triggering additional queries while iterating through records.
Final Thoughts
N+1 queries are dangerous because they don't look like a problem when reading the code.
The API works.
The response is correct.
The tests pass.
But every additional record increases the number of database queries being executed.
The easiest way to catch them is to keep an eye on your query logs. If you notice the same query being executed repeatedly for each record in a collection, chances are you've run into an N+1 problem.
A query that runs once is usually harmless.
The same query running 500 times rarely is.
