ぼくの考えた最強のアルゴリズムを雑に検証してみたら、想定通り駄目なアイディアだった話

この記事は、はてなエンジニア Advent Calendar 2024の 29日目の記事です。昨日は id:todays_mitsui さんの「React の className と SolidJS の class」でした。

頼りないアイディア

日の下に新しきものなしという有名な言葉があります。いろんな使われ方がありますが、私は、これを「思いついたことの先行事例がないか考えてみよう」という意味でも捉えています。

さて、私はかねて思っていました。モデル層で書いた状態を判定するためのロジックを永続化層へのクエリでも管理しないといけないのがつらいな、と。我々は、アクティブな状態のユーザーを検索したり、ユーザー管理画面を開いたときユーザーの表に「アクティブ」と表示させたいのであって、別にrepository.findActiveUsersで返ってくるレコード全てに対してuser.isActiveがtrueであることを確認したりしたいわけではないはずです。そんなわけで、なんとかSQLはシンプルに保ちつつ、アプリケーション側でフィルタする方向に倒せないかなと考えていたのですが、ある日思いついたのです。

「ゆるくSQLで絞り込んでからアプリケーションできちんと絞り込む。欲しい件数に足りていなければ、カーソルを動かしてもう一回クエリ、もう一度絞り込む。件数が十分になったら打ち切って結果を返す。どうだ!これなら完璧なのでは?」

自分は天才なんじゃないかと5秒くらい思いましたが、日の下に新しきものなし、です。いいアイディアなら、誰かが思いついて実用されているんじゃないか?調べてみると、先行事例を見つけることはできませんでした。どうにも雲行きが怪しいですね。自分はヘンテコな思いつきをしてしまったのではないか?

まあ、冷静になって机上で考えただけでも不穏な点はあります。例えば、テーブル全体を走査して結果が0件になるような状況なら、レコード数 / ページサイズ だけのクエリが発行されることになるでしょう。基本的にはすぐ見つかるが、時々例外的に除外したいものが混じっている、程度の状況にしか使えないのではないかという予想はすぐにつきます。

それでも試してみる

予想を立てて諦めるのもいいのですが、目の前にあるキーボードをちょっと叩けば、この程度のアイディアの検証はすぐできるはずです。ちょうどGoの勉強をしたかったこともあり、実際にやってみることにしました。

github.com

1回のクエリに100ms掛かる検索処理で一定数のレコードを取得したあと、乱数によって一定確率でアプリケーション側のフィルタによって絞られる。件数が必要数に足りなければ、もう一度クエリを投げて同じことをする。必要数を満足するまでこれを繰り返す。コードにすると、以下のような処理です(全体は上記リポジトリ参照)。

func Find(after FooId, limit uint, isActiveThreshould uint) []Foo {
    r := rand.New(rand.NewSource(time.Now().UnixNano()))
    randomNumber := r.Intn(100)
    rng := lo.Range(int(limit) * 2)
    time.Sleep(QUERY_EXECUTION_TIME_SECOND)
    return lo.Map(rng, func(item int, index int) Foo {
        return Foo{
            ID:       FooId(item),
            IsActive: randomNumber < int(isActiveThreshould),
        }
    })
}

このようなアルゴリズムによって得られた結果がこちらになります。

アプリケーション層フィルタ処理通過率と処理時間の関係

急激に処理時間が延びていく様子がわかります。うーん。厳しい!通過率50%までは要件によってはギリギリ許容されるかな~という気もしますが、そもそもクエリをちゃんと書いていたら不要な心配なわけで、あまり検討されないのもむべなるかな、という結果に終わりました。予想を裏切らない結果ですね。

感想

このように手軽にベンチマークが書けるエコシステムが整っているのはGoのいいところですね。考え込む前にまず実験!という動きをするにはとても便利です。

今回考えたアイディアは日の目を見ませんでした。似たような、しかし微妙に違う絞り込みロジックが沢山あるとき、それぞれのクエリに名前を付けていくのが結構大変なんですよね。しかし、そうした問題を解決する方法としては、別の方法を考えなくてはいけないようです。モデル層のロジックとクエリそれぞれを考えなくてはいけないことは受け入れて、片方を変えたときもう一方の変更が漏れないような方法を工夫する方が筋がよいのかもしれません。

はてなエンジニア Advent Calendar 2024、明日は id:yutailang0119です。