Start learning free

LESSON 127 OF 173 · BACKEND: SPEED

Fix the N+1 in Go

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 handler showing one order's items could get each item's quantity with its product's name in a single query:

query := "SELECT order_items.quantity, products.name " +
	"FROM order_items " +
	"JOIN products " +
	"ON products.id = order_items.product_id " +
	"WHERE order_items.order_id = ? "
rows := runQuery(query, orderID)

The + glues the string pieces into one string, so each piece ends with a space. Each row of the orders JOIN is one item, with its order's columns next to it, so an order with two items comes as two rows. A map from order id to a pointer to its Order 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 GET /customers/{id}/orders handler, build one query that joins orders and order_items for this customer, sorted by order id. Run it once, scan each row into an Order and an Item, and group the rows into orders with a map entries.


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


Hint
An order with two items comes as two rows with the same o.ID. Only when entries[o.ID] is nil, store &o in entries and append the same &o to result. Then add every row's item to entries[o.ID].Items. SELECT * gives 8 columns per row: the 3 the answer doesn't use go into &skip.


STEP 1 OF 12
    Ctrl + Enter to check

    Speed N Plus One in Go

    Keep going with Go1,600+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free