先日(1/12)の技術雑談会で「Railsでデータベースビューを使ってみた」というテーマで話をしました。 そのときの内容をまとめた記事です。
データベースビューを使用した理由
1回のリクエストで複数回のSQL実行を行っているモデルがあり、それがパフォーマンス低下の原因になっていました。
下記の例では、Accountのインスタンスに対して各メソッドを呼び出すとそれぞれSQLが実行されます。 これが1回のリクエストですべてのメソッドが呼び出されて、SQLがそれぞれ実行されると少し冗長な気がします。
class Account
def initialize(customer)
@customer = customer
end
def nickname
Profile.find_by(customer: @customer).nickname
end
def article_count
Article.where(customer: @customer).count
end
def order_count
Order.where(customer: @customer).count
end
end
そこで、複数のDBテーブルにまたがるデータを1回のSQL実行で取得することができるデータベースビューを使えば、SQLの実行回数を少なくすることができ、パフォーマンスの改善が見込めると考えました。
データベースビュー作成・管理するために使用したgem
Railsでデータベースビューを使用するにあたって、マイグレーションファイルに直接SQLを書いてデータベースビューを作成することもできるのですが、今回はデータベースビューの作成・管理に便利なscenicというgemを使用しました。 scenicを使うことで、Railsのマイグレーションシステムと同じ用にデータベースビューを扱うことができます。
今回の場合、自動生成された*.sqlファイルに下記のSQLを記載し、マイグレーションするだけでデータベースビューを作成することができました。
SELECT
customers.id AS customer_id,
profiles.nickname AS nickname,
T1.article_count AS article_count,
T2.order_count AS order_count,
FROM customers
LEFT JOIN profiles ON profiles.customer_id = customers.id
LEFT JOIN (
SELECT
customers.id AS customer_id,
count(articles.id) AS article_count
FROM customers
INNER JOIN articles ON articles.customer_id = customers.id
GROUP BY customers.id
) AS T1 ON T1.customer_id = customers.id
LEFT JOIN (
SELECT
customers.id AS customer_id,
count(orders.id) AS order_count
FROM customers
INNER JOIN orders ON orders.customer_id = customers.id
GROUP BY customers.id
) AS T2 ON T2.customer_id = customers.id
scenicの使い方は非常にシンプルで分かりやすいため、READMEをみればすぐに使えるようになると思います。
データベースビューに対応するモデルの扱い
RailsのActiveRecordではDBに作成されたデータベースビューを何も考えず参照することができます。 例えば、account_summariesというデータベースビューを作成した場合、AccoutSummaryというモデルを定義するだけで、下記の用に参照することができます。
> bin/rails console
> AccountSummary.find(<ID>)
=> #<AccountSummary:0x000000012125ecd0
nickname: "murakami",
article_count: 3,
order_count: 4>
ただしデータベースビューの場合、主キーがないので明示的に設定する必要があります。
class AccountSummary < ApplicationRecord
# customer_idを主キーに設定する
self.primary_key = :customer_id
end
データベースビューを使用した結果・感想
はじめに示した例をデータベースビューを使用した実装に書き換えると以下のようになります。
class Account
delegate :nickname, :article_count, :order_count, to: :account_summary
def initialize(customer)
@customer = customer
end
private
def account_summary
@_account_summary ||= AccountSummary.find(@customer.id)
end
end
SQLを実行する回数が1回に減り、パフォーマンスの向上につながりました。また、モデルの実装が少しシンプルになったような気がします。
一方で、データベースビューを作成するためのSQLが複雑になった場合、他の人がメンテナンスしづらくなるというデメリットがあると感じました。