この記事は
- 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方言への対応はそんなに怖くないですね!