ねこものがたり

いちにちいっぽ

「”Rubyでこれをやりたい”と思った時にはやりたいことができる状態になっている」ということ(RubyKaigiの感想の延長)

前段

先日開催された「After RubyKaigi 2022〜メドピア、ZOZO、Findyの参加者がLTします〜」(長いので以下After RubyKaigi )というYouTubu Live配信形式のイベントに参加しました。 その日は火曜だったのでAsakusa.rbにその日参加したメンバーでわいがやしながら視聴する形式*1となって、個人的には斬新で楽しい参加となりました。

ところでRubyKaigi 2022に参加しました - ねこものがたりを書いたけれど、記事の中では具体的な登壇の感想はほとんど触れていませんでした。しかし後日、新たに感じたことがあるのでそれを書いておきます。

出来事

After RubyKaigiの中で、Wasmで簡単な実装をしてみたよというトークがありました。 それを受けて、また前々から理解ができていなくて疑問だった点をAsakusaの皆さんに質問してみました。 「Rubyがブラウザで実行できるのはすごい画期的だと思うのですが、Rubyでの使い所がわかりません。JSの代わりにするだけなら好みの話になりそう」というような内容です。

その答え(正確には「答え」というのはなく、一意見というのが良いかもしれませんが、コミッターの方々からのレスポンスは「答え」という感じがする)は「今はまだないのでは」「今から生み出していくものでは」というものでした。

この出来事から思ったこと

話は少し変わって、Ruby Committers vs The World - RubyKaigi 2022という定番セッションがあります。Rubyコミッターの方々が壇上に揃って、最近や未来のRubyの在り方についてやんややんやと議論したり活動報告をしてくださったりする時間です。 その中で、(何についてだったか忘れたけど)「それって何のために必要なんですか?何かをやるために必要だから今開発しているというのではなくRubyで作りたいから作っている状態なのではないか」と問いかけが出て、それに対して「”Rubyで○○をしたい”と誰かが思った時にはできるようにしておきたいんだ」と意志を述べる場面がありました。

当日は私はそれがピンと来ていなかったのですが、上述の「WasmでのRubyの使い所がわからないのだが」に関するやりとりで「やりたいことができる状態になっていっている。もっと肉付けしたり、やってみたいと思ってやってみるのがこの先の自分の一歩なんだ」と思いました。電気が走るような感覚を覚えました。

この出来事から思ったこと(その2)

数年前のことですが、Rubyにパターンマッチがexperimentalで導入された時、実装された辻本さん*2が「ぜひ使って感想を聞かせてほしい」「現場での色々なユースケースを教えてほしい」とおっしゃっていたことをすごく強く記憶しています。 なぜ強く記憶しているのかあまり自分の中で心当たりはないのですが、その割には当時の自分は積極的にそのようなことはせずに過ごしていました。「APIでパターンマッチしたいときは確かにある。でもそれ以外思いつかない。思いつかない自分はだめ」みたいな謎の自己卑下に走ったりしていました。

それは今思えば、「自分はお客さんモードだったんだなあ」と痛烈に刺さって悶えるに至っています。 刺さり悶えるために鮮明な記憶に残っていたのではないかと思うほどです笑

Wasmについても、今のところ「こんなことしてみたいなー」は自分自身の中にないんですけど、なんかそれだと自分に対して物足りなさを感じていて、何か面白いことをしてみたい気もしています。 まあいきなりオリジナリティーを求めなくても、巷で既出のWasm x Rust*3なものを書き換えてみるとかから始めても良いかもしれません。

本当の感想

コミッターの人は色々な思いや興味関心から多様な活動をされていることがRubyKaigiではよくわかります。 その中の1つに「”Rubyでこれをやりたい”と思った時にはやりたいことができる状態になっている」という方向性があるのは今年のRubyKaigiに参加するまで知りませんでした。

知らなかった自分が恥ずかしい気持ちもあるけど、でもRubyKaigiに参加して知ることができました。 今回具体的にはWasmについて、パターンマッチの時にはできなかった後悔も交えて、「使い所がわからないので使わない」みたいな姿勢ではなくて、「なんか色々やってみて自分の中で蓄えたり、使ってみた感想やこんなこともできたらいいなって言えるように遊んでみよう!」と思ったし、「なんかよくわかんないけど遊んでみよう」でもいいのかなって思える日になりました。

*1:参考:meetup/2022/09/27/第679回 - asakusarb.esa.io

*2:辻本さんの紹介記事はどれがいいんだろうかと迷ったのでこちらを貼っておきます。Ruby Prize 2019 最終ノミネート者 辻本和樹 インタビューRubyPrize

*3:今の所根拠は特にないけどWasmはRustでやってるケースが多そうという印象がある。全く根拠はないんですが。

N+1問題とは

「ある日突然人に〇〇を説明することになったら説明できるか?」を時々自分に試すんですが、N+1問題についてあわあわしてしまったので、ちゃんと自分の言葉にしてみます。ついでに、そういうアウトプットの時には「#とは」カテゴリーに分類して、自分の蓄積を見返せるようにすることにしました。過去のエントリは面倒なので未対応です。

