go 124 lines · 3 tabs

Batch-Load Related Rows with a Single IN Query to Avoid N+1

Shared by codesnips Aug 2026
3 tabs
package feed

import "time"

type Post struct {
	ID        int64
	AuthorID  int64
	Title     string
	Body      string
	CreatedAt time.Time
	Comments  []Comment
}

type Comment struct {
	ID        int64
	PostID    int64
	AuthorID  int64
	Body      string
	CreatedAt time.Time
}
3 files · go Explain with highlit

This snippet shows the classic dataloader technique for eliminating N+1 queries in Go: instead of issuing one SELECT per parent row to fetch its children, all the parent IDs are collected and their children are loaded with a single WHERE ... IN (...) query, then grouped back into the parents in memory.

In models.go, the domain types Post and Comment are defined along with a CommentsByPostID map that later holds each post's comments. Keeping the grouping structure explicit makes the assembly step obvious and avoids surprising the caller with lazy database access hidden behind a getter.

comment_loader.go contains the core of the pattern. LoadCommentsForPosts first walks the posts to build a de-duplicated slice of IDs; the seen set matters because the same author or parent could otherwise be requested twice, inflating the query and the result set. The helper buildInClause generates the positional placeholders $1, $2, ... that pgx and lib/pq expect, since Go's standard database/sql cannot expand a slice into an IN list automatically. The rows are scanned once and bucketed into a map[int64][]Comment keyed by post_id, giving O(1) attachment back onto each post. The early return on an empty ID slice avoids emitting a malformed IN () query, which is a common crash in naive implementations.

service.go wires it together: GetFeed fetches the page of posts with one query, then calls the loader exactly once. The result is two queries total regardless of page size — the defining property of this pattern — rather than 1 + N.

The main trade-off is memory and query-size: a very large ID list can exceed database parameter limits (Postgres caps bind parameters around 65535), so production code often chunks the IDs into batches. The approach also loads all children eagerly, which wastes work if the caller only needs a few. When those constraints are acceptable, batch-loading is the simplest and most predictable fix for N+1, and it composes naturally with request-scoped caching or a full dataloader library when nested relations appear.


Related snips

Share this code

Here's the card — post it anywhere.

Batch-Load Related Rows with a Single IN Query to Avoid N+1 — share card
Link copied