n1detect

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 11, 2026 License: MIT Imports: 6 Imported by: 0

README

n1detect

Go Reference Go Version 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


Documentation

Overview

Package n1detect provides a Go static analysis tool that detects N+1 database query patterns in Go source code.

What is an N+1 Query?

An N+1 query occurs when code executes one query to retrieve a list of N items and then executes an additional query for each item, resulting in N+1 total queries. This is a common performance anti-pattern.

Example of an N+1 pattern:

rows, _ := db.Query("SELECT id FROM users")
for rows.Next() {
    var id int
    rows.Scan(&id)
    db.QueryRow("SELECT * FROM orders WHERE user_id = ?", id) // N+1 here!
}

Instead, prefer a JOIN or an IN clause:

db.Query("SELECT u.id, o.* FROM users u JOIN orders o ON o.user_id = u.id")

Installation and Usage

Install the standalone binary:

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

Run against your packages:

n1detect ./...

Supported Libraries

The following database libraries are detected out of the box:

  • database/sql (DB, Tx, Conn)
  • gorm.io/gorm (DB)
  • github.com/jackc/pgx/v5 (Conn)
  • github.com/jackc/pgx/v5/pgxpool (Pool)
  • github.com/jmoiron/sqlx (DB, Tx)

Custom Patterns

You can extend detection with your own patterns using WithPatterns:

myPattern := n1detect.Pattern{
    PkgPath:  "myorg/mydb",
    TypeName: "Client",
    Methods:  []string{"Query", "Exec"},
}
analyzer := n1detect.WithPatterns(myPattern)

Integration with go vet

n1detect uses the golang.org/x/tools/go/analysis framework, which means it integrates natively with go vet:

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

Index

Constants

This section is empty.

Variables

View Source
var Analyzer = newAnalyzer(builtinPatterns)

Analyzer is the default n1detect analyzer using built-in patterns.

Functions

func WithPatterns

func WithPatterns(extra ...Pattern) *analysis.Analyzer

WithPatterns returns a new analyzer that uses the built-in patterns plus any extra patterns provided by the caller. Methods from duplicate PkgPath+TypeName entries are merged.

Types

type Pattern

type Pattern struct {
	PkgPath  string
	TypeName string
	Methods  []string
}

Pattern describes a set of methods on a named type that trigger N+1 detection.

func BuiltinPatterns added in v1.0.1

func BuiltinPatterns() []Pattern

BuiltinPatterns returns a deep copy of the default pattern set used by Analyzer. Modifying the returned slice does not affect Analyzer.

Directories

Path Synopsis
cmd
n1detect command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL