【課題 3】N+1 を消したのに速くならない
読了目安 約3分
教科書どおりの N+1 を 2 か所直して、スコアがほぼ動かなかった回の記録です。
この章の目次
インデックスが効いたので、次はコードを読みます。
課題
ランキング取得の中心部分です。
pss := []PlayerScoreRow{}
if err := tenantDB.SelectContext(
ctx,
&pss,
"SELECT * FROM player_score WHERE tenant_id = ? AND competition_id = ? ORDER BY row_num DESC",
tenant.ID, competitionID,
); err != nil {
return fmt.Errorf("error Select player_score: %w", err)
}
ranks := make([]CompetitionRank, 0, len(pss))
scoredPlayerSet := make(map[string]struct{}, len(pss))
for _, ps := range pss {
if _, ok := scoredPlayerSet[ps.PlayerID]; ok {
continue
}
scoredPlayerSet[ps.PlayerID] = struct{}{}
p, err := retrievePlayer(ctx, tenantDB, ps.PlayerID)
if err != nil {
return fmt.Errorf("error retrievePlayer: %w", err)
}
ranks = append(ranks, CompetitionRank{
Score: ps.Score,
PlayerID: p.ID,
PlayerDisplayName: p.DisplayName,
RowNum: ps.RowNum,
})
}参加者の詳細を返すハンドラです。
p は、URL で指定された参加者の行です。
cs := []CompetitionRow{}
if err := tenantDB.SelectContext(
ctx, &cs,
"SELECT * FROM competition WHERE tenant_id = ? ORDER BY created_at ASC", v.tenantID,
); err != nil && !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("error Select competition: %w", err)
}
pss := make([]PlayerScoreRow, 0, len(cs))
for _, c := range cs {
ps := PlayerScoreRow{}
if err := tenantDB.GetContext(
ctx, &ps,
"SELECT * FROM player_score WHERE tenant_id = ? AND competition_id = ? AND player_id = ? ORDER BY row_num DESC LIMIT 1",
v.tenantID, c.ID, p.ID,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
continue
}
return fmt.Errorf("error Select player_score: %w", err)
}
pss = append(pss, ps)
}
psds := make([]PlayerScoreDetail, 0, len(pss))
for _, ps := range pss {
comp, err := retrieveCompetition(ctx, tenantDB, ps.CompetitionID)
if err != nil {
return fmt.Errorf("error retrieveCompetition: %w", err)
}
psds = append(psds, PlayerScoreDetail{
CompetitionTitle: comp.Title,
Score: ps.Score,
})
}この 2 つのコードには、ループの中のクエリが合わせて 3 か所あります。 どこか、そしてどう直すかを答えてください。
解答例
ランキング側
retrievePlayer が、重複を除いた参加者の数だけ呼ばれています。
参加者を先にまとめて引いて、map から取り出します。
pls := []PlayerRow{}
if err := tenantDB.SelectContext(
ctx, &pls, "SELECT * FROM player WHERE tenant_id = ?", tenant.ID,
); err != nil {
return fmt.Errorf("error Select player: %w", err)
}
playerByID := make(map[string]PlayerRow, len(pls))
for _, p := range pls {
playerByID[p.ID] = p
}参加者詳細側
大会ごとに 1 クエリ、さらにスコアごとに大会を引き直しています。
後者はとくに無駄で、大会の情報は cs としてすでに手元にあります。
スコアは 1 クエリにまとめられます。
row_num の降順に並べれば、大会ごとに最初に現れた行が最新のスコアです。
rows := []PlayerScoreRow{}
if err := tenantDB.SelectContext(
ctx, &rows,
"SELECT competition_id, score, row_num FROM player_score WHERE tenant_id = ? AND player_id = ? ORDER BY row_num DESC",
v.tenantID, p.ID,
); err != nil {
return fmt.Errorf("error Select player_score: %w", err)
}
scoreByCompetitionID := make(map[string]int64, len(rows))
for _, ps := range rows {
if _, ok := scoreByCompetitionID[ps.CompetitionID]; ok {
continue
}
scoreByCompetitionID[ps.CompetitionID] = ps.Score
}
// 大会のタイトルは既に cs にあるので、引き直さない
psds := make([]PlayerScoreDetail, 0, len(cs))
for _, comp := range cs {
score, ok := scoreByCompetitionID[comp.ID]
if !ok {
continue
}
psds = append(psds, PlayerScoreDetail{
CompetitionTitle: comp.Title,
Score: score,
})
}この書き換えには、第 3 章とは別のインデックスが要ります。
competition_id で絞らなくなったからです。
CREATE INDEX idx_ps_player_row
ON player_score (tenant_id, player_id, row_num, competition_id, score);第 3 章と同じく、初期化スクリプトと 10_schema.sql の両方に足します。
効果
| スコア | |
|---|---|
| インデックスを足した | 10319 |
| ランキングの N+1 を消した | 10434 |
| 参加者詳細の N+1 を消した | 10105 |
動きませんでした。 3 回の走行のばらつきの範囲です。
なぜ効かないのか
Part 6 では、N+1 の害は DB への往復が N 回起きることだと説明しました。 ここでの往復先は SQLite です。
SQLite を使うと、アプリケーションが直接ストレージ上のファイル(.dbや.sqliteなど)を読み書きします。 ネットワークもプロセス間通信もしないため、データベースの操作にかかる時間が桁違いに少なく、回数を減らしても合計はあまり減りません。
ただし、このことことだけが理由だとは決められません。
ランキング側は N+1 を消すと同時に、テナントの参加者を全員取る SELECT * に変えています。
減った往復のぶんを、増えた読み出しが打ち消した可能性が残ります。
この全件取得は第 6 章で pprof に出てきます。
30 秒のプロファイルのうち 3.06 秒を、この 1 行が使っていました。
切り分けるなら、必要な参加者 ID だけを IN でまとめて取る形にして測り直します。
教訓
定石を当てて効かなかったときは、そこで粘らずに計測へ戻ります。 「N+1 だから直すべき」ではなく、「計測がここを指しているから直す」の順です。
次に見るのは、計測がずっと指し続けているのに手をつけていない場所です。
REPLACE INTO id_generator が 17914 回走っています。