Lを探す日常

I lov esoteric programming

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_ContainsORDER 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 を満たす領域である。これを得るために、右の図のように領域全体をいくつか(N^ 2とおく)のセル(ここでは(swarm)と呼ぶ)に分割する。X は全体が赤長方形に含まれるような群からなり、長方形状である。Y は一部のみが赤長方形に含まれるような群からなり、L字状である。

f:id:akouryy:20200913120245p:plain

このとき、上記クエリは

  • X 内の物件をソートされた状態で上位20個保持し、
  • Y 内の物件をソートされた状態で全て保持する

と、X と「Y 内の door_min >= c0 AND door_max >= c1 を満たす物件上位20件」の UNION ALL をとり、もう一度並び替え LIMIT 20 することで得られる。

X と Y は左下の角の数つまりN^ 2種類ずつ存在する。よって X の要素はのべ20N^ 2件である。また、各物件は Y の要素として平均N回カウントされるので、Yの要素はのべ35000N件である。ただし35000は物件数。これらは初期化時と入稿時に更新する。

更新速度は現実的にはNの線形時間である。一方 SELECT のクエリの速度は、真面目に絞り込む範囲が Y の中に限定されるので元の\dfrac{1}{N}倍程度になる。

実際は door_min <= door_max を使ってもっと削れる。

以上を頑張って実装した1

15時半頃からほかに改善できる場所が思い付かない状態が続き、16時前から考察を始め、16時半頃から実装を始めた。よほどのブレイクスルーがない限りこれ以上大きな伸びはないと考えていたので、16時半から競技終了の21時まで全部使ってでもこれを実装するという決断を下した。

結局実装のベースができるまでで2時間、そこからエラーを消し十分な速度を出すまで1時間半かかったが、なんとかそれなりに完成した。N=32520点。

N=7 にして3543点

初期化(制限時間30秒)と入稿(制限時間5秒?)がタイムアウトしないように、安全めに制限の半分程度の時間で終わるような N を選んだ。本選だったらもっと攻めて N=10 とかにしていたかもしれない。

しかしこれは予選なので、25位の点数を予想しそれより十分上なら終われば良いと思っていた。自分たちが3000点付近で壁に当たり、次に何をすればよいか分からなくなったので、他チームもこのあたりで団子になっているような気がした。従ってそれより500点程度上なら大体安全圏だろうと思い、ここでこれ以上の改善を放棄した。

追記 2020/9/13

長々と書いたが

SELECT * FROM estate WHERE δ ORDER BY γ LIMIT 20SELECT * FROM (SELECT * FROM estate ORDER BY γ) estate WHERE δ LIMIT 20 にするだけでいいのでは…?(未検証)

ただし δdoor_min >= c0 AND door_max >= c1

追記 2020/9/14

流石にそれではだめで、結果が20件未満になるぐらい c0 や c1 が最大値に近いと全件舐めることになる。

しかし右端と上端の swarm に属する物件だけ競技中の手法を用い、それ以外は上の追記の手法を使えば更新速度が(20N+定数)みたいな感じになってNをめっちゃ増やせそう(ここまで空論)。

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 が効かないことを知った上で臨めるからさらに上位を目指せるね!

それ以外だと、上の(swarm)のやつはめっちゃ頑張ったけど既存でもおかしくない。適当にググっただけでは見つからなかったが、何か一般名称があるなら知りたい。

またそもそも群に分割する以外の解法が使えるかもしれない。感想戦では人々が SPATIAL INDEX に触れていた。これは幾何検索の文脈で、そっちは分からないのでおいておくが、door_min の方でも長方形の内部を列挙するのが重要なので何か使えるのかもしれない。SPATIAL INDEX について全く知らないので適当なことしか言えないけど。

ベンチマーカーの気持ちを考えるのも競技中ずっとやっていて、なんか検索結果を並べ替えてみたり件数を増やしてみたりしたが、ベンチマーカーが厳しくて fail した。また「椅子が通れる物件検索」が一番コンバージョン率が高いのかと思ったりしたがよく分からない。今回は作者の気持ちを考えるフェーズはなくて単に高速化するだけだったのだろうか。採点基準が購入数と資料請求数なのでそういうフェーズもありそうだったが。

 

以上のようなことを今は考えています。皆さんお疲れ様でした。本選もがんばるぞい!

https://twitter.com/yosuke_furukawa/status/1305519659551084544


  1. まあ DESC INDEX が効いてなくてソートされた状態では保持されてないんだけどね

  2. どこかで予選と本選の問題作成者は別という情報を見ていたはずなのに忘れていました、申し訳ございません Tweet