日々雑記

旅行の事ばっかり。神社成分多め。
Recent Tweets @

少し調べたのでメモ。 なお、調べた時のバージョンは、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つある場合にのみ使用可能との事です。

Railsから使う

こっからが本題。 と言っても、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オプションがあるので、ビューのリフレッシュも割と気軽に出来そう。

参考資料

  1. reminiscentorbitalatm09tdからリブログしました
  2. y-yagiの投稿です