0

In order to make a single DB query, I am eager loading Posts along with their translation data (using Rails 6 and Mobility (*)), but instead it yields 2 SQL queries:

# app/models/post.rb
class Post < ApplicationRecord
  extend Mobility
  translates :title, :description, backend: :table
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationRecord
  def index
    @posts = Post.eager_load(:translations).limit(3)
  end
end
<%# app/views/posts/index.html.erb %>
<% @posts.each do |post| %>
  <h1><%= post.title %></h1>
  <div><%= post.description %></div>
<% end %>

Result:

  • first, all Post IDs are selected
  • then all attributes of posts with those IDs are returned, using WHERE IN
 SELECT DISTINCT "posts"."id" FROM "posts"
    LEFT OUTER JOIN "post_translations" 
    ON "post_translations"."post_id" = "posts"."id" LIMIT $1  [["LIMIT", 3]]

  SELECT "posts"."id" AS t0_r0, "posts"."created_at" AS t0_r1, "posts"."updated_at" AS t0_r2, "post_translations"."id" AS t1_r0, "post_translations"."locale" AS t1_r1, "post_translations"."created_at" AS t1_r2, "post_translations"."updated_at" AS t1_r3, "post_translations"."title" AS t1_r4, "post_translations"."description" AS t1_r5, "post_translations"."post_id" AS t1_r6 FROM "posts" 
    LEFT OUTER JOIN "post_translations" 
    ON "post_translations"."post_id" = "posts"."id" 
    WHERE "posts"."id" IN ($1, $2, $3)  [["id", "00060a7d-b846-5fc5-a372-1fc3462c695c"], ["id", "008db504-6fb4-5e90-bdca-4293ebe6d920"], ["id", "034944c1-4067-5ae5-89aa-4777ef14d66b"]]

How can this double SQL statement with in-memory of IDs be avoided?


(*) A note mon Mobility

The Mobility documentation has examples yielding a single SQL statement, but as pointed out by Chris Salzberg, its query API is not used at all in this example so should not be the culprit. To try to demonstrate that the issue might not be related to Mobility but Active Record itself, below is a somewhat equivalent code stripped off of Mobility, which shows the same double-querying issue (NB: this is just for demonstration purposes, as I do want to keep using Mobility):

class Post < ApplicationRecord
  has_many :translations, ->{ where(locale: I18n.locale) }

  %i(title description).each do |attr|
    define_method(attr) do
      translations.first.send(attr)
    end
  end

  class Translation < ApplicationRecord; end
end
<%# app/views/posts/index.html.erb %>
<% Post.eager_load(:translations).limit(3).each do |post| %>
  <h1><%= post.title %></h1>
  <div><%= post.description %></div>
<% end %>
  • 1
    What are you trying to achieve out of that particular query? That would help me to understand and maybe try providing suggestions. – Naveen Honest Raj Jul 08 '21 at 12:49
  • 1
    AFAICT this is not a Mobility question, it's an ActiveRecord question. You have a relation between two tables, and you're not using Mobility's query plugin here, so it's just straight AR query stuff. I would look into `eager_load` and what it does. – Chris Salzberg Jul 09 '21 at 04:06
  • Thanks @ChrisSalzberg and Naveen, I've clarified the question. I don't know why Active Record behaves this way. – Sébastien Dubois Jul 09 '21 at 07:03

1 Answers1

1

If incase you are trying to get some very specific attributes from your collection, then the CollectionProxy would provide you with single query. If none of the attributes(columns) are provided, it does a distinct query before doing the OUTER JOIN query.

Honestly I haven't read through the whole implementation to confirm the reasoning behind it.

But let me show you one thing.

2.5.1 :037 > Post.eager_load(:translations).each do |post|
2.5.1 :038 >     puts post.title
2.5.1 :039?>   end
  
  SQL (0.5ms)  SELECT "posts"."id" AS t0_r0, "posts"."title" AS t0_r1, "posts"."created_at" AS t0_r2, "posts"."updated_at" AS t0_r3, "post_translations"."id" AS t1_r0, "post_translations"."title" AS t1_r1, "post_translations"."content" AS t1_r2, "post_translations"."locale" AS t1_r3, "post_translations"."post_id" AS t1_r4, "post_translations"."created_at" AS t1_r5, "post_translations"."updated_at" AS t1_r6 FROM "posts" LEFT OUTER JOIN "post_translations" ON "post_translations"."post_id" = "posts"."id"

title 1
title 2
title 3
title 4

In this above case, you can see the eager_load does what you are expecting. The similar case where you don't mention the needed attributes, I think when it lazy evaluates, it picks up a distinct query in addition to the OUTER JOIN query

2.5.1 :040 > Post.eager_load(:translations)
  SQL (0.3ms)  SELECT  DISTINCT "posts"."id" FROM "posts" LEFT OUTER JOIN "post_translations" ON "post_translations"."post_id" = "posts"."id" LIMIT ?  [["LIMIT", 11]]
  SQL (0.5ms)  SELECT "posts"."id" AS t0_r0, "posts"."title" AS t0_r1, "posts"."created_at" AS t0_r2, "posts"."updated_at" AS t0_r3, "post_translations"."id" AS t1_r0, "post_translations"."title" AS t1_r1, "post_translations"."content" AS t1_r2, "post_translations"."locale" AS t1_r3, "post_translations"."post_id" AS t1_r4, "post_translations"."created_at" AS t1_r5, "post_translations"."updated_at" AS t1_r6 FROM "posts" LEFT OUTER JOIN "post_translations" ON "post_translations"."post_id" = "posts"."id" WHERE "posts"."id" IN (?, ?, ?, ?)  [["id", 1], ["id", 2], ["id", 3], ["id", 4]]
 => #<ActiveRecord::Relation [#<Post id: 1, title: nil, created_at: "2021-07-08 12:42:13", updated_at: "2021-07-09 15:32:48">, #<Post id: 2, title: nil, created_at: "2021-07-09 15:33:50", updated_at: "2021-07-09 15:33:50">, #<Post id: 3, title: nil, created_at: "2021-07-09 15:33:55", updated_at: "2021-07-09 15:33:55">, #<Post id: 4, title: nil, created_at: "2021-07-09 15:33:57", updated_at: "2021-07-09 15:33:57">]>

Hope this is someway helpful. Please post if any comments are there, so I can clarify if I could. :)

  • Thanks Naveen. I don't observe your second example, when simply calling `Post.eager_load(:translations)` in a console I get a single query. However I get two queries as soon as I add a `limit` clause, which I need. How to render a limited amount of posts while avoiding the double query? NB: I've clarified the question, see OP. – Sébastien Dubois Jul 10 '21 at 06:38