[Rails] has_many: through関連付けで発生する2重INNER JOIN問題
2025/8/31
rails
Railsのアソシエーション機能は、モデル間のリレーションシップを簡潔に定義でき、コードの可読性と保守性を大幅に向上させるが、生成されるSQLクエリを正確に理解していないと、意図しないパフォーマンス問題を引き起こすことがある。
今回は、実際の業務で遭遇したhas_many: through
関連付けと明示的なjoins
メソッドの併用による2重INNER JOIN問題について解説する。
has_many: through
の基本
has_many: through
関連付けは、中間テーブルを介した多対多のリレーションシップを表現する際に使用される。例として、記事(Article)とタグ(Tag)が関連づけられているケースを見てみる。
class Article < ApplicationRecord
has_many :article_tags, dependent: :destroy
has_many :tags, through: :article_tags
end
class Tag < ApplicationRecord
has_many :article_tags, dependent: :destroy
has_many :articles, through: :article_tags
end
class ArticleTag < ApplicationRecord
belongs_to :article
belongs_to :tag
end
この定義により、中間テーブルであるArticleTagを意識することなく、記事からタグ・タグから記事へアクセスできる。
# articleに紐づく全てのtag
article.tags
# tagに紐づく全てのarticle
tag.articles
実行されるクエリの確認
article.tags
を実行すると(article = Article.find(1)
の場合)、以下のようなクエリが発行される。
SELECT "tags".*
FROM "tags"
INNER JOIN "article_tags"
ON "tags"."id" = "article_tags"."tag_id"
WHERE "article_tags"."article_id" = 1
遭遇した2重INNER JOIN問題
問題の概要
has_many: through
関連付けが定義されているモデルに対して、さらに明示的にjoins
メソッドを使用していたケースがあった。これにより、同じテーブルに対して重複したINNER JOINが発生し、取得レコード数が意図せず増大する問題が生じていた。
問題のあるコード例
class Article < ApplicationRecord
has_many :article_tags, dependent: :destroy
has_many :tags, through: :article_tags
def active_tags
tags.joins(:article_tags)
.where(article_tags: { active: true })
end
end
解決方法
joinsの指定を削除してあげれば2重INNER JOINは解消される。
class Article < ApplicationRecord
has_many :article_tags, dependent: :destroy
has_many :tags, through: :article_tags
def active_tags
tags.where(article_tags: { active: true })
end
end