Ruby on RailsによるPostgresqlテーブルの継承

Postgres継承への移行







それは何で、なぜ必要なのですか?







さまざまな種類の資料がある主要なニュース出版物があるとします。







マテリアルの各タイプには、 Topics::Article



Topics::Online



Topics::NewsItem



などの独自のモデルがあります。 タイトル、カバー、テキスト、著者などのフィールドの大部分が同じになります。 違いは、各タイプのトピックに固有のいくつかの特定のフィールドのみです。







したがって、それらを別々のテーブルに配置する必要はありません。 ほとんど完全に繰り返されるテーブルを作成することに抵抗があることに加えて、これにはいくつかの他の理由があります。 これらのタイプ、UNIONウォーターフォール、および接続モデルのポリモーフィズムのさまざまな組み合わせを持つ複雑なサンプルの必要性。







catの下で、Postgresql内で同様のモデルを整理し、その結果、テーブルを継承する移行の形での経験。 銀の弾丸で脚を撃つこともありますが、それがない場合です。







単一テーブルの継承



最初に頭に浮かぶのは、古典的な単一テーブルの継承です。 Railsでは、あるモデルを別のモデルから継承すると自動的にオンになります。







1つの共通トピックテーブルを作成し、それにタイプサービスフィールドを追加することにより、1つのテーブル内にすべてのクラスを格納できます。







 class Topic < ActiveRecord::Base end class Topics::Article < Topic end class Topics::NewsItem < Topic end ...
      
      





一般的なロジック(投稿資料など)はトピックに移動します。 継承されたクラスに固有。







このシンプルで実行中の回路にはスケーリングの問題があります。 15を超える種類のトピックがあり、それぞれに2〜10個の一意のフィールドがある場合はどうすればよいですか。







この時点で、純粋主義者は激しい歯痛のように眉をひそめます-線が常に15-20%以下で満たされているテーブルを想像してください。







STI + Jsonb



STI + Jsonb







ジョンブ



Postgresql c 9.4には、jsonbなどのフィールドを作成する機能があります。 これはどのように役立ちますか?

このタイプのデータフィールドをトピックに追加することにより、すべての追加フィールドをjsonキーに格納できます。







Railsで次のように接続できます。







 class Topics::Online < Topic store_accessor :data, :start_at, :live end
      
      





これで次のことができます。







 online = Topics::Online.new(live: true)
      
      





 online.live # => true
      
      





または、jsonに直接連絡してください:







 online['data']['live'] # => true
      
      





Jsonbの問題



成功の楽しみは、松葉杖の小道具によってすぐに覆い隠されます。







型変換



配列やオブジェクト(ハッシュ)などのエキゾチックに加えて、jsonbはすべてのフィールドにNumber、String、およびBooleanのみを使用することを提案しています。

他のタイプのフィールドの場合、追加のメソッドを記述する必要があります。 そして、砂糖を好むなら、これらの畑にも。







述語:







 def live? live == true end
      
      





DateTimeのより複雑なケース:







 def start_at return Time.zone.parse(super) if super.is_a?(String) end
      
      





ここでは、文字列を時間内に解析する必要があり、中断しないでください。 データを保存する段階で分解する必要があるからです。







入力値の検証



dbレベルには型の検証はありません;そのようなトピックを簡単かつ自然に保存できます:







 online.live = 'Elvis' online.start_at = 'Presley'
      
      





activerecord-typedstoreなどの既存のgemは、文字列の解析の問題を部分的に解決しますが、入力値のチェックにはまったく対処できません。 カスタム検証を使用して、アプリケーションからすべてを閉じる必要があります。







かさばるクエリ



ブールフィールド:







 scope live, -> { where(live: true) }
      
      





 scope live, -> { where("(params->>'live')::bool = ?", true) }
      
      





そして今、日付:







 scope :by_range, ->(start_date, end_date) { where(date: start_date..end_date) }
      
      





 scope :by_range, lambda { |start_date, end_date| where( "to_date(params->>'date', 'YYYY-MM-DD') BETWEEN ? AND ?", start_date, end_date ) }
      
      





一般的な怪物に加えて、 to_date



Postgresを強制的に使用するため、このリクエストの動作も遅くなります。







ユニック



Postgresはjsonbを使用したエントリに対して通常のDISTINCT(.uniq)を実行する方法を知りません。これを行う必要があります。







 .select('DISTINCT ON (tags.id) tags.*')
      
      





デフォルトなし



before_initialize



では、通常のnull: false, default: false



代わりに、異なる構成を使用する必要があります。







コミュニケーションズ



レールhas_many



を使用すると、 belongs_to



は機能しません。 あなた自身の何かを書く必要があります。







この時点で、jsonbは黒いマークを取得し、一andのパンからトロリーに到達しませんでした。







Postgres継承への移行



テーブルの継承は長い間Postgresに登場し(ほとんどの場合、バージョンを更新する必要はありません)、クラス継承の概念は近いです。

