ISUCON10予選を3位で通過しました (vs descending index)
2020年9月12日に行われたISUCON10予選にチーム FetchDecodeExecWrite で参加し、3804点、3位で通過しました!やったことを書いていきます。
要点
- Descending Index が効いていると思い込み、その誤りに最後まで気付かなかった。
- 椅子に似合う物件検索を色々分割して頑張った。大きい変更で苦労したがそこそこ役に立った。
追記 2020/9/14 過去のISUCONの参加記
cookies.hatenablog.jp cookies.hatenablog.jp akouryy.hatenablog.jp ISUCON9本戦は書いてない
12:20 競技開始
この記事を見てマニュアルを印刷しようと思ったため、競技開始時はコンビニにいた。
印刷後部屋に戻り、サーバーからの initial commit が終わるまでマニュアルを読んだ。今回は簡潔で嬉しかった。
マニュアルを読みながら次の点などについてメンバーと話した。
- 椅子に似合う物件を返せばコンバージョンも上がる?
- ベンチマークは静的ファイルを見ないらしいのでそこは適当でいい。
- スコアは購入と資料請求のみなので検索結果の選び方、並べ方が重要になるかもしれない。
- ISUCON8 本選で我々を苦しめたキャンペーンレートはない。
- bot を弾くのは簡単そうで明記されているので初手でやる。
Go 初期実装で 464点。
※ 以降は自分がやった変更以外は書いていなかったり適当だったりします。
13:03 pprof 追加
結局追加だけして最後までメンバー誰も一度も見なかったようだが、一度でも見ていたらもっとスコアが上がったかもしれない…… (JSON 整形とか?)
13:45 検索用のカテゴリの generated column を追加
物件の詳細条件検索の際、たとえばドアの高さとして指定できるのは「80cm 未満」「80〜110cm」「110〜150cm」「150cm 以上」の4つのみだった。これを毎回 BETWEEN
句でやるのは無駄なので、generated column を追加した。
-- door height category dh_cat INTEGER AS ((CASE WHEN (door_height < 80) THEN 1 WHEN (door_height < 110) THEN 2 WHEN (door_height < 150) THEN 3 ELSE 4 END)) NOT NULL,
ほかにもいくつか入れた。インデックスも適当に入れた。730点。
13:50 インデックス強化(嘘)
来るクエリを見て INDEX (rent_cat)
を INDEX (rent_cat, popularity DESC, id ASC)
に変えるなどした。
この後スロークエリを見て改善した気でいたが、実は MySQL 5.n なので popularity DESC
はあまり効いてなかったらしい。感想戦で人々が「Descending Index を使うために MySQL のバージョンを8に上げた」と言っていたのを見て知った。競技終了後 EXPLAIN
をしてみると確かにこのクエリでは using filesort
が残っていた。
文法エラーも出なかったし、DESC INDEX が使えないという発想自体がなかったのでスルーしてしまった。この後同じミスを何回もすることになる。533点(このあたりの点数変動はベンチマーカーのぶれの範囲かもしれなかったりほかのメンバーの施策も関係していたりでそこまで気にしていなかった)。
14:00 椅子がドアを通るかどうかの判定を修正
物件検索には「指定した椅子が(回転すれば)ドアを通れる」という条件のものがあり、元の実装では椅子の高さ・幅・奥行きから2つを選ぶ順列6通りのそれぞれでドアの通過条件を書き下し、それらの論理和でクエリを構成していた。
WHERE (ChairWidth <= door_width AND ChairHeight <= door_height) OR (ChairWidth <= door_width AND ChairDepth <= door_height) OR (ChairHeight <= door_width AND ChairWidth <= door_height) OR (ChairHeight <= door_width AND ChairDepth <= door_height) OR (ChairDepth <= door_width AND ChairWidth <= door_height) OR (ChairDepth <= door_width AND ChairHeight <= door_height)
しかし、ドアの幅と高さのうち小さい方を door_min
, 大きい方を door_max
とおき、椅子の高さ・幅・奥行きのうち最小のものを c0
, 中央値を c1
とおくと、この条件は c0 <= door_min AND c1 <= door_max
と同値である。よって door_min
, door_max
を generated column で持たせて、OR
のないクエリで表した。
14:52 chair テーブルにもカテゴリの generated column を追加
626点。ちなみにこのあたりまで1時間ほどベンチマーカーが止まっていたため色々な施策が混ざっていて、それぞれにどれぐらい効果があったかは分からない。
15:05 前述の door_min にインデックス追加
はい
15:14 データベースを2台に分散 ⭐️
MySQL が律速だったので chair テーブルと estate テーブルを別のマシンで管理するようにしたらしい。1331点。
14:50 幾何検索の N+1 を修正 ⭐️
したらしい。1971点。
15:39 緯度経度のインデックス追加
2087点。関係ないがマニュアルの「郊外に住む」に何か意味があるのかと話したりした。結局分からなかったが。
15:54 幾何検索のクエリをさらに改善 ⭐️
SELECT * FROM estate WHERE α AND ST_Contains(β) ORDER BY γ LIMIT 50
を
SELECT * FROM (SELECT * FROM estate WHERE α ORDER BY γ) WHERE ST_Contains(β) LIMIT 50
に変える、つまり重そうな関数 ST_Contains
を ORDER BY
句より後に処理させることで呼び出し回数を上から抑えたらしい。2978点。
17:36 SQL_CALC_FOUND_ROWS でなんかした
らしい。3127点。
19:56 ドアのサイズの絞り込み ⭐️
15時半あたりで一番重いクエリが
SELECT * FROM estate WHERE (door_min >= ? AND door_max >= ?) ORDER BY popularity DESC, id ASC LIMIT 20
になった。まあつまり14:00に改善したクエリで door_min >= c0 AND door_max >= c1
にインデックスが効かないのが悪いのだが、それを解消するのは難しい。
しかし、平方分割的な適当な分割をやるとうまくいきそうだというアイデアが湧いてきた。下の画像で、左の図の赤の長方形部分が door_min >= c0 AND door_max >= c1
を満たす領域である。これを得るために、右の図のように領域全体をいくつか(とおく)のセル(ここでは群と呼ぶ)に分割する。X は全体が赤長方形に含まれるような群からなり、長方形状である。Y は一部のみが赤長方形に含まれるような群からなり、L字状である。
このとき、上記クエリは
- X 内の物件をソートされた状態で上位20個保持し、
- Y 内の物件をソートされた状態で全て保持する
と、X と「Y 内の door_min >= c0 AND door_max >= c1
を満たす物件上位20件」の UNION ALL
をとり、もう一度並び替え LIMIT 20
することで得られる。
X と Y は左下の角の数つまり種類ずつ存在する。よって X の要素はのべ件である。また、各物件は Y の要素として平均回カウントされるので、Yの要素はのべ件である。ただし35000は物件数。これらは初期化時と入稿時に更新する。
更新速度は現実的にはの線形時間である。一方 SELECT
のクエリの速度は、真面目に絞り込む範囲が Y の中に限定されるので元の倍程度になる。
実際は door_min <= door_max
を使ってもっと削れる。
以上を頑張って実装した1。
15時半頃からほかに改善できる場所が思い付かない状態が続き、16時前から考察を始め、16時半頃から実装を始めた。よほどのブレイクスルーがない限りこれ以上大きな伸びはないと考えていたので、16時半から競技終了の21時まで全部使ってでもこれを実装するという決断を下した。
結局実装のベースができるまでで2時間、そこからエラーを消し十分な速度を出すまで1時間半かかったが、なんとかそれなりに完成した。 で2520点。
にして3543点。
初期化(制限時間30秒)と入稿(制限時間5秒?)がタイムアウトしないように、安全めに制限の半分程度の時間で終わるような を選んだ。本選だったらもっと攻めて とかにしていたかもしれない。
しかしこれは予選なので、25位の点数を予想しそれより十分上なら終われば良いと思っていた。自分たちが3000点付近で壁に当たり、次に何をすればよいか分からなくなったので、他チームもこのあたりで団子になっているような気がした。従ってそれより500点程度上なら大体安全圏だろうと思い、ここでこれ以上の改善を放棄した。
追記 2020/9/13
長々と書いたが
SELECT * FROM estate WHERE δ ORDER BY γ LIMIT 20
を SELECT * FROM (SELECT * FROM estate ORDER BY γ) estate WHERE δ LIMIT 20
にするだけでいいのでは…?(未検証)
ただし δ
が door_min >= c0 AND door_max >= c1
。
追記 2020/9/14
流石にそれではだめで、結果が20件未満になるぐらい c0 や c1 が最大値に近いと全件舐めることになる。
しかし右端と上端の swarm に属する物件だけ競技中の手法を用い、それ以外は上の追記の手法を使えば更新速度が定数みたいな感じになってをめっちゃ増やせそう(ここまで空論)。
20:15 解析用のコード等の無効化、再起動確認、点数ガチャ
再起動の確認は4回ぐらい行った。点数ガチャはそれを含め7回ぐらい。
3804点!!!
残り45分は雑談などをしていた。今回は競技時間が8時間40分だったが結局8時間ぐらいで全ての作業を終えることとなった。これ以上は気力が持たない。
結果、感想、反省
再起動試験も無事パスし、3位通過となった。本選もがんばるぞい!
今回の問題は比較的簡潔だったように感じる。簡潔な問題は好きなので嬉しい。<追記 2020/9/14: ここにあった文章は偽なので削除されました2>
今回3位ではあったが、さらにちょっと注意すれば格段orそれなりにスコアを伸ばせたところがあった。上述の Descending Index である。なんとなく SQL では DESC INDEX を使えて当然だと思い込んでいて、古いバージョンでは使えないという発想がなかった。できないなら INDEX の定義に DESC と書いた時点で文法エラーか何かで弾いてほしい。受け取っておきながら無視するのはひどい。
しかし思い込みを正す機会はいくらでもあった。まず今回一度も EXPLAIN
しなかったが、もししていれば using filesort
が見つかっていたはずである。それなりに暇な時間もあったのでこういうことを念のためしておくべきだった。また、スロークエリログで LIMIT 25
のくせに平均0.1秒かかっているやつもあった。これを目に留めていればインデックス以外を使っていることは明らかである。ところが実際にはクエリの回数と合計実行時間はよく見ていたが、平均時間はあまり気にしていなかったようだ。さらに、インデックス追加直後などに SHOW CREATE TABLE
をしてインデックスが入っていることを確認していたが、その際の出力が KEY `rent_cat` (`rent_cat`,`popularity`,`id`)
のように DESC
の入っていないものになっていたので、そこまで気が回っていれば怪しめたと思う。どれをとっても悔しい。
まあ、DESC INDEX によるスコア向上がどの程度かは知らないが、それでも3位に入れたのはすごいと思う(自画自賛)。本選は DESC INDEX が効かないことを知った上で臨めるからさらに上位を目指せるね!
それ以外だと、上の群のやつはめっちゃ頑張ったけど既存でもおかしくない。適当にググっただけでは見つからなかったが、何か一般名称があるなら知りたい。
またそもそも群に分割する以外の解法が使えるかもしれない。感想戦では人々が SPATIAL INDEX に触れていた。これは幾何検索の文脈で、そっちは分からないのでおいておくが、door_min の方でも長方形の内部を列挙するのが重要なので何か使えるのかもしれない。SPATIAL INDEX について全く知らないので適当なことしか言えないけど。
ベンチマーカーの気持ちを考えるのも競技中ずっとやっていて、なんか検索結果を並べ替えてみたり件数を増やしてみたりしたが、ベンチマーカーが厳しくて fail した。また「椅子が通れる物件検索」が一番コンバージョン率が高いのかと思ったりしたがよく分からない。今回は作者の気持ちを考えるフェーズはなくて単に高速化するだけだったのだろうか。採点基準が購入数と資料請求数なのでそういうフェーズもありそうだったが。
以上のようなことを今は考えています。皆さんお疲れ様でした。本選もがんばるぞい!
https://twitter.com/yosuke_furukawa/status/1305519659551084544