Skip to content
cccskills
FreePublished on GitHub

sql-query-performance

Use when writing or changing a query against PostgreSQL from Go — avoiding N+1 loops, reading EXPLAIN output, indexing, and keyset pagination

The English summary is being prepared. Showing the original description.

Works withPostgreSQL
See how to install

Files included (1)

  • SKILL.md4.3 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

SQL Query Performance (Go)

Overview

Go has no ORM to blame for an N+1 — it's a loop you wrote yourself, calling the repository once per item instead of once for the batch. This skill is the Go-side half of query performance; java-persistence covers the JPA/Hibernate side.

Core principle: one round trip per request-shaped operation, not one per row.

N+1 in Go

// ❌ one query per item: N+1
for _, id := range ids {
    task, err := repo.Get(ctx, id)   // round trip per id
    ...
}

// ✅ one query for the batch
tasks, err := repo.GetMany(ctx, ids)   // SELECT ... WHERE id = ANY($1)
SELECT id, title, status FROM tasks WHERE id = ANY($1);

Bind ids as a []uuid.UUID (pgx encodes a Go slice as a Postgres array directly — no manual IN (...) string building). For a write-side batch, use pgx.Batch to pipeline multiple statements over one round trip instead of looping with individual Exec calls.

Database hygiene

  • Every call takes a context.Context — QueryContext/ExecContext (database/sql) or pgx's ctx parameter — so a slow query is cancelable and traceable.
  • defer rows.Close() immediately after a successful Query, and check rows.Err() after the loop — a Query that returns rows can still fail mid-stream, and an unclosed Rows leaks the connection.
  • Use Exec, not Query, for statements that return no rows (INSERT/UPDATE/DELETE without RETURNING) — Query leaves a result set open that must still be drained.
  • No SELECT * in application code — name the columns, so an added column doesn't silently change scan order or payload size.

Reading EXPLAIN

psql "$DATABASE_URL" -c "EXPLAIN (ANALYZE, BUFFERS) <query with literal values, not placeholders>"

Run it against realistically seeded data, not an empty table — an empty-table plan hides the index Postgres would actually need. Read for:

  • Seq Scan on a table bigger than a few thousand rows → missing index.
  • Estimated vs actual row counts far apart → stale statistics (ANALYZE <table>) or a predicate the planner can't estimate well.
  • A Sort that spills to disk (Sort Method: external merge) → needs work_mem tuning or an index that avoids the sort.

Indexes

  • Postgres does not auto-index foreign-key columns — add one explicitly for every FK you join or filter on.
  • Composite index column order follows the leftmost-prefix rule: an index on (a, b) serves queries filtering on a alone or a AND b, not b alone.
  • A partial index (WHERE status = 'open') for a hot subset of a much larger table.
  • A covering index (INCLUDE (col)) to let an index-only scan satisfy a query without a heap fetch.

See postgres-migrations for how to add an index on a live table without locking it.

Query-count regression test

Wrap the pool/connection to count statements in a test, or use pg_stat_statements where the test DB has it enabled, and assert the count stays constant as the dataset grows — this is what catches an N+1 before it ships, the same way java-persistence's Hibernate-statistics assertion does on the Java side.

Keyset pagination

See api-design-conventions for the full pagination guidance; the query shape is:

SELECT ... FROM tasks
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT $3 + 1;   -- fetch one extra row to know has_more

Common Mistakes

  • A loop calling the repository once per id instead of a batched = ANY($1) query.
  • rows.Close() missing or only deferred conditionally (e.g. after an early if err != nil { return } that skips the defer).
  • rows.Err() never checked after the loop.
  • Adding a filter/join column with no matching index.
  • Reading EXPLAIN against an empty or tiny local table and concluding the query is fine.

Red Flags

  • Query count in a test or log scales linearly with row count.
  • Seq Scan on a table expected to hold more than a few thousand rows.
  • A new WHERE/JOIN column with no migration adding its index.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 20, 2026

Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 20, 2026

Use on every UI change - semantic HTML, labels for controls, keyboard-navigable dialogs/menus, visible focus, and never color as the only signal

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 20, 2026

Use when a task changes any screen, form, dialog, menu or control - Lighthouse/axe scan of the changed screens, a keyboard walk, and the thresholds that fail a task

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 20, 2026

How to work a task returned with review, QA or UAT findings. Use when a task is in need_revision or PR review comments are in your context.

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 20, 2026

Use when the repository keeps its alerting in code, or a person asks for alerts outside TaskTrooper - PrometheusRule YAML, a Cloud Monitoring alert policy in Terraform, a Datadog monitor, each routed to TaskTrooper's alert ingest URL so it opens an incident here

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 20, 2026

See all skills by makifbaysal

Report a problem with this skill