# ARGUS-A17: Forbidden Database Queries Inside Loops (N+1 Anti-Pattern) > **Rule Code:** `ARGUS-A17` > **Identifier:** `FORBIDDEN_QUERY_IN_LOOP` (Alias: `N_PLUS_ONE`) > **Severity:** `HIGH` (N+1 Latency Collapse & Connection Saturation Blocker) > **Category:** `Performance, Network I/O & Latency Scalability` > **Target Standards:** CWE-400 (Uncontrolled Resource Consumption), OWASP ASVS v4.0.3/v5.0 §V1.4.3, §V11.1.4, Database Optimization Standards --- ## 1. Overview & Core Invariant Individual database query executions (**`Query`**, **`QueryRow`**, **`Exec`**) **must never be called inside iterative application loops (`for`, `range`)** (the $N+1$ query anti-pattern). Iterative round-trips over network sockets incur severe serialization latency and lock up pool connections for extended durations. All multi-record operations must be restructured into set-based batch operations (`WHERE id = ANY($1)` or `pgx.Batch`). Legitimate exemptions are strictly limited to: 1. Small constant retry loops (`for range 3`). 2. Dedicated background event poller routines. --- ## 2. Technical Grounding & PostgreSQL Engine Realities ### 2.1. The Network Round-Trip Latency Multiplier ($N \times \text{RTT}$) Executing queries in a loop multiplies network Round-Trip Time (RTT). In a typical 1ms latency network environment, fetching 1,000 records sequentially inside a loop blocks the thread for over **1 second** purely on TCP socket overhead, ignoring PostgreSQL's internal query execution capabilities. ### 2.2. Connection Pool Starvation A single goroutine holding a database connection while performing hundreds of sequential round-trips monopolizes the connection pool. Under concurrent user traffic, this results in connection pool exhaustion and cascading HTTP timeout errors. ```mermaid flowchart TD subgraph N1 ["N+1 Query Anti-Pattern (HAZARDOUS)"] direction TB Loop1["for _, id := range ids (1,000 items)"] --> S1["Query 1 (1ms RTT)"] S1 --> S2["Query 2 (1ms RTT)"] S2 --> S3["Query ..."] S3 --> SN["Query 1000 (1ms RTT)"] SN --> Total1["Total Time: > 1,000ms Blocking Pool Connection!"] end subgraph SET ["Set-Based / Batch Execution (COMPLIANT)"] direction TB Batch["SELECT ... WHERE id = ANY($1::uuid[])"] --> Fast["1 Single Network Round-Trip (1ms RTT)"] Fast --> Engine["PostgreSQL Executes Vectorized Array Scan"] Engine --> Total2["Total Time: 2ms (99.8% Latency Reduction)"] end ``` --- ## 3. How Argus Detects Violations (Static Analysis Architecture) Argus combines AST loop depth tracking with Go type-checking and local call graph traversal: ```mermaid flowchart LR Scan["Scan Go AST
(Exclude _test.go)"] --> LoopWalk["loop_walker.go:
Track ForStmt / RangeStmt Scope"] LoopWalk --> CallDetect["call_detector.go:
Type-Checked Receiver Detection"] CallDetect --> IsDB{"Receiver is *pgxpool.Pool,
pgx.Tx, or *sql.DB?"} IsDB -->|Yes| RetryCheck{"Is Constant Retry Loop
(for range 3)?"} IsDB -->|No| HelperCheck["callgraph_check.go:
Inspect Local Helper Functions"] HelperCheck -->|Contains DB Call| RetryCheck HelperCheck -->|No DB Call| Pass["Pass (Non-DB Loop)"] RetryCheck -->|Yes| Pass["Pass (Exempted Retry)"] RetryCheck -->|No| Report["Report HIGH Violation:
N+1 Query Inside Loop"] ``` 1. **Loop Walker (`loop_walker.go`):** Tracks nested loop depths and ignores constant small retry loops (`for range 3`). 2. **Call Detector (`call_detector.go`):** Validates receiver types using `pass.TypesInfo` to eliminate false positives on non-database `Exec()`/`Query()` methods. 3. **Call Graph Inspector (`callgraph_check.go`):** Detects internal helper function invocations inside loops that secretly trigger database queries. --- ## 4. Vulnerability & Risk Taxonomy | Failure Mode | Technical Impact | Risk Severity | | :----------------------------------- | :------------------------------------------------------------------------- | :------------ | | **N+1 `QueryRow` in Range Loop** | Multiplies network round-trips linearly, inflating API latency to seconds. | **HIGH** | | **N+1 `Exec` in Loop** | Causes connection pool starvation and degrades database write throughput. | **HIGH** | | **Hidden Helper DB Queries in Loop** | Obscures N+1 queries behind abstraction layers. | **HIGH** | --- ## 5. Non-Compliant Code Patterns (Bad Examples) ### Example 1: QueryRow Inside Range Loop ```go // VIOLATION: Executes 1 database query per item in slice func GetUsers(ctx context.Context, pool *pgxpool.Pool, userIDs []int) ([]User, error) { var users []User for _, id := range userIDs { // Flagged: Database query executed inside loop (N+1 antipattern) var u User err := pool.QueryRow(ctx, "SELECT id, name FROM users WHERE id = $1", id).Scan(&u.ID, &u.Name) if err != nil { return nil, err } users = append(users, u) } return users, nil } ``` ### Example 2: Hidden Helper Method Invocation ```go // VIOLATION: Loop calls helper that executes database queries func (s *Service) ProcessOrders(ctx context.Context, orderIDs []int) error { for _, id := range orderIDs { // Flagged: Helper function executes DB query inside loop if err := s.updateOrderStatus(ctx, id); err != nil { return err } } return nil } ``` --- ## 6. Compliant Implementation Patterns (Good Examples) ### Solution 1: Set-Based Query with PostgreSQL Array `ANY($1)` ```go // COMPLIANT: Single network round-trip fetching all records func GetUsers(ctx context.Context, pool *pgxpool.Pool, userIDs []int) ([]User, error) { const query = "SELECT id, name FROM users WHERE id = ANY($1)" rows, err := pool.Query(ctx, query, userIDs) if err != nil { return nil, err } defer rows.Close() return scanUsers(rows) } ``` ### Solution 2: Batch Pipeline Execution (`pgx.Batch`) ```go // COMPLIANT: Pipelined batch execution in a single round-trip func UpdateBalances(ctx context.Context, pool *pgxpool.Pool, updates []BalanceUpdate) error { batch := &pgx.Batch{} for _, u := range updates { batch.Queue("UPDATE accounts SET balance = balance + $1 WHERE id = $2", u.Amount, u.AccountID) } br := pool.SendBatch(ctx, batch) defer br.Close() return br.Close() } ``` --- ## 7. How to Suppress (Ignore Directives) For background poller daemons that poll database tables for work items: ```go // argus:ignore-a17 background polling worker query in continuous loop for { rows, err := pool.Query(ctx, "SELECT id FROM tasks WHERE status = 'pending' LIMIT 10") // ... } ``` Alternatively, use the canonical identifier alias: ```go // argus:ignore FORBIDDEN_QUERY_IN_LOOP event poller queue worker rows, err := pool.Query(ctx, pollEventsQuery) ``` --- ## 8. Configuration Reference (`.argus.yaml`) Enable or configure this rule in `.argus.yaml`: ```yaml rules: ARGUS-A17: enabled: true ```