【インターンレポート】LINEスキマニの求人レコメンド配信におけるMySQLクエリの改善

自己紹介

東京大学情報理工学系研究科 修士1年の新道明吉と申します。

技術職の就業型インターンシップに参加し、LINEスキマニという新規サービスのサーバーサイド開発を担当させていただきました。以下ではインターン中に取り組んだ業務(主に2つ)、そしてインターンの感想などを書いていきます。

LINEスキマニ

LINEスキマニとは、ユーザーの「ちょっと空いている時間に働きたい」というニーズに応え、「急に予定がなくなった」「数時間だけ空いている」といったスキマ時間に、自分の経験・スキルに合ったアルバイトを探して応募すると、選考なしでその店舗で働くことができます。従来のアルバイトでは面接して採用を待つ過程がありますが、LINEスキマニではそういった選考の過程がないため、本当に短いスキマ時間でも、求人があれば働いてお金を稼ぐことができる魅力的なサービスです。

スキマニサービスは主にKotlin + Spring Bootを用いて開発されているのですが、僕は全く触ったことがなかったため、初めの期間はKotlinとSpring Bootの勉強も兼ねて軽いタスクをこなしていました。

インターン中に取り組んだ業務

サブタスク: ペナルティポイント取り消し用のフォーム画面を作成

LINEスキマニにはペナルティポイントという制度があります。ユーザーが企業とマッチングを成立させた後、その企業でアルバイトすることになった際に、直前でキャンセルをしたり、無断で遅刻や欠席をした場合にペナルティポイントが与えられます。このペナルティポイントが一定以上溜まると、利用停止などの処置が下されるようになっています。

しかし、アクシデントなど事情によってはペナルティポイントを付けるべきでは無い場合もあるため、確認を取った上で例外的に取り消しを行う事もあります。これまで取り消し操作は、運営から開発エンジニアに依頼し、専用の内部ツールで実行していましたが、フォーム画面を実装することで、運営側で画面を操作して直接取り消し対応ができるようになります。これによって、運営側だけで操作が完結するため、開発エンジニアの取り消し作業を待たなくてよくなり、開発エンジニアも手をとめて取り消し操作対応をする必要がなくなるので、双方にとって利益があることになります。

実際にフォーム画面の実装とサーバーサイドの更新処理の両方を担当しました。サーバーサイド担当ですが、フロントの経験も積めてちょっと得した気分です。実装した画面は以下のようになりました。取り消すポイントをフォームに記入して実行すると、現在のペナルティポイントの部分が指定したポイントだけ減り、動作できていることがわかります。

HTMLはThymeleaf、CSSはkoromo(Bootstrap をベースに、LINE独自のカスタマイズを加えたCSSフレームワーク)を用いて開発しました。画面と機能の実装の両方を行い、Localで正しく動作することを確認しましたが、Git上のPull Requestには、さらにユニットテストも含める必要があり、すべてを一人で書くのは大変でした。とくにユニットテストについては、機能の実装では意識しなかった機能(mockなど)を使うため、初めて見るような書き方や文法が含まれており、かなり苦労しました。しかし、Pull Reqeustを作成し、プロジェクトにマージされるまでの実装を経験することで、Spring Bootでの開発に慣れることができました。

ただ、今思うとこのタスクでは実装することより、企画側と連絡を取って、仕様を調整するところがメインになっていました。作成された仕様について開発チームで検討したところ、他の機能の仕様との整合性を考えた際に食い違っている部分があることに気がつきました。そこで、企画と開発で認識を揃えるために開発チーム側でミーティングを行い、現状の仕様に関して変更したい箇所をまとめて、企画との調整役としてそのまとめた内容を企画側に伝え、仕様を完全に固めました。

今回のタスクは途中で仕様変更が入りましたが、少しの変化ですのですぐに対応できました。そもそもSpring Bootの開発に慣れるためのタスクなので、ちょうど良い勉強になりました。チームメンバーからコードレビューを受けてメソッド名のルールなどを指摘され、チーム開発ならではのルールがあることも知ることができました。また、要件キャッチアップ・詳細設計・仕様調整・本番リリースまでの開発の流れを経験でき、企画側ともコミュニケーションを取って開発を進めるという経験ができたため、非常に為になりました。

メインタスク: 求人のレコメンドシステムで配信対象ユーザーを絞るSlow queryの高速化

今回のメインタスクになります。

ユーザーへのレコメンド条件(店舗から一定の距離以内にいる、同じ店舗の求人をお気に入り登録しているなど)がいくつかあり、これらの条件から対象ユーザーを絞る際にSQLクエリを実行しているのですが、現状最も遅い場合に40sec近くかかっていました。このような遅いクエリのことをSlow query(スロークエリ)と呼んだりします。これを高速に実行できるよう改善していくことが目標となります。

改善していくクエリ文の構成は以下のようになっています。言語はMySQLです。(実際のSQL文は載せられないため、抽象化して簡易な表現をしています)

RDB(Relational Database)にはインデックスという概念があり、インデックスを張ることでデータの検索が高速に行えます。RDBは通常テーブルの形でデータを持っていますが、インデックスを張ることで木構造のデータを別で用意します(厳密には木構造以外もあります)。葉の部分にはインデックスを張ったデータをソート済みの状態で並べておくことで、テーブルのデータを上から順番に探索していくより効率よく検索を行えます。線形探索から二分探索になるイメージです。

【推測した原因と提案手法】

