🤡

【Go】sqlcでbulk insertをする方法

に公開

はじめに

大量データを一括で挿入したい場面は多いのではないでしょうか?
例えばバッチ処理で数百万件のデータを扱うとき、通常のINSERT文を1件ずつ実行すると、ネットワーク往復やトランザクションオーバーヘッドで処理時間が膨大になります。
PostgreSQLが提供するCOPYプロトコルを利用したbulk insert(まとめて挿入)を使うことで、シーケンシャルなINSERTと比べて数十倍〜数百倍の高速化が可能です。

Go向けORMツールのsqlcでは、コメントに:copyfromを付与するだけでこのCOPYプロトコルを簡単に利用できます。[1]

結論

SQLファイルのコメントに:copyfromを付与するだけで、bulk insertが可能になります。

query.sql
-- name: BulkInsertUsers :copyfrom
INSERT INTO users (name, age) VALUES ($1, $2);

基本的なやり方

まずは最小構成で動かしてみましょう。

schema.sql
CREATE TABLE users (
  id   BIGSERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  age  INT NOT NULL
);
query.sql
-- name: BulkInsertUsers :copyfrom
INSERT INTO users (name, age) VALUES ($1, $2);
sqlc.yaml
version: "2"
sql:
  - engine: "postgresql"
    schema: "schema.sql"
    queries: "query.sql"
    gen:
      go:
        package: "db"
        sql_package: "pgx/v5"
        out: "db"

トランザクションを使用したやり方

生成されたbulk insertメソッドは、トランザクション内でもそのまま利用できます。以下はpgxpool.PoolBegin/Commitを使った例です。

main.go
func BulkInsertWithTx(ctx context.Context, pool *pgxpool.Pool, users []db.BulkInsertUsersParams) error {
    tx, err := pool.Begin(ctx) // pgxpool.Begin でトランザクションを開始 :contentReference[oaicite:0]{index=0}
    if err != nil {
        return err
    }
    defer tx.Rollback(ctx)

    q := db.New(tx)
    count, err := q.BulkInsertUsers(ctx, users)
    if err != nil {
        return err
    }

    if err := tx.Commit(ctx); err != nil {
        return err
    }

    fmt.Printf("Inserted %d rows\n", count)
    return nil
}

トランザクションとの組み合わせ方法については、sqlc公式ドキュメント「Using transactions」を参照してください 。

copyfromのメリット・デメリット

  • メリット

    • シーケンシャルINSERTと比べて数十倍〜数百倍の高速化が可能。
    • Goコード側はスライスを渡すだけでOK。ループや個別Execを自前で書く必要がない。
  • デメリット

    • COPY FROMは複雑なWHERE付き挿入や部分的挿入には対応しないため、全件一括投入に限定される。
    • エラーが発生するとデフォルトで処理全体が中断される(ON_ERROR stop)、細かいレコード単位の例外ハンドリングが難しい。

まとめ

本記事では、sqlcでbulk insertを行う方法を紹介しました。ポイントは以下の通りです。

  1. SQLファイルのコメントに :copyfrom を付与するだけでOK。
  2. sql_packagepgx/v5 または pgx/v4 を指定。
  3. トランザクション内でもそのまま利用可能。
脚注
  1. https://docs.sqlc.dev/en/stable/howto/insert.html#using-copyfrom ↩︎

Discussion