Home Database sqlc: Type-Safe SQL Query Compilation for Go Developers
Intermediate 3 min · July 13, 2026

sqlc: Type-Safe SQL Query Compilation for Go Developers

Learn sqlc: a tool that generates type-safe Go code from SQL queries.

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 13, 2026
last updated
2,165
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of SQL (SELECT, INSERT, UPDATE, DELETE).
  • Familiarity with Go programming language.
  • Go installed on your machine (version 1.18+).
  • A database (PostgreSQL, MySQL, or SQLite) for testing.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • sqlc compiles SQL queries into type-safe Go code, catching errors at compile time.
  • It supports PostgreSQL, MySQL, and SQLite.
  • No ORM overhead: you write raw SQL, sqlc generates Go functions.
  • Ideal for projects where performance and type safety are critical.
  • Easy to integrate with existing Go projects via go generate.
✦ Definition~90s read
What is sqlc?

sqlc is a code generation tool that compiles SQL queries into type-safe Go code, giving you compile-time safety and full control over your database interactions.

Imagine you have a recipe book (SQL queries) and a chef (your Go code).
Plain-English First

Imagine you have a recipe book (SQL queries) and a chef (your Go code). Normally, you'd write the recipe in English, then the chef interprets it, but sometimes misreads and burns the dish. sqlc is like a translator that converts your recipe into precise instructions the chef can follow perfectly every time, catching mistakes before cooking starts.

In modern backend development, Go is praised for its performance and simplicity. However, when it comes to database interactions, developers often face a trade-off: use an ORM for convenience but sacrifice control and performance, or write raw SQL and manually map results, risking runtime errors and boilerplate. sqlc offers a third path: it compiles your SQL queries into type-safe Go code, giving you the best of both worlds. With sqlc, you write standard SQL, run a command, and get generated functions that return strongly-typed structs. This eliminates entire classes of bugs (e.g., column name typos, type mismatches) and reduces boilerplate. sqlc supports PostgreSQL, MySQL, and SQLite, and integrates seamlessly into Go projects via go generate. In this tutorial, you'll learn how to set up sqlc, write queries, generate code, and use it in production. We'll cover CRUD operations, advanced queries, and best practices to avoid common pitfalls. By the end, you'll be able to ship database code faster and with more confidence.

What is sqlc and Why Use It?

sqlc is a code generation tool that takes SQL queries and produces type-safe Go code. Unlike ORMs that abstract SQL, sqlc keeps you writing raw SQL but eliminates the manual mapping step. This means you get compile-time checks for column names, types, and nullability. sqlc supports PostgreSQL, MySQL, and SQLite. It's particularly useful for projects that require high performance, as there is no runtime reflection or query building overhead. Additionally, sqlc integrates with go generate, making it easy to regenerate code whenever your schema or queries change. The generated code is idiomatic Go, using standard database/sql interfaces, so it works with any database driver.

sqlc.yamlYAML
1
2
3
4
5
6
7
8
9
10
version: "2"
sql:
  - engine: "postgresql"
    schema: "schema.sql"
    queries: "query.sql"
    gen:
      go:
        package: "db"
        out: "db"
        sql_package: "pgx/v5"
🔥Getting Started
📊 Production Insight
In production, always run sqlc vet in CI to catch schema-query mismatches before deployment.
🎯 Key Takeaway
sqlc generates type-safe Go code from SQL, combining the safety of ORMs with the control of raw SQL.

Setting Up sqlc: Schema and Configuration

To use sqlc, you need a database schema file (DDL) and a queries file (DML). The configuration file sqlc.yaml tells sqlc where to find these files and how to generate Go code. Below is an example for PostgreSQL. The schema file defines tables, and the queries file contains your SQL statements with special annotations. sqlc uses these annotations to determine the method name and return type. For example, -- name: GetUser :one indicates a query that returns a single row. The generated code will include a method GetUser that returns a struct and an error.

schema.sqlSQL
1
2
3
4
5
CREATE TABLE users (
    id   SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL
);
💡Schema Best Practice
📊 Production Insight
Use a single source of truth for your schema, e.g., a migration tool like golang-migrate, and generate sqlc code after each migration.
🎯 Key Takeaway
Define your schema in a DDL file and configure sqlc with sqlc.yaml to point to it.

Writing Queries with sqlc Annotations

sqlc queries are standard SQL with a comment annotation that specifies the method name and result type. The annotation format is -- name: <MethodName> <:one|:many|:exec>. :one returns a single row (or error if no rows), :many returns a slice, and :exec returns a result (for INSERT/UPDATE/DELETE without returning rows). You can also use :execrows to get the number of affected rows. Parameters are automatically inferred from the query; sqlc uses $1, $2, etc. for PostgreSQL, and ? for MySQL. The generated function will accept these parameters as arguments.

query.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
-- name: GetUser :one
SELECT id, name, email FROM users WHERE id = $1;

-- name: ListUsers :many
SELECT id, name, email FROM users ORDER BY name;

-- name: CreateUser :exec
INSERT INTO users (name, email) VALUES ($1, $2);

