← レッスン一覧に戻る

解答 02.5 — トランザクションとインデックス(countUsersByEmailDomain)¶

このノートブックは 02.5-transactions-indexes.ipynb の練習問題の解答です。 まず問題の前提コードを再掲し、次に解答、最後にチェックを実行します。

先に自分の力で解いてから、答え合わせに使ってください。

前提コード(問題ノートブックと同じ定義・シード)¶

In [1]:
import (
	"database/sql"
	"errors"
	"fmt"

	_ "modernc.org/sqlite"
)

func openCourseDB(path string) *sql.DB {
	db, err := sql.Open("sqlite", path)
	if err != nil {
		panic(err)
	}
	db.SetMaxOpenConns(1)
	return db
}

const dbPath = "file:_02_5_transactions_indexes_solutions.db"
In [2]:
%%
// users テーブルをシードする(練習問題が使うテーブルのみ再掲。本編のaccountsは解答には不要)。
db := openCourseDB(dbPath)
defer db.Close()

db.Exec(`DROP TABLE IF EXISTS users`)
db.Exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)`)

tx, err := db.Begin()
if err != nil {
	panic(err)
}
for i := 0; i < 20000; i++ {
	if _, err := tx.Exec(`INSERT INTO users (email) VALUES (?)`, fmt.Sprintf("user%d@example.com", i)); err != nil {
		panic(err)
	}
}
if err := tx.Commit(); err != nil {
	panic(err)
}
db.Exec(`CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)`)

var count int
db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&count)
fmt.Println("✅ users をシードしました。行数:", count)
✅ users をシードしました。行数: 20000

解答: countUsersByEmailDomain¶

LIKE で "%@" + domain にマッチする行数を数えるだけです。COUNT(*) は該当が無くても 0 を返すので、「0件」と「エラー」を区別するコードは不要です。

In [3]:
func countUsersByEmailDomain(db *sql.DB, domain string) (int, error) {
	var n int
	err := db.QueryRow(`SELECT COUNT(*) FROM users WHERE email LIKE ?`, "%@"+domain).Scan(&n)
	if err != nil {
		return 0, err
	}
	return n, nil
}

チェック(問題ノートブックと同じ期待値)¶

答え合わせ用のヘルパー mustEqual を定義します。

In [4]:
import "reflect"

func mustEqual(got, want any, name string) {
	if reflect.DeepEqual(got, want) {
		fmt.Printf("✅ Passed: %s\n", name)
		return
	}
	panic(fmt.Sprintf("❌ %s\n  got  = %v (%T)\n  want = %v (%T)", name, got, got, want, want))
}
In [5]:
%%
// このチェックは SELECT のみで DB を変更しないため、何度実行しても同じ結果になる。
db := openCourseDB(dbPath)
defer db.Close()

n1, err1 := countUsersByEmailDomain(db, "example.com")
mustEqual(err1, nil, "example.com はエラーなし")
mustEqual(n1, 20000, "全ユーザーが example.com ドメイン")

n2, err2 := countUsersByEmailDomain(db, "not-exist.example")
mustEqual(err2, nil, "存在しないドメインはエラーではない")
mustEqual(n2, 0, "存在しないドメインは0件")

fmt.Println("🎉 すべてのチェックが通りました")
✅ Passed: example.com はエラーなし
✅ Passed: 全ユーザーが example.com ドメイン
✅ Passed: 存在しないドメインはエラーではない
✅ Passed: 存在しないドメインは0件
🎉 すべてのチェックが通りました

解説¶

  1. LIKE のパターン: "%@" + domain は「任意の文字列 + @ + ドメイン」にマッチします。 % はワイルドカードで、先頭の任意の文字列(ローカル部分)を許容します
  2. 🔴 このクエリは idx_users_email を使わない: 先頭が % で始まる LIKE パターンは、 B-tree索引による絞り込み(範囲スキャン)ができません。実際に EXPLAIN QUERY PLAN SELECT COUNT(*) FROM users WHERE email LIKE '%@example.com' を実行すると SCAN users になります(索引作成前と 同じ)。索引が効くのは等価一致(WHERE email = ?、02.5本編で確認)と前方一致 (WHERE email LIKE 'user123%' のように % が末尾にある場合)だけです。「ドメイン名で絞り込む」 という自然な要求が、実は今回張った索引の使用条件(先頭一致)と噛み合わないという、実務でも よく起きる落とし穴を体感するのがこの練習問題の狙いです
  3. 索引の効果は関数の書き方には現れない: 索引を使うかどうかは実行エンジン側が自動判断するため、 countUsersByEmailDomain の Go コード自体は索引の有無に関わらず同じです。「索引を張ったから 速くなるはず」と思い込まず、EXPLAIN QUERY PLAN で実際に確認する習慣が大切です
  4. SELECT のみ = 何度実行しても安全: DB を変更しないため、チェックセルを何度実行しても 同じ結果になります