N+1問題とは?

DBのクエリの問題で、本来は1クエリで済ませられる処理をN個分余計にクエリを発行してしまっている状態。 クエリの数が多い分処理が遅くなり、Nの数が多ければ多いほどそれが顕著になる。

N+1問題が発生する状況

データ的には親子関係にあるレコードや1対多などの関連の関係にあり、あるテーブルのレコードを参照するとき同時に関連レコードも参照したい場合に起こり得るようです。

N+1問題の例

Ruby on Railsの場合で書いてみます。

# app/models/user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end

# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.all
  end
end
# app/views/users/index.html.erb
<% @users.each do |user| %>
  <p>Name: <%= user.name%></p>
  <ul>posts
    <% user.posts.each do |post| %>
      <li><%= post.body %></li>
    <% end %>
  </ul> 
<% end %>

この状態で/usersにアクセスした時のログは以下のようになります。このケースではusersレコードは5つある状態です

N+1問題が発生しているログ
N+1問題が発生しているログ

ここではSELECT users.* FROM usersでusersテーブルに対するSELECTで1回、それに加えてSELECT posts.* FROM posts WHERE posts.user_id = 1のようなpostsテーブルに対するSELECTが5回、合計6回のSELECTが走っています。N+1に当てはめると、1はusersのSELECT、今回のNはpostsテーブルへの5回のSELECTが具体的な値となります。

usersテーブルのSELECTが1回+postsテーブルのSELECTがn回実行されている
usersテーブルのSELECTが1回+postsテーブルのSELECTがn回実行されている

N+1問題を解消する

発行するSQLを効率のよいものに変えることが必要になります。 方法はいくつかありますが、Railsの場合やりたいことによって適切なメソッドが異なります。私の場合それは頭に入ってなくて何回も同じ記事を参照しているので、それを貼っておきます。

qiita.com

N+1問題の解消例

先に書いたN+1問題が発生するコードをこのように変えてみます。

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
- @users = User.all
+ @users = User.includes(:posts)
  end
end

この場合のログは以下のようになります。ここではSELECTは2回走っていて、最初で対象となるuser.idを抽出する、2回目でそのuser_idを持つpostsを抽出しています。JOINとかして1クエリにすることも可能ですが、詳しいところはこの記事に任せます。

SELECTが2回に減ったログ
SELECTが2回に減ったログ

N+1問題に気づく方法

N+1問題に気づくORMの提供するメソッドがどのようなSQLを発行しているかを確認し、その妥当性を判断する必要があります。

今の自分では3つ方法があるだろうと考えます。

1. ログを見る

最も簡単な方法として例に挙げたようにログを見る方法が挙げられます。 最も手早くできる方法です。しかし、それでは人間の目でログをさらっていき、SQLが適切かどうかの判断を下す必要がり、巨大なアプリケーションや流.量の多いログでそれをやるには限界があるなど、コスパが悪そうではあります。

2. 検知ツールを導入する

私は日頃Ruby on Railsで開発しているのでbulletというgemにはお世話になっています。ここでは簡単な説明にとどめますが、READMEにあるように、N+1問題が発生している箇所があるとログに出力して教えてくれます。この例だと、Post.includes([:comments])みたいにするといいよって教えてくれています。便利〜。

2009-08-25 20:40:17[INFO] USE eager loading detected: Post => [:comments]· Add to your query: .includes([:comments]) 2009-08-25 20:40:17[INFO] Call stack /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in each' /Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:inindex'

Rails、というかActive Record以外のORMでもN+1を検出するツールがあるそうで、以下の記事にいくつか紹介されていました。 medium.com

記事が削除されたり見れなくなってしまった時のために該当部分引用しておきます。

・spring-hibernate-query-utils, a library for Java, makes your tests fail if N+1 queries are detected

・The Database Machine, an ORM for PHP, implements a smart eager loading mechanism

・laravel-query-detector, a laravel package (in PHP), detects N+1 queries on your development environment

・nplusone, a python library for detecting the n+1 queries problem in Python ORMs, including SQLAlchemy, Peewee, and the Django ORM

3. その他:コードを見たときに脳内でSQL変換する

多分これはできる時できない時があって必須ではないと思うのですが、「これだとクエリはどうなるかな?」という観点を常時持って実装を読み書きしていれば、ログを見る前に気づけることもあるのかなというのは体感としてあります。

全体的な所感

「N+1問題とは何か」を改めて言葉にするにあたり、「なぜN+1問題が発生するのか」を考えました。いくつかの記事では「ORMを使うと発生する 」というようなことも言われていました。確かに生でSQLを書く場合はあまりこういうことは起きないかなあと思いました。とすると「ORMがDBの複雑さを隠蔽してくれているというのに甘えず、このコードでどのようなSQLが走っているのか、それは大量にレコードがあった時にどうなるのかということを常に考えましょう。そのためにそして対策が”気をつけましょう”に止まらないようにすでにある便利なツールを便利に利用したり、少しずつでもORMがやっていることを理解したり、DBそれ自体の良い使い方を知ってよく使って行きましょう」ということがN+1問題から得られる心構えかなと思いました。