-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;
⚠ Parameter Order
📊 Production Insight
For complex queries, use CTEs or subqueries; sqlc handles them correctly as long as the final SELECT defines the return columns.
🎯 Key Takeaway
Annotate each query with :one, :many, or :exec to control the generated method signature.

Generating and Using the Go Code

Run sqlc generate in the directory containing sqlc.yaml. This produces a Go package with generated functions. Each function accepts a context.Context and a sql.DB (or pgxpool.Pool if using pgx) and returns the appropriate types. The generated code uses database/sql interfaces, so you can mock it for testing. Below is an example of using the generated code in a Go application. Note that sqlc also generates a Queries struct that holds a *sql.DB, allowing you to call methods on it.

main.goGO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
    "context"
    "database/sql"
    "log"
    "yourproject/db"
)

func main() {
    conn, err := sql.Open("postgres", "postgres://user:pass@localhost/dbname")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    queries := db.New(conn)

    // Create a user
    err = queries.CreateUser(context.Background(), "Alice", "alice@example.com")
    if err != nil {
        log.Fatal(err)
    }

    // Get user by ID
    user, err := queries.GetUser(context.Background(), 1)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("User: %+v", user)

    // List all users
    users, err := queries.ListUsers(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    for _, u := range users {
        log.Printf("User: %+v", u)
    }
}
💡Integration with go generate
📊 Production Insight
Use connection pooling (e.g., pgxpool) for production to avoid opening/closing connections frequently.
🎯 Key Takeaway
sqlc generates a Queries struct with methods that accept context and parameters, returning strongly-typed results.

Advanced Queries: Joins, Aggregates, and Subqueries

sqlc supports complex SQL including joins, aggregates, subqueries, and CTEs. The generated struct will contain fields for all selected columns. For joins, you can either select specific columns or use sqlc.embed to embed a whole table's columns. sqlc.embed is a special annotation that tells sqlc to include all columns from a table, which is useful for JOINs. For example, -- name: GetUserWithOrders :many with a JOIN can embed both user and order fields. sqlc also handles nullable columns by generating pointer types (e.g., *string) for columns that are nullable in the schema.

query_advanced.sqlSQL
1
2
3
4
5
6
7
8
9
10
-- name: GetUserWithOrders :many
SELECT sqlc.embed(users), sqlc.embed(orders)
FROM users
JOIN orders ON users.id = orders.user_id
WHERE users.id = $1;

-- name: CountOrdersByUser :many
SELECT user_id, COUNT(*) as order_count
FROM orders
GROUP BY user_id;
🔥sqlc.embed
📊 Production Insight
For large result sets, use pagination with LIMIT and OFFSET to avoid memory issues.
🎯 Key Takeaway
sqlc handles joins and aggregates seamlessly; use sqlc.embed to avoid listing all columns manually.

Handling Transactions and Batch Operations

sqlc generated methods work with transactions by accepting a sql.Tx instead of sql.DB. You can create a Queries instance with a transaction using db.New(tx). This allows you to perform multiple operations atomically. For batch operations, you can use :exec for each statement within a transaction. sqlc does not generate batch insert methods, but you can use PostgreSQL's COPY or write a loop. Below is an example of using a transaction.

transaction.goGO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
    log.Fatal(err)
}
defer tx.Rollback()

q := db.New(tx)

// Create user and order in a transaction
userID, err := q.CreateUser(ctx, "Bob", "bob@example.com")
if err != nil {
    log.Fatal(err)
}
err = q.CreateOrder(ctx, userID, "item1")
if err != nil {
    log.Fatal(err)
}

tx.Commit()
⚠ Transaction Safety
📊 Production Insight
For high-throughput systems, consider using a connection pool and keep transactions short to avoid lock contention.
🎯 Key Takeaway
Use db.New(tx) to run queries within a transaction, ensuring atomicity.

Testing with sqlc Generated Code

Testing database code is crucial. sqlc generates interfaces that can be mocked. You can use the Querier interface (generated by sqlc) to create mock implementations. Alternatively, use a test database with real queries. For unit tests, you can use database/sql mock drivers like go-sqlmock to simulate database responses. For integration tests, use a temporary database (e.g., via Docker) and run migrations before tests. sqlc's generated code is straightforward to test because it's just functions that take a context and a DB handle.

test_example.goGO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func TestGetUser(t *testing.T) {
    // Using go-sqlmock
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close()

    queries := db.New(db)
    rows := sqlmock.NewRows([]string{"id", "name", "email"}).
        AddRow(1, "Alice", "alice@example.com")
    mock.ExpectQuery("SELECT id, name, email FROM users WHERE id = \$1").
        WithArgs(1).
        WillReturnRows(rows)

    user, err := queries.GetUser(context.Background(), 1)
    if err != nil {
        t.Errorf("unexpected error: %v", err)
    }
    if user.Name != "Alice" {
        t.Errorf("expected Alice, got %s", user.Name)
    }
}
💡Mocking vs Integration
📊 Production Insight
Run integration tests against a dedicated test database in CI to catch schema changes early.
🎯 Key Takeaway
Test sqlc-generated code with mocks or real databases; the generated interfaces make mocking straightforward.
● Production incidentPOST-MORTEMseverity: high

