LoginSignup
1
0

(メモ)RailsでMySQLのstraight joinを利用する方法

Posted at

この記事は

  • RailsでMySQLの方言「STRAIGHT_JOIN」をある程度ちゃんと使う方法をまとめます

やりたかったこと

  • MySQLで実行計画を見ていると、期待と異なる順序でテーブル結合がされることがあります
  • STRAIGHT_JOINはそんな場合に強制的に結合順序を指定する句です
  • 左側のテーブルが常に右側のテーブルの前に読み取られるようになります
  • SQL的には普通のJOINの場所にSTRAIGHT_JOINと書けばいいだけです

しかし

  • ActiveRecordはSTRAIGHT_JOINに対応しておらず、普通に差し込むことはできません
  • 太古の昔に無理やり文字列的に差し込んで対応している人はいたりするのですが、複雑なSQLを組んでいる場合、そんな雑なやり方ではSQL構文エラーになり、うまくいきません

ちなみに

  • MySQL8系以降では、optimizer hintsという、OracleみたいなSQLコメントのshebangを書けて、オプティマイザへテーブル結合の優先度を提案することができます
  • そして、最近のRailsではoptimizer_hintsメソッドを使うことで、オフィシャルに利用することができます
  • MySQL5.7以前でもhintはあるのですが、テーブル結合に関する指定ができず、STRAIGHT_JOINを利用することになります
  • MySQL8以降でもhintはあくまでヒントなので、絶対に結合順序を指定したい場合はやはりSTRAIGHT_JOINを使う局面はあるかもしれません

ざっくりしたやり方

  • RailsのActiveRecordはArelというSQL生成ライブラリを内部で利用しています
  • 内部ライブラリというには汎用性が高すぎて超便利で、通常時でもちょっと凝ったサブクエリなどを使いたい場合は、Arelを使うといい感じに書けたりします
  • 今回はArelを拡張し、新たな句を追加することでSTRAIGHT_JOINに対応させます

具体的なやり方

Arelの拡張

  • initializerの中で新たなArelのNodeサブクラスを定義します
    • class StarightJoin < Arel::Nodes::Joinという名前にしました
    • クラス名が大事なので、実装は空で問題ないです
  • ArelがSQLを組み上げる時に構文木を巡回するvisitorに、STRAIGHT_JOIN対応の機能を追加します
    • visit_Arel_Nodes_StraightJoinというメソッド名にします。これは上記クラスの名前をスネークケースっぽく書いたものです(命名規則)
    • メソッドの中身はほぼINNER JOINのコピペです
initializer/straight_join_ext.rb
module Arel
  module Nodes
    class StraightJoin < Arel::Nodes::Join
    end
  end
end

module Arel
  module Visitors
    class MySQL < Arel::Visitors::ToSql
      def visit_Arel_Nodes_StraightJoin(o, collector)
        collector << "STRAIGHT_JOIN "
        collector = visit o.left, collector
        if o.right
          collector << " "
          visit(o.right, collector)
        else
          collector
        end
      end
    end
  end
end

使い方

  • こんな感じでArel::Nodes::StraightJoinを使って、通常のArelの書き方に則った結合を書くと、STRAIGHT_JOINが出力されます
  • TableA.joins(join_phrases)以降はただのActiveRecordですので、ここからチェーンで諸々条件だとかを繋げていくこともできます
table_a = ModelA.arel_table
table_b = ModelB.arel_table
join_phrases = table_a.join(table_b, Arel::Nodes::StraightJoin)
  .on(table_a[:model_b_id].eq(table_b[:id]))
  .join_sources
TableA.joins(join_phrases)

終わりに

  • このあたりの仕組みを使えば、SQL方言への対応はそんなに怖くないですね!
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0