少し調べたのでメモ。 なお、調べた時のバージョンは、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
を使うのを忘れずに、という感じで。
まあ、そもそも配列型使うより普通に関連テーブル作成した方が良いケースが多いと思うので、そんなに使う機会は無さそうな気も…。