一般にWHERE句の条件でORを用いるとインデックスによる高速な探索が効かなくなってしまうと言われています。EXPLAINで上記のクエリ文の実行計画を確認したところ、typeがindex、つまりフルインデックススキャンされている状態になっていました(インデックスを全て読み込んでいるため、二分探索などが効いていないことになる)。ですので、このフルインデックススキャンされている状態がSlow queryの原因ではないかと考え、それを引き起こしているWHERE句の後ろのORを取り除く方針で解決しようと思いました。具体的には、WHERE句の後ろにあるOR条件をすべて分割して、独立したクエリとしてそれぞれ実行し、その結果をアプリケーション側で結合処理するという方法を考えました。(下図参照、条件2つを分割する場合のイメージ)

このようにすれば、分割したそれぞれのクエリにおいてインデックスが効くようになり、トータルで速くなるだろう予想しました。一度この方針で解決できるのかをLINEスキマニのDBA(Database Administrator : いわゆるデータベースの専門家)チームに相談しました。そこで実際に本番と同じデータセットのもと、フルインデックススキャンで実行してもらいましたが、結果は1sec未満で、40secほどかかるSlow queryにはほとんど影響していないことがわかりました。

【本当の原因と解決方法】

EXPLAINの実行結果にDEPENDENT SUBQUERY(相関サブクエリ)が出ているのを見逃しており、これが本当の原因でした。条件を判定するところで、本来ならばIN句の後ろのサブクエリがマテリアライズされる(実行結果のテーブルが保持される)はずが、毎回そのサブクエリが実行されて相関サブクエリの状態になっているものがあり、この部分が非常に遅くなっていました。

マテリアライズされない場合にクエリ全体を実行すると、例えば上の図のように、テーブル(橙色)の行を1つずつ見てIN条件を実行する際に、その都度サブクエリのテーブル(青色)を作成して、終わったらテーブル(青色)ごと消去をするため、サブクエリのテーブル(青色)を作成するのに時間Tかかるとしたら、テーブル(橙色)がN行あるときにはおおよそN×Tの時間が掛かることになります。一方マテリアライズされる場合は、最初に一度サブクエリのテーブル(青色)を作成し、あとはクエリの全ての行でIN条件を実行する際に、そのテーブル(青色)を参照すればよく、およそ(N行の探索時間)+Tの時間になります。Tが大きいとマテリアライズされない場合には非常に遅くなることがわかると思います。特に今回近隣地域を計算するサブクエリでは座標を元に範囲を計算しているため処理がかなり重く、時間がかかっていました。

マテリアライズされない問題を解決するために、該当するサブクエリの結果を先に求めて、現クエリにリテラルの状態で渡すようにしたところ、実行時間を2-3secにまで抑えることができ、40secから大幅に改善できました。(リテラルで渡す部分はMyBatisというO/R Mapperのフレームワークを使うことでできます)

【当初の提案手法の検証】

OR条件を分割した場合でも検証しました。結果は分割したクエリ実行時間を合計しても元々のクエリ実行時間より早くなりました。ただ、EXPLAINで確認したところ、相関サブクエリではなく、通常のサブクエリとして実行されているため、インデックスが効いたおかげで速くなったというよりは、やはり相関サブクエリではなくなったから(IN句の後ろのサブクエリがマテリアライズされたから)速く実行できたと考えられます。ORを分割した場合に相関サブクエリではなくなったのもSQLのoptimizerが関連している可能性がありますので、今回のデータがたまたま分割して速く実行できただけかもしれません(ここをもっと検証してみたかったのですが、時間が足りませんでした)。そのため、確実に相関サブクエリをなくすためには、IN句の後ろにサブクエリを置くのではなく、MyBatisを使ってサブクエリを別で処理した結果をそのままリテラルで埋め込んであげるという方法の方が今回の場合確実でした。

その他

オフィス出社

基本リモートで仕事をしていましたが、オフィスを見学してみたいのと、オフィスで実際の社員さんのように仕事をしてみたいため、インターン3週目から毎週金曜日に感染対策をした上で四ツ谷オフィスに出社させていただきました。非常に綺麗で素敵なオフィスで、高いところから見渡せる景色もよかったです。人間よりちょっと大きいサイズのブラウンの人形があり、かわいかったです。昼はLINEオフィスにあるカフェで食べましたが、サラダが100円のvolumeとは思えないくらい多かったです。近所のスーパーで似た大きさのサラダが300円で売られていたので、もう近所のスーパーではサラダが買えないですね。

インターンを終えて

6週間は長いようであっという間でした。

インターン中は常に自分の知らないことに向き合い続け、ハードながらも様々な学びが得られて楽しかったです。特に今回行った業務では、チーム開発で意識すべきこと、要件設計から本番リリースまでの開発の流れ、SQLの知識など、インターンで経験してみたいことの多くを経験し、実際のエンジニアの働き方を知ることができました。また、企画の方とコミュニケーションをとって仕様の調整をしたり、DBAの方にSQLのことで相談したりなど、大規模なチームの中で連携しながら働いている感じがしました。

基本リモート勤務でしたが、メンターには毎日ZoomやSlackで頻繁に連絡を取り、リモートでも相談しやすい環境を作っていただきました。最後まで楽しく業務を行えたのもメンターや開発メンバーのサポートのおかげです。この6週間で身につけた知識や経験を大事にして、今後の開発に活かしていきたいと思います。