← 記事一覧に戻る

[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