sqlc のすすめ:SQL から型安全な Go コードを生成する
今回は Go の SQL コード生成ツール sqlc を、仕組み・使い方・利点の順で整理します。
sqlc とは
-
sqlc は、SQL を書くと、それを実行するための型安全な Go コードを生成してくれるツールです。
- 入力はふつうの SQL ファイルです。CREATE TABLE と SELECT や INSERT を書くだけです。
- 出力は、引数も戻り値も具体的な型を持つ Go の関数と構造体です。
- 公式ドキュメントは「SQL から完全に型安全で慣用的な Go コードを生成する」と説明しています。
sqlc は ORM ではありません。クエリビルダでもありません。
Go のコードから SQL を組み立てるのではなく、SQL を出発点にして Go を導出する「SQL ファースト」のツールです。
開発の流れは 3 ステップに要約できます。
- SQL でスキーマとクエリを書く
- sqlc generate を実行して Go コードを生成する
- アプリケーションから生成されたメソッドを呼ぶ
この立ち位置を理解するために、まず既存のアプローチと並べてみます。
データベースアクセスの 3 つのアプローチ
Go からデータベースにアクセスする方法は、大きく 3 つに分けられます。
| SQL を書くのは誰か | 型安全性 | 実行時オーバーヘッド | 学習対象 | |
|---|---|---|---|---|
| database/sql を手書き | 自分 | 低い(Scan の対応づけは実行時まで検査されない) | 小さい | SQL と標準ライブラリ |
| ORM(GORM など) | ライブラリが組み立てる | 中くらい(条件や列を文字列で渡す箇所が残る) | リフレクションの分だけある | ORM 独自の API と規約 |
| sqlc | 自分 | 高い(生成時とコンパイル時に検査される) | 手書きと同等 | SQL だけ |
database/sql の手書きは素直ですが、rows.Scan の並び順を間違えても実行してみるまで気づけません。
ORM は記述量を減らしてくれますが、SQL とは別に ORM の API を学び直す必要があります。
sqlc はこの 2 つの中間ではなく、第 3 の道です。
SQL は自分で書き、退屈な部分だけを生成に任せる——手書きの制御と ORM の省力を両取りする発想です。
3 つの位置関係を図にすると次のようになります。

見ての通り、sqlc は「SQL を書く手間」を減らすのではなく、「SQL の周りの Go を書く手間」を消す道具です。では、どうやって消しているのでしょうか。
sqlc の仕組み
sqlc への入力は 3 種類のファイルです。
-
schema.sql:テーブル定義(CREATE TABLE)です。
- sqlc はこれを解析して、各カラムの名前と型を知ります。
- マイグレーションツールのファイル群をそのまま指定することもできます。
-
query.sql:実行したいクエリの一覧です。
- 各クエリの直前に「-- name: メソッド名 :アノテーション」というコメントを付けます。
- この名前がそのまま生成される Go のメソッド名になります。
-
sqlc.yaml:設定ファイルです。
- データベースエンジン・入力ファイルの場所・出力先パッケージなどを指定します。
この 3 つを読み込んで、sqlc generate コマンドが Go コードを吐き出します。
sqlc は SQL を内部でパースし、スキーマと突き合わせて各クエリの引数と結果の型を推論します。だから生成物に型が付くわけです。
クエリに付けるアノテーションは、そのクエリが「何を返すか」の宣言です。
| アノテーション | 意味 | 生成されるメソッドの戻り値 |
|---|---|---|
| :one | ちょうど 1 行を返すクエリ | 行に対応する構造体と error |
| :many | 複数行を返すクエリ | 構造体のスライスと error |
| :exec | 結果の行が要らない実行(UPDATE や DELETE) | error のみ |
| :execrows | 影響を受けた行数が欲しい実行 | 影響行数(int64)と error |
このほかに、挿入 ID を返す :execlastid や、PostgreSQL 向けの一括処理用アノテーションもあります。まずは上の 4 つを覚えれば十分です。
入力から出力までの流れを図にまとめます。

