Y

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

この記事は何か?

ISUCON12の復習をやっていきます。

第1回はISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)の「adminDB visit_history にINDEXを張る」までをやります。

目次

  1. 環境構築
  2. 「adminDB visit_history にINDEXを張る」を試す

1. 環境構築

用意されたCloudFormationテンプレートを利用する場合に記載された方法でAWS上に環境を用意します。

  1. cloudformation.yamlをダウンロードして、CloudFormationの画面から実行
  2. 3台起動されるが、節約のため1台停止 (1台をベンチ実行用、2台目をチューニング用に利用します)
  3. Systems Manager > パラメータストア を開き、秘密鍵をコピペ、SSHログイン
vi ~/.ssh/isucon12q.pem
chmod 400 ~/.ssh/isucon12q.pem
ssh -i ~/.ssh/isucon12q.pem ubuntu@xx.xx.xx.xx ← 1台目にログイン

04.初回ベンチ実行

sudo su - isucon
cd /home/isucon/bench
./bench -target-addr xx.xx.xx.xx:443  ← 2台目に向けてベンチ実行

初回スコアは2,781でした。運営の方がCloudFormationテンプレートを用意してくれたおかげで準備が超簡単です。ありがとうございます。

「adminDB visit_history にINDEXを張る」を試す

初手は「adminDB visit_history にINDEXを張る」でした。私と同じで嬉しかったのですがINDEXの張り方が違っていました。

最後にcreated_atまで入れておくと、Covering Indexになるのでセカンダリインデックス単体でクエリを返すことができて高速ですが、created_atを張るのを忘れるとcreated_atを読むためにクラスタインデックスを読みに行くのでそこまで伸びない(もちろん張らないよりはマシ)といった感じです。私は最初created_atまで入れてなくて「アルェー」と言ってました。

私は2カラムしか設定しませんでしたが、解説では4カラムでした。

私: tenant_id, competition_id
解説: tenant_id, competition_id, player_id, created_at

気になったので、インデックス毎に「Extra」の値を確認しました。

  • スロークエリログ
Reading mysql slow query log from /var/lib/mysql/slow.log
Count: 1152  Time=0.11s (129s)  Lock=0.00s (0s)  Rows=92.7 (106831), isucon[isucon]@localhost
  SELECT player_id, MIN(created_at) AS min_created_at FROM visit_history WHERE tenant_id = N AND competition_id = 'S' GROUP BY player_id
  • EXPLAIN結果 (インデックス無し)
mysql> explain SELECT player_id, MIN(created_at) AS min_created_at FROM visit_history WHERE tenant_id = 1 AND competition_id = 'S' GROUP BY player_id\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: visit_history
   partitions: NULL
         type: ref
possible_keys: tenant_id_idx
          key: tenant_id_idx
      key_len: 8
          ref: const
         rows: 1293162
     filtered: 10.00
        Extra: Using where; Using temporary
1 row in set, 1 warning (0.00 sec)
  • EXPLAIN結果 (インデックス tenant_id, competition_id)
mysql> explain SELECT player_id, MIN(created_at) AS min_created_at FROM visit_history WHERE tenant_id = 1 AND competition_id = 'S' GROUP BY player_id\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: visit_history
   partitions: NULL
         type: ref
possible_keys: tenant_id_idx,idx_tenant_id_competition_id
          key: idx_tenant_id_competition_id
      key_len: 1030
          ref: const,const
         rows: 1
     filtered: 100.00
        Extra: Using temporary
1 row in set, 1 warning (0.00 sec)
  • EXPLAIN結果 (インデックス tenant_id, competition_id, player_id)
mysql> explain SELECT player_id, MIN(created_at) AS min_created_at FROM visit_history WHERE tenant_id = 1 AND competition_id = 'S' GROUP BY player_id\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: visit_history
   partitions: NULL
         type: ref
possible_keys: tenant_id_idx,idx_tenant_id_competition_id_player_id
          key: idx_tenant_id_competition_id_player_id
      key_len: 1030
          ref: const,const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)
  • EXPLAIN結果 (インデックス tenant_id, competition_id, player_id, created_at)
mysql> explain SELECT player_id, MIN(created_at) AS min_created_at FROM visit_history WHERE tenant_id = 1 AND competition_id = 'S' GROUP BY player_id\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: visit_history
   partitions: NULL
         type: ref
possible_keys: tenant_id_idx,idx_all_cover
          key: idx_all_cover
      key_len: 1030
          ref: const,const
         rows: 1
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)
  • スコア比較
// Before (インデックス無し)
07:03:05.717432 SCORE: 2781 (+2781 0(0%))

Count: 1152  Time=0.11s (129s)  Lock=0.00s (0s)  Rows=92.7 (106831), isucon[isucon]@localhost
  SELECT player_id, MIN(created_at) AS min_created_at FROM visit_history WHERE tenant_id = N AND competition_id = 'S' GROUP BY player_id

// After (インデックス idx_all_cover)
07:13:45.086791 SCORE: 3238 (+3238 0(0%))

Count: 4076  Time=0.01s (20s)  Lock=0.00s (0s)  Rows=153.0 (623605), isucon[isucon]@localhost
  SELECT player_id, MIN(created_at) AS min_created_at FROM visit_history WHERE tenant_id = N AND competition_id = 'S' GROUP BY player_id

idx_all_coverは「Using index」になっていて、スコアも上がっています。(他のスコアは省略して計測しませんでした)

感想

クラスタインデックス」と「セカンダリインデックス」の説明は(通称)ISUCON本[2]の説明が分かりやすいです。その本では、「クラスタインデックス」だけから情報を取得できることでの性能向上について書かれていましたが、「セカンダリインデックス」だけにアクセスすることも可能ということを知りました。またyoku0825さんの以下の説明[3]も分かりやすいです。

MySQLのインデックスはほぼB+Treeです。MySQLのB+Treeインデックスのリーフには「テーブル内での行の位置」が記録されています(MyISAMであれば.MYDファイルの先頭からオフセットバイト数、InnoDBであればクラスターインデックスの値が記録されている)。そのため、インデックスを利用した行フェッチを行う際は、以下の3ステップで行われます。 1 インデックス上から条件にマッチするリーフを探す 2 インデックスのリーフ上に書かれた情報から行の位置を探す 3 行をフェッチする

Using indexが示すのはインデックス上に書かれた情報だけで(インデックスは「ソート済みのデータの複製(サブセット)」でありインデックスを作成したカラムの値を含む)、要求された情報の取り出しが終了したため 2. と 3. のステップを省略した、というものです。

参考

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

2 達人が教えるWebパフォーマンスチューニング 〜ISUCONから学ぶ高速化の実践

3 SQL実行計画の疑問解決には「とりあえずEXPLAIN」しよう