少し調べたのでメモ。 なお、調べた時のバージョンは、PostgreSQL 9.4.2のRails 4.2.1。
マテリアライズドビューとは、簡単に言うとテーブルのように実体を持ったビューのこと。PostgreSQL 9.3から使用可能。
ビューは使用するたびに、結合したり演算したりしますが、マテリアライズドビュー計算結果を保持するのでビューに比べるとパフォーマンス的に有利とのこと。
マテリアライズドビューは CREATE MATERIALIZED VIEW コマンドで作成する事が出来ます。
CREATE MATERIALIZED VIEW mymatview AS SELECT * FROM mytab;
マテリアライズドビューはビュー作成時のデータで構成されているので、データが常に最新であるとは限りません。最新のデータを使用したい場合はビューのリフレッシュを行う必要があります。
REFRESH MATERIALIZED VIEW mymatview;
これで最新のデータでビューが再構築されます。
ただこれには問題があって、REFRESH MATERIALIZED VIEW は対象のマテリアライズドビューに対して、AccessExclusiveという、最も強いモードのテーブルロックを取得してしまいます。その為、リフレッシュ途中のマテリアライズドビューに対するSELECTコマンドは、リフレッシュが終わるまで待たされてしまいます。
これに対する解決策として、PostgreSQL 9.4 からCONCURRENTLY というパラメータがREFRESH MATERIALIZED VIEWコマンドに指定可能となりました。
REFRESH MATERIALIZED VIEW CONCURRENTLY mymatview;
CONCURRENTLY オプションを指定すると、Exclusiveという一段階弱いモードのロックしか取得せず、対象マテリアライズドビューに平行してSELECTコマンドを実行できます。 リフレッシュ中に実行されたSELECT命令には、以前のデータが返ります。
なお、CONCURRENTLY オプションは、マテリアライズドビューに、列名だけを使い、すべての行を含むUNIQUEインデックスが少なくとも1つある場合にのみ使用可能との事です。
こっからが本題。 と言っても、ActiveRecordはビューを普通に扱えるので特別な対応は不要。
因みに、以降のサンプルに使用したアプリは y-yagi/materializedviews_sample においてあります。
まず、普通にマイグレーションでマテリアライズドビューを作成します。
class CreateBookSaleSummaries < ActiveRecord::Migration
def up
connection.execute <<-SQL
CREATE MATERIALIZED VIEW book_sale_summaries AS
SELECT books.name as name,
books.id as id,
books.author_id as author_id,
sum((sales.number * books.price)) as total_sales
FROM sales
LEFT JOIN books ON sales.book_id = books.id
GROUP BY sales.book_id, books.name, books.author_id, books.id;
CREATE UNIQUE INDEX ON book_sale_summaries (id);
SQL
end
def down
connection.execute 'DROP MATERIALIZED VIEW IF EXISTS book_sale_summaries'
end
end
モデルは以下の通り。
# ## Schema Information
#
# Table name: `book_sale_summaries`
#
# ### Columns
#
# Name | Type | Attributes
# ------------------ | ------------------ | ---------------------------
# **`name`** | `string` |
# **`id`** | `integer` | `primary key`
# **`author_id`** | `integer` |
# **`total_sales`** | `integer` |
#
class BookSaleSummary < ActiveRecord::Base
belongs_to :author
self.primary_key = :id
def self.repopulate
connection.execute("REFRESH MATERIALIZED VIEW CONCURRENTLY #{table_name}")
end
def readonly?
true
end
end
ビューなので、一応 readonly?をtrueを返すようにして、更新出来ないようにしています(どっちにしろSQLレベルでエラーになるので、更新は出来ないので、無くても問題無いのですが)
あとは普通のテーブル同様、ActiveRecordのモデルとして操作出来ます。 association も問題無し。
[1] pry(main)> BookSaleSummary.count (4.5ms) SELECT COUNT(*) FROM "book_sale_summaries" => 10000 [2] pry(main)> BookSaleSummary.first BookSaleSummary Load (1.5ms) SELECT "book_sale_summaries".* FROM "book_sale_summaries" ORDER BY "book_sale_summaries"."id" ASC LIMIT 1 => #<BookSaleSummary:0x007f7c9116d6a8 name: "book_0", id: 1, author_id: 124, total_sales: 44800> [3] pry(main)> BookSaleSummary.first.author BookSaleSummary Load (0.9ms) SELECT "book_sale_summaries".* FROM "book_sale_summaries" ORDER BY "book_sale_summaries"."id" ASC LIMIT 1 Author Load (2.6ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT 1 [["id", 124]] => #<Author:0x007f7c90c0db90 id: 124, name: "author_123", created_at: Wed, 27 May 2015 22:53:34 UTC +00:00, updated_at: Wed, 27 May 2015 22:53:34 UTC +00:00> [4] pry(main)> BookSaleSummary.repopulate (506.2ms) REFRESH MATERIALIZED VIEW CONCURRENTLY book_sale_summaries => #<PG::Result:0x007f7c9033d8f8 status=PGRES_COMMAND_OK ntuples=0 nfields=0 cmd_tuples=0>
集計系データを表示するときには大分便利そうな気はします。PostgreSQL 9.4.0以降であれば、CONCURRENTLYオプションがあるので、ビューのリフレッシュも割と気軽に出来そう。