Y

ISUCON12予選の復習をしました 3

この記事は何か?

ISUCON12予選の復習記録です。

第3回はISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)の「3. Score APIの追加のループクエリをなくす」を試します。

目次

  1. 「3. Score APIの追加のループクエリをなくす」を試す (N+1問題)
  2. 「3. Score APIの追加のループクエリをなくす」を試す (バルクインサート)

1. 「3. Score APIの追加のループクエリをなくす」を試す (N+1問題)

「3. Score APIの追加のループクエリをなくす」には2つの対策が解説されています。

N+1問題とバルクインサートです。どちらもISUCONでは馴染みのある対応ですね。

まずは、N+1問題を解決します。

ということで、「CSVを全行チェックしてPlayerIdの重複ないリストを作る」→「それをSELECT COUNT(*) as count FROM player WHERE id IN (...) に渡してCOUNTに差分があるか確認する」という処理を追加すると参加者数チェックの部分は1クエリでできるようになります。

解説では上記の対応方法が記載されていますが、私は件数だけの確認では整合性が取れない場合はあるのではないかと思い、先にテナントに紐づくプレイヤーをスライスに格納することにしました。今後他の人の解法も見てみるつもりなので、そこで答え合わせしたいと思います。

+
+       "golang.org/x/exp/slices"

+       // プレイヤー一覧をスライスにいれておく
+       playerIDs := []string{}
+       if err := tenantDB.SelectContext(
+               ctx,
+               &playerIDs,
+               "SELECT DISTINCT(id) FROM player WHERE tenant_id = ?",
+               v.tenantID,
+       ); err != nil && err != sql.ErrNoRows {
+               return fmt.Errorf("error Select player ID: tenantID=%d, %w", v.tenantID, err)
+       }

+
+               // 存在しない参加者が含まれている (スライス利用)
+               if !slices.Contains(playerIDs, playerID) {
+                       fmt.Errorf("error retrievePlayer: %w", err)
+
+                       return echo.NewHTTPError(
+                               http.StatusBadRequest,
+                               fmt.Sprintf("player not found: %s", playerID),
+                       )
+               }
+
+               /*
                if _, err := retrievePlayer(ctx, tenantDB, playerID); err != nil {
                        // 存在しない参加者が含まれている
                        if errors.Is(err, sql.ErrNoRows) {
                        ...
                        }
                        return fmt.Errorf("error retrievePlayer: %w", err)
                }
+               */

「go get golang.org/x/exp/slices」 を実行します。

  • 変更前のスコア

21:50:11.084591 SCORE: 2811 (+2811 0(0%))

  • 変更後のスコア

21:46:04.478709 SCORE: 3188 (+3188 0(0%))

2. 「3. Score APIの追加のループクエリをなくす」を試す (バルクインサート)

これは分かりやすいバルクインサート適用部分です。

+
+       // バルクインサート
+       if _, err := tenantDB.NamedExecContext(
+               ctx,
+               "INSERT INTO player_score (id, tenant_id, player_id, competition_id, score, row_num, created_at, updated_at) VALUES (:id, :tenant_id, :player_id, :competition_id, :score, :row_num, :created_at, :updated_at)",
+               playerScoreRows,
+       ); err != nil {
+               return fmt.Errorf("error Insert player_score: %w", err)
+       }
+
+        /*
        for _, ps := range playerScoreRows {
                if _, err := tenantDB.NamedExecContext(
                        ctx,
            .....
                }
        }
+        */
  • 変更前のスコア (N+1対応後)

21:46:04.478709 SCORE: 3188 (+3188 0(0%))

  • 変更後のスコア

22:05:24.438783 SCORE: 3862 (+3862 0(0%))

感想

今回の対応でスコアが1,000点改善しました。

上記のとおりN+1問題とバルクインサートはISUCON頻出問題ですが、僕は当日解決できませんでした 😭

参考

1 ISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)