酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

Scenic RailsでMaterialized Viewを使う

RailsでPostgreSQLのビュー(View)やマテリアライズド・ビュー(Materialized View)を使うためのGem「thoughtbot/scenic」の紹介です。 thoughtbot謹製なので、安心して使えそうなのも魅力です!


PostgreSQLのViewとMaterialized View

PostgreSQLのViewとMaterialized Viewの大まかかな説明は次の通りです。

View(ビュー)について

  • ViewとはSQLで構成された実態のな仮想的なテーブルのこと。Viewの作成時にSQLを定義する
  • Viewを呼び出すとSQLが即時実行される
  • 最新の情報を常に取得できるが、SQLが複雑だったり、レコードが多いと負荷がかかる

Materialized View(マテリアライズド・ビュー)について

  • 実態を持ったViewのことである。作成するときにSQLの実行結果のテーブルを作る
  • 最新の情報を反映するためにはリフレッシュ処理を行う必要がある
  • SQLが複雑だったり、レコードが多い処理の負荷を下げるのに有効なアプローチである

PostgreSQLのMaterialized Viewの機能

RailsでPostgreSQLのマテリアライズドビューを使う | 日々雑記』にある通り、 PosgreSQLにはリフレッシュ時にSELECTコマンドを発行した時にロックされて応答が遅延しないように、CONCURRENTLY というパラメータをつけるといいようです!

1
REFRESH MATERIALIZED VIEW CONCURRENTLY materialized_view;

Gemのインストール

ではいよいよ、Viewを扱いやすくするGem「thoughtbot/scenic」の紹介です。

Gemfileに以下を追加して、bundle installを実行してください。

1
2
3
4
5
# View & Materialized View (GitHubじゃないとちょっと古い気がします)
gem 'scenic', git: 'git@github.com:thoughtbot/scenic.git'

# Fake Data(サンプルデータを作るために使います)
gem 'faker'

Viewを作成するためのModelを作成

まずは、Viewを作成するための元となるテーブルをScaffoldで作成します。

1
2
3
4
5
6
# 新しい Rails プロジェクトを作成
$ rails new scenic_sample
$ cd scenic_sample

# scaffoldで一通り作成
$ rails g scaffold book title:string author:string price:float

次にサンプルデータを作ります。

1
2
3
4
5
6
7
8
9
10
# db/seeds.rb
require 'faker'

1000.times do |i|
  Book.create(
      title: Faker::Name.title,
      author: Faker::Name.name,
      price: Faker::Commerce.price
  )
end

ではテーブルを作って、テストデータを投入します。

1
2
3
4
5
# テーブルを作成
$ bundle exec rake db:migrate

# テストデータを投入
$ bundle exec rake db:seed

これでサンプルデータが作成されました。

いよいよマテリアライズド・ビューを作成

次にマテリアライズド・ビューを作成します。

今回は$50以上のBookをExpensiveBookとして、それを抽出するテーブルにします。

1
2
3
4
5
6
7
8
9
10
# ExpensiveBookの作成
$ rails generate scenic:model expensive_book --materialized
      invoke  active_record
      create    app/models/expensive_book.rb
      invoke    rspec
      create      spec/models/expensive_book_spec.rb
      invoke      factory_girl
      create        spec/factories/expensive_books.rb
      create  db/views/expensive_books_v01.sql
      create  db/migrate/20160106124232_update_expensive_books.rb

マテリアライズド・ビューのSQL

そんなマテリアライズド・ビューのSQLがこちら。

1
2
3
4
5
6
7
# db/views/expensive_books_v01.sql
SELECT
  *
FROM
  books
WHERE
  price >= 50.0

あとはリフレッシュ時にCONCURRENTLYを使うために、UNIQ INEXを追加します。

1
2
3
4
5
6
7
8
# db/migrate/20160106123247_create_expensive_books.rb
class CreateExpensiveBooks < ActiveRecord::Migration
  def change
    create_view :expensive_books, materialized: true
    # 追加
    add_index :expensive_books, :id, unique: true
  end
end

ではマテリアライズド・ビューを作成します。

1
2
3
4
5
$ bundle exec rake db:migrate
== 20160106123247 CreateExpensiveBooks: migrating =============================
-- create_view(:expensive_books)
   -> 0.0272s
== 20160106123247 CreateExpensiveBooks: migrated (0.0272s) ====================

こんな感じのSQLが実行されたようです。

1
2
3
4
5
6
7
8
CREATE OR REPLACE VIEW public.expensive_books AS SELECT books.id,
    books.title,
    books.author,
    books.price,
    books.created_at,
    books.updated_at
   FROM books
  WHERE (books.price >= (50.0)::double precision);

SQLを変更する場合は、rails g scenic:view expensive_book とすれば変更用のSQLのmigrationファイル生成されます。

マテリアライズド・ビューのModelファイル

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# app/models/expensive_book.rb
class ExpensiveBook < ActiveRecord::Base
  # find_by!とかを使う場合はprimary_keyが必要なので
  self.primary_key = :id

  # このメソッドを実行するとマテリアライズド・ビューが更新される
  def self.refresh
    Scenic.database.refresh_materialized_view(table_name, concurrently: true)
  end

  private
  # マテリアライズド・ビューは書き込みができないので読み込み専用のViewとして作成
  def readonly?
    true
  end
end

一点GemのREADMEに書いていた注意点として、リフレッシュ時のパラメータにconcurrently: trueとした場合 はlockがかからないけど、PostgreSQL 9.4 以上 && uniq index が必須なので注意してください。

Special Thanks

おすすめの書籍