生成は完全にビルド前のローカル処理で、実行時に sqlc は一切登場しません。仕組みが分かったところで、実際に動かしてみます。
最小構成での使用例
users テーブルを題材に、入力 3 ファイルと生成物の使い方を順に見ていきます。
まずはスキーマです。
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name text NOT NULL,
email text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now()
);
ふつうの CREATE TABLE 文です。sqlc はここから User 構造体(ID・Name・Email・CreatedAt を持つ)を生成します。
次にクエリです。名前とアノテーションのコメントが sqlc への指示になります。
-- name: GetUser :one
SELECT * FROM users WHERE id = $1 LIMIT 1;
-- name: ListUsers :many
SELECT * FROM users ORDER BY created_at DESC;
-- name: CreateUser :one
INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *;
GetUser は 1 行なので :one、一覧は :many です。CreateUser は RETURNING で挿入行を返すため、これも :one になります。
設定ファイルはこれだけです。
version: "2"
sql:
- engine: "postgresql"
schema: "schema.sql"
queries: "query.sql"
gen:
go:
package: "db"
out: "db"
sql_package: "database/sql"
engine にデータベースを、gen.go に出力先パッケージを指定します。sql_package を pgx/v5 にすれば pgx 用のコードも生成できます。
この状態で sqlc generate を実行すると、db ディレクトリに次のようなシグネチャのコードが生成されます。
func (q *Queries) GetUser(ctx context.Context, id int64) (User, error)
func (q *Queries) ListUsers(ctx context.Context) ([]User, error)
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
注目してほしいのは、引数も戻り値もすべて具体的な型であることです。interface{} も map も出てきません。
使う側はこうなります。
conn, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatal(err)
}
queries := db.New(conn) // *sql.DB を渡して Queries を作る
user, err := queries.CreateUser(ctx, db.CreateUserParams{
Name: "tekuteku",
Email: "tekuteku@example.com",
})
if err != nil {
log.Fatal(err)
}
users, err := queries.ListUsers(ctx) // []db.User が返る
New に接続を渡して Queries を作り、あとは ctx を先頭にメソッドを呼ぶだけです。トランザクションを使いたいときは、New に *sql.Tx を渡せば同じメソッド群がそのまま使えます。
書いたのは SQL と数行の呼び出しだけ——ここに sqlc の利点が詰まっています。
sqlc の利点
sqlc を使う理由を 5 つに整理します。
-
SQL の誤りが生成時に見つかります。
- sqlc は SQL をパースしてスキーマと突き合わせます。
- カラム名の typo や存在しないテーブルへの参照は、sqlc generate の時点でエラーになります。
- たとえば SELECT id, nmae FROM users と書けば、実行どころか go build より前に落ちます。
- 手書きの database/sql では、この種のミスは本番のクエリ実行まで潜伏します。
- sqlc は SQL をパースしてスキーマと突き合わせます。
-
型安全です。
- 生成されるメソッドは引数も戻り値も具体的な型です。id を文字列で渡せばコンパイルエラーになります。
- SQL 文字列の組み立てが消えるので、プレースホルダの数と引数の数のずれという古典的なバグも起こりません。
-
ボイラープレートが消えます。
- rows.Next のループ、rows.Scan の羅列、rows.Err の確認、defer rows.Close。
- この定型文一式を二度と手で書かなくてよくなります。
- カラムを 1 つ追加したときも、直すのは SQL だけです。再生成すれば構造体と Scan が追従します。
- rows.Next のループ、rows.Scan の羅列、rows.Err の確認、defer rows.Close。
-
実行時オーバーヘッドがありません。
- 生成物は database/sql(または pgx)を呼ぶ薄い定型コードで、リフレクションを使いません。
- つまり実行時の性能もデバッグのしやすさも、手書きコードとまったく同じです。
-
SQL がそのまま資産になります。
- query.sql は実行される SQL そのものなので、プルリクエストで SQL をレビューできます。
- 遅いクエリはファイルからコピーして EXPLAIN にそのまま貼れます。
- ORM が生成した SQL を復元する作業なしに、DBA と同じ言葉で会話できます。
このうち最初の 1 つが、体感としていちばん効くと思います。
手書きや ORM では実行時まで待たされたエラーが、保存して生成した瞬間に手元で見つかる。このフィードバックの速さが sqlc の核です。
エラーが見つかるタイミングを 3 つのアプローチで比べると、差がはっきりします。

図で注目してほしいのは、sqlc だけがエラー検出を実行時より左、つまり開発マシンの上に引き寄せていることです。
注意点と向き不向き
もちろん万能ではありません。採用前に知っておきたい点を挙げます。
-
動的なクエリは苦手です。
- sqlc のクエリは生成時に固定されるため、検索条件の組み合わせで WHERE 句が変わる画面のような要件とは相性がよくありません。
- NULL なら無視する条件をあらかじめ SQL に織り込むなどの回避策はありますが、条件が多いと SQL が読みにくくなります。
- そうした箇所だけクエリビルダや手書きを併用するのが現実的です。全部を sqlc で書く必要はありません。
-
対応データベースは PostgreSQL・MySQL・SQLite です。
- 設定の engine にこの 3 つを指定できます(公式ドキュメントの Configuration リファレンス参照)。
- それ以外のデータベースを使う場合は候補から外れます。
-
SQL を書けることが前提です。
- JOIN や集計を含めて、クエリは全部自分で書きます。SQL から逃げるための道具ではありません。
- 裏を返せば、書いた SQL の知識が ORM の規約と違って他の言語や現場でもそのまま通用する資産になります。
静的なクエリが大半を占める API サーバーやバッチ処理なら、sqlc はまっすぐ刺さります。逆にクエリの形が実行時に決まるアプリでは、部分的な採用にとどめるのがよいと思います。
まとめ
- sqlc は SQL から型安全な Go コードを生成するツールであり、ORM でもクエリビルダでもない「SQL ファースト」の第 3 の道です。
- 入力は schema.sql・query.sql・sqlc.yaml の 3 ファイルで、sqlc generate が構造体とメソッドを生成します。
- アノテーションは :one が 1 行・:many がスライス・:exec が error のみ・:execrows が影響行数を返します。
- カラム名の typo や型の不一致は生成時とコンパイル時に落ち、実行時まで潜伏しません。
- 生成物はリフレクションを使わない薄いコードで、実行時オーバーヘッドは手書きと同等です。
- rows.Scan の羅列が消える一方で、SQL 自体はレビューや EXPLAIN に使える資産として残ります。
- 動的なクエリは苦手なので、対応 DB(PostgreSQL・MySQL・SQLite)と要件を確認してから採用します。
参考
Discussion