Skip to content

renaldid/n1detect

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

n1detect

Go Reference Go Version License: MIT Coverage

n1detect is a Go static analysis tool that detects N+1 database query patterns — one of the most common and costly performance bugs in database-driven applications.

It integrates with go vet, golangci-lint, and any tool built on the go/analysis framework.


What is an N+1 Query?

An N+1 query happens when you fetch a list of N records and then issue one additional query per record inside a loop:

// 1 query to get all user IDs
rows, _ := db.Query("SELECT id FROM users")
for rows.Next() {
    var id int
    rows.Scan(&id)
    // N queries — one per user!  <-- n1detect flags this
    db.QueryRow("SELECT * FROM orders WHERE user_id = ?", id)
}

This pattern produces N+1 round-trips to the database. With 1,000 users that's 1,001 queries instead of 1. Under load it becomes the primary source of database saturation, high latency, and connection pool exhaustion.

Fix: batch with JOIN or IN

// 1 query — always, regardless of user count
db.Query(`
    SELECT u.id, o.id, o.total
    FROM users u
    JOIN orders o ON o.user_id = u.id
`)

// or with an IN clause
db.Query("SELECT * FROM orders WHERE user_id IN (?)", userIDs)

Installation

Standalone CLI

go install github.com/renaldid/n1detect/cmd/n1detect@latest

Run on a module:

n1detect ./...

As a go vet plugin

go vet -vettool=$(which n1detect) ./...

With golangci-lint (custom linter)

Add to .golangci.yml:

linters-settings:
  custom:
    n1detect:
      path: n1detect
      description: Detects N+1 database query patterns
      original-url: github.com/renaldid/n1detect

Supported libraries

n1detect detects N+1 patterns across all major Go database libraries out of the box:

Library Detected types Detected methods
database/sql DB, Tx, Conn Query, QueryRow, QueryContext, QueryRowContext, Exec, ExecContext, Prepare, PrepareContext
gorm.io/gorm DB Find, First, Last, Take, Create, Save, Delete, Updates, Update, Scan, Row, Rows, Exec
github.com/jackc/pgx/v5 Conn Query, QueryRow, Exec, SendBatch
github.com/jackc/pgx/v5/pgxpool Pool Query, QueryRow, Exec, SendBatch
github.com/jmoiron/sqlx DB, Tx Query, QueryRow, QueryContext, QueryRowContext, Exec, ExecContext, Select, SelectContext, Get, GetContext

Example output

./service/user.go:42:3: potential N+1 query: DB.QueryRow called inside loop; consider batching or using JOIN
./repository/post.go:18:4: potential N+1 query: DB.Query called inside loop; consider batching or using JOIN
./store/order.go:31:3: potential N+1 query: Tx.Exec called inside loop; consider batching or using JOIN

Detected patterns

For loop

func loadTags(db *sql.DB, postIDs []int) {
    for _, id := range postIDs {
        db.Query("SELECT * FROM tags WHERE post_id = ?", id) // flagged
    }
}

Range loop

func deletePosts(tx *sql.Tx, ids []int) {
    for _, id := range ids {
        tx.Exec("DELETE FROM posts WHERE id = ?", id) // flagged
    }
}

GORM

func loadUserProfiles(db *gorm.DB, users []User) {
    for _, u := range users {
        var profile Profile
        db.First(&profile, "user_id = ?", u.ID) // flagged
    }
}

Function literal in loop

for _, id := range ids {
    go func() {
        db.QueryRow("SELECT * FROM t WHERE id = ?", id) // flagged
    }()
}

Nested loops

for _, dept := range departments {
    for _, emp := range dept.Employees {
        db.Query("SELECT * FROM salaries WHERE employee_id = ?", emp.ID) // flagged
    }
}

Custom patterns

Register your own database types and methods using WithPatterns:

import "github.com/renaldid/n1detect"

var myAnalyzer = n1detect.WithPatterns(
    n1detect.Pattern{
        PkgPath:  "github.com/myorg/mydb",
        TypeName: "Client",
        Methods:  []string{"Find", "Query", "Exec"},
    },
)

Use it with multichecker:

package main

import (
    "golang.org/x/tools/go/analysis/multichecker"
    "github.com/renaldid/n1detect"
)

func main() {
    multichecker.Main(
        n1detect.WithPatterns(/* your patterns */),
    )
}

Programmatic API

import "github.com/renaldid/n1detect"

// Use the default analyzer (all built-in patterns)
var Analyzer = n1detect.Analyzer

// Extend with custom patterns
var Extended = n1detect.WithPatterns(
    n1detect.Pattern{
        PkgPath:  "github.com/myorg/cache",
        TypeName: "DB",
        Methods:  []string{"Get", "Set"},
    },
)

n1detect.Analyzer implements analysis.Analyzer and works with any tool in the go/analysis ecosystem.


How it works

n1detect uses Go's type-checker — not string matching — to identify database calls:

  1. For each for or range loop in the AST, it collects all call expressions within the loop body.
  2. Each call is type-checked: the receiver's concrete type (e.g. *sql.DB) is resolved via go/types.
  3. The resolved type is matched against the pattern registry (PkgPath + TypeName + MethodName).
  4. A diagnostic is reported at the call site.

Because analysis is type-aware, there are no false positives from unrelated types that happen to share a method name (e.g. a custom Query() on your own struct).

Known limitation: interprocedural N+1 patterns (where the DB call is inside a helper function called from the loop) are not detected in v1. Only direct calls inside loop bodies are flagged.


False positives

n1detect only flags calls where the receiver is a known database type. These patterns are intentionally not flagged:

// Interface type — receiver is unknown at compile time
var q Querier
for _, id := range ids {
    q.Query(id) // NOT flagged
}

// Batch query outside loop — safe
db.Query("SELECT * FROM users WHERE id IN (?)", ids) // NOT flagged

// Custom struct with same method name
type MyService struct{}
func (s MyService) Query() {}

for range items {
    s.Query() // NOT flagged — MyService is not a registered DB type
}

Contributing

Issues and pull requests are welcome. Please open an issue before submitting large changes.


License

MIT


About

Go static analysis tool that detects N+1 database query patterns in for/range loops

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages