DIGGLE開発者ブログ

予実管理クラウドDIGGLEのプロダクトチームが、技術や製品開発について発信します

Rails+ApartmentにPostgreSQLのプロシージャを導入して多軸分析の集計速度を向上させた話 その3~Rails + PL/pgSQL編~

こんにちは。DIGGLEエンジニアのzakkyです。最近、長女、次女&三女(双子)がそれぞれ高校、中学校へ入学しました。可愛かったです。

いよいよレポート(多軸分析)の集計処理部分のパフォーマンス向上施策の第3弾となります。

前回までの記事の紹介

diggle.engineer diggle.engineer

今回のお題

今回はRails+ApartmentでRails Wayに乗っかりつつPL/pgSQLを導入した際のあれやこれやをご紹介します。plv8については、今回以上に大変だったので別記事として次回お話しする予定となっています。

書かないこと

以下の内容は含みませんので予めご了承ください

  • Rails、Apartmentの導入などの基礎的な部分
  • PostgreSQLのプロシージャに関する説明

どれくらい速くなったのか

前回も記載しましたが、以下が今回改善した集計処理部分のLatency推移となります。 リリース後には低下していることが見て取れるかと思います。

大体3倍速程度には改善することができました。

赤線の辺りで今回の改善版のリリースを行っています

Railsでプロシージャを呼び出すために具体的にどうしたのか

もう3回目ですので、早速本題へ行きます。 Railsからプロシージャを呼び出すためには、本当に色々なハードルがありました。

※プロシージャの導入に至った詳しい経緯を知りたい方は前回までの記事を参照いただけますと幸いです

プロシージャをRails Wayで管理する

大前提となります。今後のメンテナンス性を考え、あくまでRails Wayに乗っかった形でプロシージャを管理していく方法を考えます。

最終的な変更内容については今回やった内容のまとめに記載していますので、結果だけ知りたい方は最下部を参照ください。

プロシージャをRailsの管理下に置く

Railsガイドに記載されている通り、ダンプフォーマットをsqlへ変更することで解決できます。

railsguides.jp

publicテナントだけをダンプ対象にする

Apartmentを使っていてダンプフォーマットをsqlへ変更した場合、db:migrateを実行すると、structure.sqlに全テナント分のテーブルがダンプされてしまう問題が発生します。

こちらについてはダンプする対象をpublicにすることで回避できます。

db:resetを動くように修正する

schema_search_pathが指定されていてダンプフォーマットをsqlへ変更している場合、以下のissueにある通りdb:resetが動きません。解決策としては、issue中に記載のある通り、structure_dump_flagsを指定することで回避できます。

github.com

Apartmentの新規テナント作成時にプロシージャを適用する

以下のREADMEに記載されている通り、use_schemasとuse_sqlを適用することで新規テナント作成時にプロシージャが適用されます。

github.com

余談

上記READMEを読んでいると気になる一文があります。

this option doesn't use db/structure.sql, it creates SQL dump by executing pg_dump

ソースコードを追いかけると、確かにstructure.sqlを使わずに、pg_dumpをApartmentの中で実行しています。(次回以降にお話ししますが、ここの辺りで問題が発生してapartmentに対してPRを出すことになりました)

db:migrateでプロシージャを登録する

ここでようやくプロシージャをRailsで登録できるようになります。

シンプルに以下で実行可能です。

  def change
    execute <<~SQL
      CREATE PROCEDURE sample_procedure()
      AS $function$

      BEGIN
      -- 処理を書く
      END;

      $function$ LANGUAGE plpgsql;      
    SQL
  end

プロシージャの管理方針を考える

今回作成するプロシージャは弊社の根幹のロジックであり、今後のレポート拡充などやパフォーマンス改善で更に手を加えることが既に見えています。そして、改修の都度migrationを書いていては、どこにどのプロシージャがあるかを確認できず、管理が非常に煩雑になる未来が見えます。

上記などを考慮した結果、弊社ではプロシージャのソースコードを管理しやすいようにプロシージャ用のディレクトリを作成して、その中でプロシージャを管理するようにしました。

このようにmigrationを実行するイメージです。

def change
  execute File.read('db/procedure/sample_procedure.sql')
end

ただ、そうすると、migrationを書き忘れたりlocalのdb:migrateの実行タイミング次第では登録されるプロシージャの内容が個々人で変わったりなど、色々な危険があるのではないかと気付き、最終的には、db:migrateを実行する度にプロシージャの最新ソースを登録するようなrakeを別途作成することとしました。

lib/tasks/procedure.rake
def install_all_procedures
  Dir.glob(Rails.root.join('db/procedure/*.*')).each do |file|
    ActiveRecord::Base.connection.execute File.read(file)
  end
end

task 'db:procedure' => :environment do
  puts 'Re-install procedures..'
  install_all_procedures

  Apartment::Tenant.each do
    puts "At Tenant: #{Apartment::Tenant.current}"
    install_all_procedures
  end

  if ActiveRecord::Base.dump_schema_after_migration
    Rake::Task['db:schema:dump'].invoke
  end
end

Rake::Task['apartment:migrate'].enhance do
  Rake::Task['db:procedure'].invoke
end
rakeの中身の説明

簡単にrakeファイルの仕様を解説をします。

まず、Apartmentの仕様として、db:migrateを実行すると内部でapartment:migrateが実行されます。 今回の場合、apartmentのmigrationが終わった後にプロシージャの登録を行いたいので、Rake::Task['apartment:migrate'].enhanceとしています。

install_all_procedures周りの処理は全テナント(publicスキーマ含む)に対してdb/procedure/配下の全ファイルを実行していくだけなので割愛するとして、Rake::Task['db:schema:dump'].invokeについては、上記のプロシージャ登録をstructure.sqlの取得タイミングの後に実行しているため、再度dumpしてあげる必要があり、このような記述となっています。

結果として、db:migrateを実行するとdb/procedure/配下の全ファイルが常に実行されるようなrakeファイルとなっています。

つまり、db:migrateを行うたびに毎回procedure配下のファイルが実行されてしまうため、各プロシージャの先頭ではDROP-CREATEの形での記述をしてあげる必要があります。

DROP FUNCTION IF EXISTS sample_function;

CREATE FUNCTION sample_function()
--以下略

登録したプロシージャを呼び出す

プロシージャの呼び出し自体は非常に簡単です。

# procedure
ActiveRecord::Base.connection.execute('CALL sample_procedure()')

# function
ActiveRecord::Base.connection.execute('SELECT sample_function()')

※パラメータを渡す際にはsanitize_sqlなどを行う必要がありますので、ご注意ください。

今回やった内容のまとめ

以下のような形となりました。

application.rb
  class Application < Rails::Application
    config.active_record.schema_format = :sql
    config.active_record.dump_schemas = 'public'
    # MEMO: db:resetが動くように調整
    # NOTE: https://github.com/rails/rails/issues/38695
    ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = ['--clean', '--if-exists']
  end
apartment.rb
Apartment.configure do |config|
  config.use_schemas = true
  config.use_sql = true
end

We're hiring!

DIGGLEでは、必要に応じてライブラリのソースを読み解いて問題解決に挑む開発メンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

herp.careers

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net