sqlc: Type-Safe SQL Query Compilation for Go Developers
Learn sqlc: a tool that generates type-safe Go code from SQL queries.
20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.
- ✓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.
- 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.
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 vet in CI to catch schema-query mismatches before deployment.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.
golang-migrate, and generate sqlc code after each migration.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.
: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.
pgxpool) for production to avoid opening/closing connections frequently.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.
LIMIT and OFFSET to avoid memory issues.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.
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.
The Silent Column Rename That Broke Production
Scan or Row methods. This caused a runtime panic when the column was missing.- 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.embedto automatically map all columns from a table.
sqlc vet to validate queries against the schema.Query instead of QueryRow if multiple rows are expected, or handle sql.ErrNoRows explicitly. sqlc generates separate methods for single vs. multiple rows.sqlc.yaml. Run sqlc generate again. Check for syntax errors in the SQL.sqlc vetcat sqlc.yaml| File | Command / Code | Purpose |
|---|---|---|
| sqlc.yaml | version: "2" | What is sqlc and Why Use It? |
| schema.sql | CREATE TABLE users ( | Setting Up sqlc |
| query.sql | SELECT id, name, email FROM users WHERE id = $1; | Writing Queries with sqlc Annotations |
| main.go | "context" | Generating and Using the Go Code |
| query_advanced.sql | SELECT sqlc.embed(users), sqlc.embed(orders) | Advanced Queries |
| transaction.go | tx, err := conn.BeginTx(ctx, nil) | Handling Transactions and Batch Operations |
| test_example.go | func TestGetUser(t *testing.T) { | Testing with sqlc Generated Code |
Key takeaways
:one, :many, :exec to define method signatures.go generate and run sqlc vet in CI for safety.*sql.Tx to the generated Queries struct.Common mistakes to avoid
3 patternsForgetting to run `sqlc generate` after schema changes
Using `:one` for a query that returns multiple rows
Not handling `sql.ErrNoRows` when using `:one`
Interview Questions on This Topic
What is sqlc and how does it differ from traditional ORMs?
Frequently Asked Questions
20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.
That's ORM. Mark it forged?
3 min read · try the examples if you haven't