日々雑記

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

少し調べたのでメモ。 なお、調べた時のバージョンは、PostgreSQLは9.3.5で、Railsは4.2.0.rc3。

因みに、Rails GuideにActive Record and PostgreSQLというページがあり、 それを参考にしながら書いてます。

マイグレーションファイル

Rails本体で配列型に対応しているので、特にgem等は必要無し。オプションに”array: true”を設定してあげればOK。

def change
  create_table :books do |t|
    t.string :title
    t.string :tags, array: true
    t.integer :ratings, array: true

    t.timestamps null: false
  end
  add_index :books, :tags, using: 'gin'
  add_index :books, :ratings, using: 'gin'
end

こんな感じで。 インデックスについての説明は後ほど。

データの作成

作成する際は、普通にRubyのArrayクラスを指定してあげればOK。

Book.create!(title: "test", tags: %w(fantasy), ratings: [5, 7])
# => Book id: 2, title: "test", tags: ["fantasy"], ratings: [5, 7], created_at: "2014-12-14 23:37:23", updated_at: "2014-12-14 23:37:23"

因みに、配列型はDB上では波括弧で括られたカンマ区切りの文字列として格納されています。

select ratings from books where id = 2;
# => {5,7}

検索

PostgreSQLでは配列用の演算子や関数が幾つか定義されているのですが、Rails本体では特にラッパーとかは提供されてないので、それらの演算子を使いたい場合は、普通にwhereメソッドで指定してあげればOK。

# "tags"に"fantasy"、"fiction"を含むデータを検索
Book.where("tags @> ARRAY[?]::varchar[]", ["fantasy", "fiction"])

# ratingsの長さが3以上のデータを検索
Book.where("array_length(ratings, 1) >= 3")

配列用の演算子、関数についての詳細はPostgreSQLのマニュアル参照。

因みに、surusのように、演算子をラップしたメソッドを提供しているgemもあったりします。この辺りはお好みで。

インデックスについて

B-treeではなく、GINインデックスを使用する事で配列の各要素にインデックスをはることが出来ます。

  add_index :books, :tags, using: 'gin'

こんな感じ。

因みに、GINとは、汎用転置インデックスの事で、インデックス対象の項目が複合型であり、インデックスにより取り扱われる問い合わせが複合型の項目内に存在する要素値に対して検索する必要がある場合を取り扱うために設計されているとの事です。こちらも詳細はPostgreSQLマニュアルご参照で。

一応ざっくりとGINインデックスある/無しで性能測定をしてみた。


                                           テーブル "public.array_tests"
           列            |              型               |                          修飾語
-------------------------+-------------------------------+----------------------------------------------------------
 id                      | integer                       | not null default nextval('array_tests_id_seq'::regclass)
 string_array            | character varying[]           |
 string_array_with_index | character varying[]           |
 integer_array           | integer[]                     |
 boolean_array           | boolean[]                     |
 date_array              | date[]                        |
 date_time_array         | timestamp without time zone[] |
 created_at              | timestamp without time zone   | not null
 updated_at              | timestamp without time zone   | not null
インデックス:
    "array_tests_pkey" PRIMARY KEY, btree (id)
    "index_array_tests_on_string_array_with_index" gin (string_array_with_index)

string_arrayと、string_array_with_indexという二つの文字列の配列型をつくり、片方にだけGINインデックスを設定し、まったく同じデータをいれみた。データ件数は36,000件程度。

で、検索してみた結果は以下の通り。


test=# explain analyze SELECT * FROM "array_tests" WHERE (string_array_with_index @> ARRAY['test']::varchar[]);
                                                                       QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on array_tests  (cost=113.40..692.12 rows=181 width=632) (actual time=4.762..4.763 rows=1 loops=1)
   Recheck Cond: (string_array_with_index @> '{test}'::character varying[])
   ->  Bitmap Index Scan on index_array_tests_on_string_array_with_index  (cost=0.00..113.36 rows=181 width=0) (actual time=4.752..4.752 rows=1 loops=1)
         Index Cond: (string_array_with_index @> '{test}'::character varying[])
 Total runtime: 4.819 ms


test=# explain analyze SELECT * FROM "array_tests" WHERE (string_array @> ARRAY['test']::varchar[]);
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Seq Scan on array_tests  (cost=0.00..3468.40 rows=181 width=632) (actual time=19.930..33.894 rows=1 loops=1)
   Filter: (string_array @> '{test}'::character varying[])
   Rows Removed by Filter: 36185
 Total runtime: 34.437 ms

当然インデックスありの方が速い。約7倍位。当然インデックスあった方が、データ作成時のコストは高くなるし、大分ざっくりしたデータではあるものの、良く検索するカラムであれば、とりあえずGINインデックスはっといた方が良さそう。

その他

先に、配列型はDB上では波括弧で括られたカンマ区切りの文字列として格納されている、と書いたのですが、当然それをRubyのArrayクラスに変換する際に、文字列のパース処理が必要になります。

で、Railsではそのパース処理を行う際、まず最初にpg_array_parserというライブラリが使用出来るかチェックして、使用出来る場合はそちらを、使用出来ない場合はRails内部で持っているパーサーを使用するようにしています。コードはこの辺り

で、何でそんな事をしているかといえば、pg_array_parserはC/Javaで作成しており、Rubyで記述されたパーサーよりも高速だから、との事。

これもざっくり性能測定を。測定のスクリプトはこんな感じ。

require 'benchmark'
require 'benchmark/ips'
require 'ruby-prof'
require 'pg_array_parser'
require 'active_record/connection_adapters/postgresql/array_parser'

class MyPgArrayParser
  include PgArrayParser
end

class RailsPgArrayParser
  include ActiveRecord::ConnectionAdapters::PostgreSQL::ArrayParser
end

test_string = '{some,strings that,"May have some ,\'s"}'

Benchmark.ips do |x|
  x.report('pg_array_parser') do
    parser = MyPgArrayParser.new
    parser.parse_pg_array test_string
  end
  x.report('rails_pg_array_parser') do
    parser = RailsPgArrayParser.new
    parser.parse_pg_array test_string
  end
  x.compare!
end

MyPgArrayParserがライブラリの方のpg_array_parserを使用したクラスで、RailsPgArrayParserがRails内部のパーサを使用してクラス。

それぞれでシンプルな配列データのパースを実施。


Calculating -------------------------------------
     pg_array_parser    62.295k i/100ms
rails_pg_array_parser
                         5.488k i/100ms
-------------------------------------------------
     pg_array_parser      1.288M (± 4.2%) i/s -      6.479M
rails_pg_array_parser
                         57.497k (± 4.8%) i/s -    290.864k

Comparison:
     pg_array_parser:  1287877.7 i/s
rails_pg_array_parser:    57496.8 i/s - 22.40x slower

そりゃネイティブで書かれた方が早いよね―、という感じでした。

なので、使える環境では極力pg_array_parserを使うのが良さそう。

pg_array_parserを使うには、Gemfileにpg_array_parserを記載するだけでOK。後はRails本体で勝手にrequireしてくれます。

まとめ

とりあえずGINインデックスと、pg_array_parserを使うのを忘れずに、という感じで。

まあ、そもそも配列型使うより普通に関連テーブル作成した方が良いケースが多いと思うので、そんなに使う機会は無さそうな気も…。

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