PostgreSQL の Array を使うと ActiveRecord の初期化が重くなる場合がある事を学んだ


Rails 4.2.6, Ruby 2.3.1, PostgreSQL 9.5.2 の話

1MBくらいの要素を追加したら(!?)、SELECT 後のインスタンス初期化で非常に待たされました。
試しにカラムの型を jsonb に変更してみたところ、多少はマシになりました。

ActiveRecord のコードを探ってみると、デフォルトでは Ruby で書かれたパーサによって PostgreSQL の Array を Ruby の Array にキャストしている事が分かりました。

lib/active_record/connection_adapters/postgresql/oid/array.rb
module ActiveRecord
  module ConnectionAdapters
    module PostgreSQL
      module OID # :nodoc:
        class Array < Type::Value # :nodoc:
          include Type::Mutable

          # Loads pg_array_parser if available. String parsing can be
          # performed quicker by a native extension, which will not create
          # a large amount of Ruby objects that will need to be garbage
          # collected. pg_array_parser has a C and Java extension
          begin
            require 'pg_array_parser'
            include PgArrayParser
          rescue LoadError
            require 'active_record/connection_adapters/postgresql/array_parser'
            include PostgreSQL::ArrayParser
          end

ということで、PostgreSQL の Array を使う場合は DockYard/pg_array_parser を使っておくと良さそうです。
(DockYard/pg_array_parser のコードは読んでませんし、2013年でコミットは止まっている様です。)

一方その頃 Rails 5.0.0 は

pg が持っている機能を使っている様です。

lib/active_record/connection_adapters/postgresql/oid/array.rb
module ActiveRecord
  module ConnectionAdapters
    module PostgreSQL
      module OID # :nodoc:
        class Array < Type::Value # :nodoc:
          include Type::Helpers::Mutable

          attr_reader :subtype, :delimiter
          delegate :type, :user_input_in_time_zone, :limit, to: :subtype

          def initialize(subtype, delimiter = ',')
            @subtype = subtype
            @delimiter = delimiter

            @pg_encoder = PG::TextEncoder::Array.new name: "#{type}[]", delimiter: delimiter
            @pg_decoder = PG::TextDecoder::Array.new name: "#{type}[]", delimiter: delimiter
          end