先日(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が複雑になった場合、他の人がメンテナンスしづらくなるというデメリットがあると感じました。