なぜQueryはCloseが必要で、QueryRowは不要なのか
Go言語で database/sql や pgx を使ってDB操作をする際、Query を使ったら必ず rows.Close() をすると思います.
ただ,なぜrows.Close()あまり理解していなかったため,この機会にまとめたいと思います.
1. Closeしないと起きる悲劇:コネクションプールの枯渇
結論から言うと、rows は読み取り専用ですが、Close() しないとDB接続(コネクション)が解放されず、プールに戻らないから です。
接続は有限のリソース
DBへの接続は、アプリケーション内で共有される「接続プール」によって管理されています。
// pgxpoolの設定例
poolConfig.MaxConns = 25 // 最大25接続
この場合、アプリ全体で同時に使える接続は25本までです。
枯渇のメカニズム
Query を実行すると、プールから接続を1つ「借り」ます。この接続は rows.Close() が呼ばれるまで「使用中」の状態になります。
もし Close() を忘れると、リクエストが終わっても接続は「借りっぱなし」になります。これを25回繰り返すと、26回目のリクエストは接続を確保できず、タイムアウトや connection pool exhausted エラーで死にます。
2. なぜQueryは自動で接続を返さないのか?
「データを取得し終わったら、ライブラリが勝手に接続を返せばいいのでは?」と思うかもしれません。
しかし、自動で返すことはできません。 なぜなら、Query はストリーミング処理(遅延読み込みを行っているからです。
一括取得ではなく、1行ずつ取得している
db.Query("SELECT * FROM users") を実行した瞬間、全てのデータがメモリに乗るわけではありません。
rows, _ := db.Query("SELECT * FROM users")
// ★この時点ではまだ接続を保持し続ける必要がある!
for rows.Next() {
// ここで初めてDBから行データを取得(Fetch)する
rows.Scan(&id, &name)
}
// ループ中も、次の行を取るために接続はずっと必要
rows.Close()
// ★ここで「もうこれ以上データはいらない」と宣言して初めて接続を返せる
PostgreSQLなどのRDBMSは、ネットワーク効率やメモリ効率のためにカーソル(またはそれに準ずる仕組み)を使い、必要に応じてデータ転送を行います。
もし Query の時点で勝手に接続を返してしまったら、rows.Next() で次の行を取りに行こうとしたときに「接続がない」状態になり、データが取れなくなってしまいます。
つまり、「いつ読み込みが終わるか」を知っているのはプログラマーだけ なので、プログラマーが明示的に Close() を呼ぶ必要があるのです。
3. なぜ QueryRow は Close が不要なのか?
一方で、1行だけ取得する QueryRow には Close メソッドが存在しません。
var name string
err := db.QueryRow("SELECT name FROM users WHERE id=$1", 1).Scan(&name)
// Close() は不要(というか書けない)
なぜこちらは不要なのでしょうか?
それは、Scan() メソッドの内部で自動的に Close しているから です。
QueryRow は「必ず最大1行だけ取得する」という仕様です。そのため、Scan が呼ばれた時点で「データの取得は(成功しても失敗しても)これで終わり」と確定します。
「読み込みの終わり」が明確であるため、ライブラリ側で自動的に Close を呼ぶことができる のです。
まとめ
| メソッド | 取得する行数 | Closeの必要性 | 理由 |
|---|---|---|---|
| Query | 0行〜無限 |
必要 (defer rows.Close()) |
プログラマが Next() で回し続ける間、接続を維持する必要があるから。 |
| QueryRow | 0行 or 1行 | 不要 |
Scan() 実行時に、内部でデータの取得と同時に接続の解放(Close)を行ってくれるから。 |
Query を使うときは、読み取り専用であっても「コネクションという電話回線を繋ぎっぱなしにしている」とイメージしてください。使い終わったら必ず受話器を置く(Close する)必要があります。