接続時のPostgreSQL型情報クエリを廃止し、静的OIDマッピングで代替
PostgreSQLへの接続時に毎回実行していたpg_typeへの問い合わせを廃止し、ビルトイン型のOIDマッピングを静的に保持することで、接続確立のコストを削減しました。未知の型に遭遇して初めてDBへの問い合わせが走る遅延ロード方式に切り替わります。
背景
PostgreSQLのビルトイン型は、サーバーバージョンに関わらず静的に割り当てられたOID(Object Identifier)を持ちます。これはPostgreSQLのソースコードで定義されており、boolのOIDは常に16、int4は常に23といった具合に固定されています。一方でユーザー定義型やhstoreなどの拡張型のOIDはサーバーインスタンス固有であり、DBのダンプ・リストアをまたいで保証されません。
これまでRailsはPostgreSQLへの接続確立のたびにpg_typeテーブルを照会し、型名とOIDのマッピングを構築していました。ビルトイン型にとってこのクエリは本質的に不要であり、接続ごとに同じ結果が返り続けていました。この問題を解消するため、ビルトイン型のOIDマッピングをRailsのソース内に静的に保持する方式へ移行しています。
技術的な変更
今回の変更の中心は、新たに追加されたOID::WellKnownモジュールと、そのデータファイルであるwell_known_values.rbです。well_known_values.rbはコード生成物であり、PostgreSQL 11以降の各メジャーバージョンのOIDマッピングを静的に定義しています。
接続初期化の流れが以下のように変わりました。
変更前:
def initialize_type_map(m = type_map)
# ...
load_additional_types
# ...
end
変更後:
def initialize_type_map(m = type_map)
# ...
OID::WellKnown.register_types(m, server_version: database_version)
# ...
end
OID::WellKnown.register_typesはサーバーバージョンに対応するマッピングを@mappings_cache(Concurrent::Map)から取得し、型エイリアス・ドメイン型・配列型・範囲型をtype_mapへ登録します。サーバーバージョンが既知のマッピングを超えた場合(FIRST_UNKNOWN_PG_VERSION以降)は最新の既知マッピングにフォールバックする設計です。
未知のOIDへの対処はcast_resultメソッドで行われます。クエリ結果のフィールドに未登録のOIDが含まれる場合、load_additional_typesを呼び出してDBへ問い合わせる遅延ロードが実行されます。
if missing_oids.any?
load_additional_types(missing_oids)
fields.size.times do |index|
ftype = field_types[index]
next if type_map.key?(ftype)
register_unknown_oid_type(ftype, fields[index])
end
end
マッピングデータの再生成はbundle exec rake db:postgresql:update_well_known_oidsタスクで行います。WellKnown::GeneratorがPostgreSQLのGitHubリポジトリからpg_type.datとpg_range.datをフェッチし、well_known_values.rbを上書き生成します。廃止済みや疑似型など登録すべきでないOIDはEXCLUDED_TYPE_OIDSで除外されており、smgr(OID: 210)・abstime(OID: 702)などのdeprecatedな型が対象です。
設計判断
Concurrent::Mapによるバージョン別キャッシュが採用されています。同一プロセス内で複数のサーバーバージョンへの接続が発生しうる環境を想定し、バージョンごとのマッピングをスレッドセーフにキャッシュします。配列型・配列型デリミタ・範囲型・ドメイン型の4つの静的マッピングはバージョン間で同一オブジェクトを共有しており(テストでassert_sameにより検証)、メモリ効率にも配慮しています。
遅延ロード戦略については、ユーザー定義型や拡張型(hstoreなど)は依然としてDBへの問い合わせが必要なため、完全に排除することはできません。そこで「接続時に必ず実行する」から「未知の型に出会ったときだけ実行する」へとタイミングをずらすことで、多くの接続で実際のクエリを不要にしています。@type_map_queriedフラグで二重実行を防ぐ制御も追加されています。
PostgreSQL 11未満のバージョンについては、11のマッピングにフォールバックする形で互換性を維持しています。これはActiveRecordが実質的にサポートしている最低バージョンを下回るケースへの安全網です。
まとめ
この変更は、接続時の定型クエリをRubyの静的データ構造で代替することで、PostgreSQL接続確立のオーバーヘッドを削減した実用的な最適化です。ユーザー定義型や拡張型の柔軟性は遅延ロードによって維持しつつ、ビルトイン型の処理を「クエリ不要」にした設計は、信頼できる事前知識を最大限に活用するアプローチとして参考になります。