The Silent Column Rename That Broke Production

Symptom
Users reported intermittent 500 errors when viewing order details. Logs showed 'column not found' errors in the orders service.
Assumption
The developer assumed that since the column rename was done in a migration and the SQL queries were updated, everything would work. They forgot to update the manual result mapping in the Go code.
Root cause
The SQL query was updated to select the new column name, but the Go code that scanned the result into a struct still referenced the old column name via Scan or Row methods. This caused a runtime panic when the column was missing.
Fix
Switched to sqlc, which automatically generates structs and scan code from the SQL. After the migration, they re-ran sqlc and the generated code used the correct column names, preventing the mismatch.
Key lesson
  • Always use code generation to keep Go structs in sync with SQL schemas.
  • Manual result mapping is error-prone and should be avoided in production.
  • sqlc catches column mismatches at compile time, not runtime.
  • Include sqlc generation in your CI/CD pipeline to catch schema changes early.
  • Use sqlc.embed to automatically map all columns from a table.
Production debug guideSymptom to Action3 entries
Symptom · 01
Generated code fails to compile: 'cannot use ... as ... in argument'
Fix
Check that the SQL query returns columns matching the expected types. Run sqlc vet to validate queries against the schema.
Symptom · 02
Runtime error: 'sql: no rows in result set' when expecting data
Fix
Use Query instead of QueryRow if multiple rows are expected, or handle sql.ErrNoRows explicitly. sqlc generates separate methods for single vs. multiple rows.
Symptom · 03
Generated code missing a method for a new query
Fix
Ensure the query file is in the directory configured in sqlc.yaml. Run sqlc generate again. Check for syntax errors in the SQL.
★ Quick Debug Cheat SheetCommon sqlc issues and immediate fixes.
sqlc generate fails with 'table not found'
Immediate action
Verify the database schema file path in sqlc.yaml. Ensure the schema file contains the table definition.
Commands
sqlc vet
cat sqlc.yaml
Fix now
Update the schema path or add the missing table definition.
Generated struct missing a field+
Immediate action
Check if the column is selected in the query. sqlc only generates fields for columns in the SELECT list.
Commands
grep 'column_name' query.sql
sqlc generate
Fix now
Add the column to the SELECT clause.
Method returns wrong number of arguments+
Immediate action
Check the query signature. sqlc uses `-- name: MethodName :one` or `:many` to determine return type.
Commands
head -1 query.sql
sqlc generate
Fix now
Correct the annotation to :one for single row, :many for multiple rows.
FeaturesqlcORM (e.g., GORM)Raw SQL with manual mapping
Type safetyCompile-timeRuntime (some)Runtime
PerformanceHigh (no overhead)Medium (reflection)High
SQL controlFullAbstractedFull
BoilerplateLow (generated)LowHigh
Learning curveLow (if you know SQL)MediumLow
Dynamic queriesNot supportedSupportedSupported
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
sqlc.yamlversion: "2"What is sqlc and Why Use It?
schema.sqlCREATE TABLE users (Setting Up sqlc
query.sqlSELECT id, name, email FROM users WHERE id = $1;Writing Queries with sqlc Annotations
main.go"context"Generating and Using the Go Code
query_advanced.sqlSELECT sqlc.embed(users), sqlc.embed(orders)Advanced Queries
transaction.gotx, err := conn.BeginTx(ctx, nil)Handling Transactions and Batch Operations
test_example.gofunc TestGetUser(t *testing.T) {Testing with sqlc Generated Code

Key takeaways

1
sqlc generates type-safe Go code from SQL, eliminating runtime mapping errors.
2
Use annotations :one, :many, :exec to define method signatures.
3
Integrate sqlc with go generate and run sqlc vet in CI for safety.
4
Handle transactions by passing a *sql.Tx to the generated Queries struct.
5
Test generated code with mocks or real databases to ensure correctness.

Common mistakes to avoid

3 patterns
×

Forgetting to run `sqlc generate` after schema changes

×

Using `:one` for a query that returns multiple rows

×

Not handling `sql.ErrNoRows` when using `:one`

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is sqlc and how does it differ from traditional ORMs?
Q02SENIOR
Explain the purpose of annotations like `:one`, `:many`, and `:exec` in ...
Q03SENIOR
How would you handle a transaction with sqlc?
Q01 of 03JUNIOR

What is sqlc and how does it differ from traditional ORMs?

ANSWER
sqlc is a code generator that compiles SQL queries into type-safe Go code. Unlike ORMs that abstract SQL, sqlc requires you to write raw SQL, giving you full control and performance. It eliminates runtime errors by catching mismatches at compile time.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Can sqlc be used with MySQL or SQLite?
02
How does sqlc handle nullable columns?
03
Does sqlc support dynamic queries?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 13, 2026
last updated
2,165
articles · all by Naren
🔥

That's ORM. Mark it forged?

3 min read · try the examples if you haven't

Previous
Drizzle ORM: TypeScript SQL ORM Guide
9 / 9 · ORM
Next
MySQL vs PostgreSQL