クラスだけでなく、テーブルだけでなく、RubyではなくPostgresで。







トピックテーブルはまだありますが、このテーブル自体の追加フィールドセットではなく、各クラスに固有のフィールドのみを含む追加テーブルを使用してテーブルを展開しています。







例を示す最も簡単な方法:







 CREATE TABLE topics ( headline text, author_id int ); CREATE TABLE topics_onlines ( status char(2) ) INHERITS (topics);
      
      





topics_onlines



を作成することで、3つのフィールドすべてを含む通常のテーブルのように作業できます。







 class Topics::Online < Topic # headline, author_id, status end
      
      





シンプルで美しく、コードを大量に書き換える必要はありません。







Postgresの継承+ Rails



 SELECT c.tableoid, c.headline, c.author_id FROM topics c
      
      





  tableoid | headline | author_id ----------+-----------+---------- 139793 | Las Vegas | 2174 139793 | Mariposa | 1953 139798 | Madison | 845
      
      





親テーブルと子テーブルは、tableoidを介してPostgres内でリンクされます。 Explainではtableoid結合は表示されません。すべてPostgres内で機能します。







アプリケーションから、 topics_onlines



は継承のない最も一般的なテーブルのようになり、 topics



すべてのフィールドとtopics_onlines



オンライン固有のフィールドがtopics_onlines



ます。







そして、これは、Railの側から、テーブルを作成するマイグレーションを記述するだけでよいことを意味します。

そして、そのテーブル継承はどのフレームワークでも使用できます。







STIからPGIへの移行



このすべての喜びを活用するには、移行を作成する必要があります。







まず、RailsでのSQL移行用の標準ラッパーが必要です。







 class CreateInheritanceTablesForTopics < ActiveRecord::Migration def change reversible do |dir| dir.up do ... end end end
      
      





次に、コードが省略記号に挿入されます。 コードシートをすぐにスローしないように、移行をバッチで示します。







すべてのトピックテーブルで一意性をチェックするトリガー



作成しますが、これまではPostgresqlでトリガーを使用しません。







 CREATE OR REPLACE FUNCTION check_for_topic_dups() RETURNS trigger AS $func$ BEGIN PERFORM 1 FROM topics where NEW.id=id; IF FOUND THEN RAISE unique_violation USING MESSAGE = 'Duplicate ID: ' || NEW.id; RETURN NULL; END IF; RETURN NEW; END; $func$ LANGUAGE plpgsql;
      
      





そのIDを持つトピックが既に存在する場合、トリガーはエラーをスローします。 これは何かがうまくいかない場合の保険です。







最も重要なPGIの制限は、すべての子テーブルにインデックスと親テーブルの制限がないことです。 つまり、この点で、すべてが異なる物理テーブルのように感じられます。







この条件では、さまざまな種類のトピックに対してIDを繰り返すことはできないため、このトリガーが追加されました。 これはオプションであり、保険として必要です。







テーブル作成



 Topic.descendants.each do |topic_type| sql = <<-SQL CREATE TABLE #{topic_type.pgi_table_name} ( CHECK (type='#{topic_type}') ) INHERITS (topics); CREATE RULE redirect_insert_to_#{topic_type.table_name} AS ON INSERT TO topics WHERE (type='#{topic_type}') DO INSTEAD INSERT INTO #{topic_type.table_name} VALUES (NEW.*); CREATE TRIGGER check_uniquiness_#{topic_type.table_name} BEFORE INSERT ON #{topic_type.table_name} FOR EACH ROW EXECUTE PROCEDURE check_for_topic_dups(); SQL execute(sql) add_index topic_type.table_name, :id end
      
      







もちろん、移行は厳密に制限され、すべてのチェックはRails自体に取り込まれます。







継承されたテーブルにネイティブフィールドを追加する



標準の移行を使用して、新しいテーブルにフィールドを追加できます。







 Class PopulateTopicsTablesWithFields < ActiveRecord::Migration def change add_column :topics_onlines, :start_at, :datetime add_column :topics_news, :main, :boolean, null: false, default: false end end
      
      





STIを完全に取り除く準備ができていない場合は、必要なテーブルがトピッククラスに記述されています。







 class Topics::Online < Topic self.table_name = :topics_online end
      
      





スキーマタイプをsqlに変更するだけです。







 # config/application.rb config.active_record.schema_format = :sql
      
      





これで完了です。







速度-PGI vs jsonb



速度-PGI vs jsonb







パフォーマンスを評価するための最後のステップは興味深いでしょう。 これはすべて、開発を容易にするために考案されたものであるため、PGIの速度のテストに費やす時間はあまりありませんが、いくつかの結論を導き出すことができます。







移行後、PGIとjsonbを使用した古いバージョンの2つのバージョンのアプリケーションが作成されました。

各データベースに5_000_000



トピック。







すべてのトピックの数



最も合成的な例:







