みなさん、ActiveRecordを使う時に頭にSQLが浮かんでいますか?
ActiveRecordはとても便利でSQLを意識することなくDBにアクセスしてデータを取得したり、更新したりすることができます。
そのためついつい実装時にSQLを意識せずに書いてしまうことがあります。
実装時はActiveRecordを使うことで素のSQLを意識しなくてもよいのですが、最終的に実行する時にはSQLが実行されています。
そのため実際に発行されるSQLをみると、こんなSQL発行されるのと驚くことがあります。
SQLを思い浮かべながら書いていたら回避できる実装例をいくつか挙げてみます。
無駄なテーブルをJOINしている
下記のようなモデルがあったとします。
def User < ApplicationRecord
has_many :user_organizations
end
def UserOrganization < ApplicationRecord
belongs_to :user
belongs_to :organization
end
def Organization < ApplicationRecord
has_many :user_organizations
end
『organization_idで絞り込んだuserモデルを取得してください。』と言われた時にどのように実装しますか?
target_organization_id = 1
users = User.join(user_organizations: :organization)
.where(user_organizations: { organization: { id: target_organization_id }})
この実装は指定された通りに正しく動作します。発行されるSQLは下記の通り
SELECT `users`.*
FROM `users`
INNER JOIN `user_organizations` ON `user_organizations`.`user_id` = `users`.`id`
INNER JOIN `organizations` ON `organizations`.`id` = `user_organizations`.`organization_id`
WHERE `organization`.`id` = 1
このSQLをみてどう思いますか?
よく考えるとorganizationsテーブルをJOINしなくてもできることに気づくと思います。
改善後のSQLは下記の通り。
SELECT `users`.*
FROM `users`
INNER JOIN `user_organizations` ON `user_organizations`.`user_id` = `users`.`id`
WHERE `user_organizations`.`organization_id` = 1
これを実現するActiveRecordは下記の通り。
target_organization_id = 1
users = User.join(:user_organizations)
.where(user_organizations: { organization_id: target_organization_id })
ActiveRecordのモデル中心に実装を考えると、最初の実装のようについ指定されたidがあるモデルまでJOINしてしまいがちです。
実際にコードレビューをしていてもこのような実装はよく見かけます。
SQLはJOINが少なければ少ないほどパフォーマンスはよくなるので、できる限りJOINが少なくて済むように意識してActiveRecordを実装するようにしましょう。
eager_loadのLEFT OUTER JOIN
先ほどと同様に『organization_idで絞り込んだuserモデルを取得してください。』に加えて、後にuser.exam_organization
を使いたいのでキャッシュしておきたい場合、どのように実装しますか?
先ほどの実装のままだと、exam_organizationsがキャッシュされていないのでexam_organizationを取得するたびにSQLが発行されてしまい、N+1になってしまいます。
そこで下記のようにjoinsをeager_load(またはincludes)に変更することでキャッシュされるようになります。
target_organization_id = 1
users = User.eager_load(:user_organizations)
.where(user_organizations: { organization_id: target_organization_id })
これで無事キャッシュされるようになるのですが、発行されるSQLをみてみるとINNER JOINがLEFT OUTER JOINに変わってしまっていることに気づきます。
SELECT `users`.id AS t0_r0, ...(全カラム列挙される。長いので省略)
FROM `users`
LEFT OUTER JOIN `user_organizations` ON `user_organizations`.`user_id` = `users`.`id`
WHERE `user_organizations`.`organization_id` = 1
SQLを考えずにRailsを書いている場合、INNER JOINで良いところが今回の例のようにLEFT OUTER JOINになっていても気にしないことが多い気がします。
ただSQLが頭に浮かんでいると、必ずデータがある結合なのにLEFT OUTER JOINを選ぶことはあり得ないので違和感しかないです。
このような場合は下記のようにjoinsも追記することでINNER JOINで結合しつつデータをキャッシュすることができます。
target_organization_id = 1
users = User.eager_load(:user_organizations).joins(:user_organizations)
.where(user_organizations: { organization_id: target_organization_id })
最後に
いくつか例を上げてみましたが、どちらもSQLを頭に浮かべながら書いていたら簡単に避けれるようなものばかりです。
多少非効率なことをしていても大抵の場合は問題なく動くので気づかないことが多いですが、積み重ねでパフォーマンスに差が出たりするので、これまでActiveRecordを使う時にSQLを意識してこなかった方も発行されるSQLを意識してみると良いと思います。