database/sql/driver: allow driver to entirely override Scan
要約
概要
database/sql/driver パッケージに RowsColumnScanner インターフェースを追加し、SQLドライバがクエリ結果のスキャン処理を完全に制御できるようにするプロポーザルです。併せて database/sql パッケージに ConvertAssign 関数を公開します。
ステータス変更
likely_accept → accepted
2026年4月29日の Proposal Review Meeting(@aclements, @adonovan, @bradfitz, @cherrymui, @griesemer, @ianlancetaylor, @neild, @rolandshoemaker 参加)にて、コンセンサスに変化なしとして正式に accepted となりました。2024年7月の初回 likely accept から一度は Go 1.26 向けにロールバックされましたが、@neild による API 再設計案(NextRow と ScanColumn の分離)が pgx と SQLite ドライバの両方で実証され、性能向上も確認されたことで最終承認に至りました。
技術的背景
現状の問題点
database/sql の Rows.Scan は、ドライバが返す driver.Value(int64, float64, string, []byte, time.Time, bool のいずれか)を中間値として経由する設計になっています。この設計には2つの根本的な制限があります。
第一の問題: カスタム型のサポート不足
PostgreSQL の pgx ドライバは、[]int64 配列や map[string]string (hstore) などのリッチな型を扱います。しかし driver.Value には配列型が存在しないため、ドライバはいったんテキスト形式に変換せざるをえず、ユーザーは手動でラッパーを記述する必要がありました。
// Before: pgx を database/sql 経由で使う場合、配列スキャンに専用ラッパーが必要だった
m := pgtype.NewMap()
var a []int64
err := db.QueryRow("select '{1,2,3}'::bigint[]").Scan(m.SQLScanner(&a))
第二の問題: 不要なメモリアロケーション
Rows.Next(dest []driver.Value) の設計では、すべての値(int64 や float64 を含む)をインターフェース型にボックス化する必要があります。1行数十カラム・数百万行を返す時系列データベースのクエリでは、数億回のアロケーションが発生し、GC 負荷による深刻なパフォーマンス劣化を引き起こします。
提案された解決策
database/sql/driver パッケージに新しいオプションインターフェース RowsColumnScanner を追加します。このインターフェースは Rows.Next を 置き換える 設計になっており、ドライバが行の進行とカラムのスキャンを直接制御できます。
package driver
// RowsColumnScanner extends the [Rows] interface by providing a way for the driver
// to scan directly into the user-provided destination.
//
// RowsColumnScanner supersedes the [Rows.Next] method.
type RowsColumnScanner interface {
Rows
// NextRow advances to the next row of data.
// It should return io.EOF when there are no more rows.
NextRow() error
// ScanColumn copies a column in the current row into the value pointed to by dest.
//
// The driver may assign a driver.Value to dest using sql.ConvertAssign.
ScanColumn(index int, dest any) error
}
合わせて、ドライバが標準の型変換ロジックにフォールバックできるよう、内部関数だった convertAssign を公開します。
package sql
// ConvertAssign copies the value in src to the value pointed at by dest.
// ConvertAssign is intended for use by driver implementations.
func ConvertAssign(dest any, src driver.Value) error
これによって何ができるようになるか
コード例
// Before: pgx 経由で PostgreSQL 配列をスキャンするには専用ラッパーが必要だった
m := pgtype.NewMap()
var names []string
err := db.QueryRow("select array['John', 'Jane']::text[]").Scan(m.SQLScanner(&names))
// After: RowsColumnScanner により、pgx ドライバがスキャンを直接処理するため
// 標準の *[]string でそのままスキャンできる
var names []string
err := db.QueryRow("select array['John', 'Jane']::text[]").Scan(&names)
ユースケース1: PostgreSQL の高度な型サポート (pgx)
pgx が ScanColumn を実装することで、PostgreSQL OID・バイナリフォーマット・コーデック情報を活用した直接デコードが可能になります。uuid、配列、範囲型など driver.Value に対応しない型をバイナリ形式で効率的に処理できます。
ユースケース2: SQLite ドライバのアロケーション削減
SQLite は bool や time.Time を持たず、現状では文字列経由の変換が必要です。ScanColumn でデスティネーション型を見て直接代入することで、中間ボックス化アロケーションを排除できます。
ユースケース3: 時系列データベースの大量行スキャン
数百万行・数十カラムを返すクエリで、各値のボックス化アロケーションを回避することで GC 負荷を大幅に削減します。pgx での実測では、1000行スキャン時のアロケーション数が 11,676 回から 6,011 回(約48%減)、メモリ使用量が 142,771 B から 61,726 B(約57%減)に改善されました。
議論のハイライト
- Go 1.26 でのロールバック: 初回実装(2024年5月)では
Rows.NextとRowsColumnScannerを並存させる設計だったが、@aclements が「Nextでdestを埋めずにScanColumnだけを使うことはNextの契約違反になる」という問題を指摘。RC 間近のタイミングでロールバックされた。 NextとNextRowの分離: @neild が提案した再設計の核心は、行を進める操作(NextRow)とカラムをスキャンする操作(ScanColumn)を明確に分離すること。これによりRows.Nextが廃止され、ドライバはdriver.Valueスライスを埋める義務から解放された。ConvertAssignの公開: ドライバが標準の型変換ロジックにフォールバックできるよう、内部関数convertAssignをsql.ConvertAssignとして公開することが決定。ドライバは高速パスを自前で実装しつつ、未対応の型はConvertAssignに委譲できる。ErrSkipの廃止: 初期提案ではErrSkipで標準パスへのフォールバックが可能だったが、最終設計ではScanColumnがConvertAssignを呼ぶことでフォールバックする設計になり、よりシンプルになった。- 後方互換性の担保:
RowsColumnScannerはオプションインターフェースであるため、既存ドライバへの影響はゼロ。RowsColumnScannerを実装するドライバでもRowsインターフェースも継続実装が必要なため、旧 Go バージョンとの互換性が保たれる。