PGI:







 Topics::Topic.count (8591.6ms) SELECT COUNT(*) FROM "topics" => 5316226
      
      





ジョンブ:







 Topics::Topic.count (8580.1ms) SELECT COUNT(*) FROM "topics" => 5316226
      
      





奇妙な数に驚かないでください、ssd上の場所がなくなるまでトピックが作成されました。







1つのタイプのトピックの数



PGI:







 Gazeta::Topics::Sport::Online.count * (219.5ms) SELECT COUNT(*) FROM "topics_sport_onlines" WHERE "topics_sport_onlines"."type" IN ('Gazeta::Topics::Sport::Online') => 1000000
      
      





ジョンブ:







 Gazeta::Topics::Sport::Online.count * (419.0ms) SELECT COUNT(*) FROM "topics" WHERE "topics"."type" IN ('Gazeta::Topics::Sport::Online') => 1000000
      
      





ブールフィールドによるリクエスト



インデックスは選択性が高いため使用されません。







PGI:







 Gazeta::Topics::Sport::Online.megauho.explain * Gazeta::Topics::Sport::Online Load (1376.2ms) SELECT "topics_sport_onlines".* FROM "topics_sport_onlines" WHERE "topics_sport_onlines"."type" IN ('Gazeta::Topics::Sport::Online') AND "topics_sport_onlines"."megauho" = $1 [["megauho", "t"]]
      
      





ジョンブ:







 Gazeta::Topics::Sport::Online.megauho.explain * Gazeta::Topics::Sport::Online Load (5819.6ms) SELECT "topics".* FROM "topics" WHERE "topics"."type" IN ('Gazeta::Topics::Sport::Online') AND ((topics.params->>'megauho')::bool = 't')
      
      





違いは大きい。







制限付きのブール値フィールドのリクエスト



少なくともすでに実世界と交差しています。







PGI:







 Gazeta::Topics::Sport::Online.megauho.limit(1000).explain * Gazeta::Topics::Sport::Online Load (9.1ms) SELECT "topics_sport_onlines".* FROM "topics_sport_onlines" WHERE "topics_sport_onlines"."type" IN ('Gazeta::Topics::Sport::Online') AND "topics_sport_onlines"."megauho" = $1 LIMIT 1000 [["megauho", "t"]]
      
      





ジョンブ:







 Gazeta::Topics::Sport::Online.megauho.limit(1000).explain * Gazeta::Topics::Sport::Online Load (23.7ms) SELECT "topics".* FROM "topics" WHERE "topics"."type" IN ('Gazeta::Topics::Sport::Online') AND ((topics.params->>'megauho')::bool = 't') LIMIT 1000
      
      





違いがあります。










低選択性クエリのPGI



インデックス検索は、100万件のインデックススキャンから123レコードを返します。







PGI:







 Gazeta::Topics::Sport::Online.megauho.megauho_by_date('2015-12-26').explain * Gazeta::Topics::Sport::Online Load (6.0ms) SELECT "topics_sport_onlines".* FROM "topics_sport_onlines" WHERE "topics_sport_onlines"."type" IN ('Gazeta::Topics::Sport::Online') AND "topics_sport_onlines"."megauho" = $1 AND (topics_sport_onlines.date = '2015-12-26') [["megauho", "t"]] QUERY PLAN ---------- * Index Scan using index_type_megauho_date on topics_sport_onlines (cost=0.42..42.12 rows=20 width=682) Index Cond: (((type)::text = 'Gazeta::Topics::Sport::Online'::text) AND (megauho = true) AND ((date)::text = '2015-12-26'::text)) Filter: megauho (3 rows)
      
      





ジョンブ:







 Gazeta::Topics::Sport::Online.megauho.megauho_by_date('2015-12-26').explain * Gazeta::Topics::Sport::Online Load (7.7ms) SELECT "topics".* FROM "topics" WHERE "topics"."type" IN ('Gazeta::Topics::Sport::Online') AND ((topics.params->>'megauho')::bool = 't') AND (topics.params->>'date' = '2015-12-26') QUERY PLAN ---------- * Index Scan using index_type_megauho_date on topics (cost=0.56..217.61 rows=27 width=948) Index Cond: (((type)::text = 'Gazeta::Topics::Sport::Online'::text) AND ((params ->> 'date'::text) = '2015-12-26'::text)) Filter: ((params ->> 'megauho'::text))::boolean (3 rows)
      
      







jsonbを正しく調理する



モデルを完全に拡張するタスクでは、jsonbは非常に有毒です。 もちろん、PGIにも制限がありますが、私たちのタスクについては、移行中に一度に克服されました。







それでも、jsonbは外部ソースからの緩やかに構造化されたデータに役立ちます。 これらのフィールドはstore_accessor



で定義する必要さえありません。そのまま保存するだけで、別のBuilderクラスが有用なものを収集します。







私たちにとっては、外部APIから取得したスポーツ放送がそのようなデータになりました。








All Articles