Start learning free

LESSON 127 OF 173 · BACKEND: SPEED

Fix the N+1

Practice

Measure first: the old GET /customers/1/orders ran one query for the customer's orders, then one more per order for that order's items. Customer 1 has 2 orders, so that was 3 queries; a customer with 50 orders would cost 51. This is the N+1 problem, and it hides in any loop that runs a query.

A JOIN brings rows of two tables together in one query. For example, a route showing one order's items could get each item's quantity with its product's name in a single query:

const query: string = [
  "SELECT order_items.quantity, products.name",
  "FROM order_items",
  "JOIN products",
  "ON products.id = order_items.product_id",
  "WHERE order_items.order_id = ?",
].join(" ");
const rows = runQuery(query).all(orderId) as { quantity: number; name: string }[];

.join(" ") glues the array's strings into one, with a space between them, so each clause can sit on its own line. Each row of the orders JOIN is one item, with its order's columns next to it: the starting code calls that shape OrderRow. An order with two items comes as two rows, and an object of type { [id: number]: OrderEntry } that maps each order id to its entry groups them back into orders.

Your code must give the same answers as before, with one query per request:

GET /customers/1/orders  →  200  [{"id": 1, "items": [{"product_id": 1, "quantity": 2}, {"product_id": 4, "quantity": 5}], "ordered": "2026-03-02", "status": "paid"}, {"id": 3, "items": [{"product_id": 2, "quantity": 3}, {"product_id": 4, "quantity": 10}], "ordered": "2026-04-05", "status": "paid"}]
GET /debug/queries       →  200  {"queries": 1}
GET /customers/3/orders  →  200  [{"id": 4, "items": [{"product_id": 5, "quantity": 1}, {"product_id": 1, "quantity": 1}], "ordered": "2026-04-18", "status": "shipped"}, {"id": 7, "items": [{"product_id": 1, "quantity": 4}, {"product_id": 2, "quantity": 2}], "ordered": "2026-05-30", "status": "paid"}]
GET /debug/queries       →  200  {"queries": 2}
GET /customers/4/orders  →  200  [{"id": 5, "items": [{"product_id": 6, "quantity": 2}, {"product_id": 3, "quantity": 1}], "ordered": "2026-05-07", "status": "paid"}]
GET /customers/5/orders  →  200  []
GET /debug/queries       →  200  {"queries": 4}

Your task: in the route for GET /customers/:customerId/orders, build one query that joins orders and order_items for this customer, sorted by order id. Run it once, keep its rows as OrderRow[], then group them into orders with an object entries.


Assignment
Follow the Coding Tutorial to replace the query-per-order loop with one JOIN and group its rows in TypeScript.


Hint
An order with two items comes as two rows with the same row.id. Make a new entry only when entries[row.id] is undefined, push that same entry to result, and push every row's item to entries[row.id].items.


STEP 1 OF 11
    Ctrl + Enter to check