解答 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件 🎉 すべてのチェックが通りました
解説¶
LIKEのパターン:"%@" + domainは「任意の文字列 +@+ ドメイン」にマッチします。%はワイルドカードで、先頭の任意の文字列(ローカル部分)を許容します- 🔴 このクエリは
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%'のように%が末尾にある場合)だけです。「ドメイン名で絞り込む」 という自然な要求が、実は今回張った索引の使用条件(先頭一致)と噛み合わないという、実務でも よく起きる落とし穴を体感するのがこの練習問題の狙いです - 索引の効果は関数の書き方には現れない: 索引を使うかどうかは
実行エンジン側が自動判断するため、countUsersByEmailDomainの Go コード自体は索引の有無に関わらず同じです。「索引を張ったから 速くなるはず」と思い込まず、EXPLAIN QUERY PLANで実際に確認する習慣が大切です - SELECT のみ = 何度実行しても安全: DB を変更しないため、チェックセルを何度実行しても 同じ結果になります