go の db.Query で too many clients already が発生した話

zumikiti

July 2, 2024

以下のようなコードを書いた。

func UpdatePrices(priceIds []int, orderKey string) {
	psqlconn := psqlConnect()

	// open database
	db, err := sql.Open("postgres", psqlconn)
	checkError(err)

	// close database
	defer db.Close()

	for i, id := range priceIds {
		// update
		updateStmt := fmt.Sprintf(
			"update prices set %s = %v where id = %v",
			orderKey,
			i+1,
			id,
		)

		_, err := db.Query(updateStmt)
		if err != nil {
			log.Printf("Error update recode %d: %v", id, err)
		}
	}
}

やっていることは、 prices テーブルの引数に応じたカラムをアップデートする。

ただ、97レコード目をアップデートしようとした時に以下エラーが出た。

pq: sorry, too many clients already

postgres には、コネクション数に上限あるらしく、それを超えたらしい。

ChatGPT に聞いてみた。

このエラーは、クライアントが多すぎるためにPostgreSQLが新しい接続を拒否していることを示しています。コードで各クエリ実行ごとに新しいデータベース接続を開いているため、コネクションプールが枯渇してしまうのです。

sql.DB自体は接続プールを管理しますが、db.Queryは実行後に結果セットを閉じないと接続が解放されません。この問題を解決するには、以下のように接続を効率的に管理し、不要な接続が解放されるようにします。

修正案

  1. データベース接続を関数の最初で1回だけ開きます。
  2. 各クエリ実行後に結果セットを閉じて接続を解放します。
  3. QueryではなくExecを使用します。Execは結果セットを返さないため、クエリ実行後に結果セットを閉じる必要がありません。

どうやら、 db.Exec を使うと良いらしい。

修正してみたコードが以下。

func UpdatePrices(priceIds []int, column string) {
	psqlconn := psqlConnect()

	// open database
	db, err := sql.Open("postgres", psqlconn)
	checkError(err)

	// close database
	defer db.Close()

	for i, id := range priceIds {
		// update
		updateStmt := fmt.Sprintf("update prices set %s = $1 where id = $2", column)

		_, err := db.Exec(updateStmt, i+1, id)
		if err != nil {
			log.Printf("Error update recode %d: %v", id, err)
		}
	}
}

別途調べると、 db.Query は rows の戻り値を期待し続けるため、コネクションが維持され続けてしまうらしく、 row を返さない場合 db.Exec を使用するらしい。