グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



database/sqlのrowsのメモリ量を調査する

今回はGoの「database/sql」を使って、結果が大きいクエリを発行したときにヒープのメモリ量を見ていきたいと思います。

検証理由

多くの場合LimitとOffsetを指定するのですが、大量の結果が返されるときに rows がどのくらいメモリを使っているのか知りたくなりました。

 rows.Next  rows.Scan を利用するので、 rows に結果が一度に入りヒープのメモリ量が増えることはないだろうという前提での検証です。

準備

MySqlにUUIDが入っているカラムと、テキトウな文字を入れるカラムを用意して、そこに30,000件のデータを登録しておきます。

mysql> select count(*) from dummy;
+----------+
| count(*) |
+----------+
|    30000 |
+----------+
*************************** 30000. row ***************************
     id: fffedc81-f3f0-4de3-86b5-3b029f502e6a
content: Voluptatem sit accusantium perferendis aut consequatur. Aut perferendis voluptatem sit accusantium consequatur. Voluptatem sit accusantium aut perferendis consequatur. Voluptatem aut perferendis consequatur sit accusantium.

内容はこんな感じになります。

次に、検証用のコードを書きます。
今回は、簡単にヒープに割り当てられたメモリ量を表示しています。

package query

import (
	"database/sql"
	"fmt"
	"log"
	"runtime"

	_ "github.com/go-sql-driver/mysql"
)

var mem runtime.MemStats

func Query() {
	db, err := sql.Open("mysql", "user:password@tcp(localhost:49152)/foo")
	if err != nil {
		panic(err)
	}

	db.SetMaxOpenConns(20)
	db.SetMaxIdleConns(20)

	var (
		id      string
		content string
	)
	rows, err := db.Query("SELECT id, content FROM dummy")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()

	for rows.Next() {
		err := rows.Scan(&id, &content)
		if err != nil {
			log.Fatal(err)
		}
	}

	// runtime.GC()

	runtime.ReadMemStats(&mem)
	fmt.Println(mem.Alloc, mem.TotalAlloc)

	err = rows.Err()
	if err != nil {
		log.Fatal(err)
	}
}

実際に動かしてメモリを計測する

それでは、sql部分にLimitを追加して検証していきたいと思います。

左から、ヒープに割り当てられたメモリ量、ヒープに割り当てられたメモリの総量(減らない)、OSから取得したメモリ量です。

Limit: 10
376048 376048 71650320
376256 376256 71912464
376544 376544 71650320

Limit: 10000
1202960 4504840 73679880
1235760 4505128 73942024
1134624 4504952 73483272

Limit: 20000
1510472 8630744 74007560
1460512 8629504 74269704
1462568 8632184 74269704

Limit: 30000
1823432 12766528 74269704
1945400 12764736 74269704
1959984 12764528 74269704

Limitを上げると順調にヒープが育っていってます。

次に、上記コードのGC部分のコメントを外して実行します。(検証用にGCをコールしています)

Limit: 10
223784 382464 71715600
226216 384672 72239888
226264 385360 72239888

Limit: 10000
224800 4503592 74269704
224472 4504720 74269704
226152 4504200 74269704

Limit: 20000
226848 8632624 74269704
229000 8633136 74007560
224312 8628920 74007560

Limit: 30000
228488 12765656 74269704
226488 12764600 74269704
224776 12764296 74007560

今度はGCによって削除されているのがわかります。

次に、ループ中のメモリを調べたいので、少し強引ですが rows.Next のループ毎にGCを呼んでみます。
毎回GCを呼ぶのでかなり遅くなることが予想されます。

	for rows.Next() {
		err := rows.Scan(&id, &content)
		if err != nil {
			log.Fatal(err)
		}

		runtime.GC()
		runtime.ReadMemStats(&mem)
		fmt.Println(mem.Alloc, mem.TotalAlloc, mem.Sys)
	}

Limit: 10
228624 399640 74007560
228640 401680 74007560
228632 403752 74007560

Limit: 10000
250304 22738464 74269704
250304 22741000 74269704
250400 22743632 74269704

Limit: 20000
251448 45285408 74335240
251448 45287880 74335240
251448 45290352 74335240

Limit: 30000
253800 67179272 74269704
253800 67181200 74269704
253800 67183336 74269704

手元の環境だと30000レコードで9秒かかりました。

こちらもヒープのメモリ量は一定になりましたが、GCが多発すると遅くなることが実感できます。

まとめ

簡易的な検証でしたが、以下のような予想ができます。

  • 一度に大量の行をクエリすると、database/sqlのrowsのヒープは増えていく
  • GCが実行されると削除されるが、GCが過剰に実行されると遅くなる

この記事を書いた人

tkr2f
tkr2f事業開発部 web application engineer
2008年にアーティスへ入社。
システムエンジニアとして、SI案件のシステム開発に携わる。
その後、事業開発部の立ち上げから自社サービスの開発、保守をメインに従事。
ドメイン駆動設計(DDD)を中心にドメインを重視しながら、保守可能なソフトウェア開発を探求している。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします