0

The following line in a list of stories an article belongs to—on an edit article page, allowing me to assign the article to stories—works but produces an n+1 query (I think).

The bit that is causing the problem is @article.stories.include?(s), which checks to see if an article already belongs to a story:

<%= check_box_tag "article[story_ids][]", s.id, @article.stories.include?(s) %>

Log says Rails is doing 50+ queries (all stories) because of that one bit; article_id = 266 in the example:

Story Exists (0.4ms)  SELECT  1 AS one FROM "stories" INNER JOIN "articles_stories" ON "stories"."id" = "articles_stories"."story_id" WHERE "articles_stories"."article_id" = $1 AND "stories"."id" = $2 LIMIT 1  [["article_id", 266], ["id", 6]]
Story Exists (0.3ms)  SELECT  1 AS one FROM "stories" INNER JOIN "articles_stories" ON "stories"."id" = "articles_stories"."story_id" WHERE "articles_stories"."article_id" = $1 AND "stories"."id" = $2 LIMIT 1  [["article_id", 266], ["id", 51]]
Story Exists (0.3ms)  SELECT  1 AS one FROM "stories" INNER JOIN "articles_stories" ON "stories"."id" = "articles_stories"."story_id" WHERE "articles_stories"."article_id" = $1 AND "stories"."id" = $2 LIMIT 1  [["article_id", 266], ["id", 41]]
Etc., etc.

The actual number of stories article_id = 266 belongs to is…2:

 select * from articles_stories where article_id = 266;

 article_id | story_id 
 ------------+----------
    266 |       35
    266 |        5

 (2 rows)

How do I fix it?

UPDATE: DB, whole chunk & controller

Article.rb:

has_and_belongs_to_many :stories

Story.rb:

has_and_belongs_to_many :articles

And the whole chunk of code the check_box_tag is in—prints a list of stories by category you can assign to an article—is:

<% @stories.group_by(&:category).sort_by{|c, stories| c.category}.each do |c, stories| %>
  <div class="admin-stories-in-categories">
    <strong><%= c.category unless c.nil? %></strong>
      <ul>
        <% for s in stories %>
          <li>
            <%= check_box_tag "article[story_ids][]", s.id, @article.stories.include?(s) %>
            <%= s.story %>
          </li>
        <% end %>
      </ul>
  </div>
<% end %>

In articles_controller.rb:

@stories = Story.all.includes(:category).order(:story)

ANSWER:

AbM's answer below makes the N+1 queries go away and instead returns a single query:

Returns `(0.8ms)  SELECT "stories"."id" FROM "stories" INNER JOIN "articles_stories" ON "stories"."id" = "articles_stories"."story_id" WHERE "articles_stories"."article_id" = $1  [["article_id", 266]]`
Matthew Bennett
  • 303
  • 2
  • 12

2 Answers2

1

We need your database schema and the query you're using from your controller (if any) — but you'll likely just have to do something like Article.includes(:stories).find(params[:id])

I'd recommend plugging in something like Bullet into your development stack.

https://github.com/flyerhzm/bullet

Josh Brody
  • 5,153
  • 1
  • 14
  • 25
1

Thanks because the include? method on a active record relation does an EXIST operation in sql if the relation is not loaded as shown here

Try storing the article stories in a variable:

<% article_stories_ids = @article.stories.pluck(:id) %>
<% @stories.group_by(&:category).sort_by{|c, stories| c.category}.each do |c, stories| %>
  <div class="admin-stories-in-categories">
    <strong><%= c.category unless c.nil? %></strong>
      <ul>
        <% stories.each do |s| %>
          <li>
            <%= check_box_tag "article[story_ids][]", s.id, article_stories_ids.include?(s.id) %>
            <%= s.story %>
          </li>
        <% end %>
      </ul>
  </div>
<% end %>
AbM
  • 7,326
  • 2
  • 25
  • 28