How to reduce eager_load query overhead (ActiveRecord)? - ruby-on-rails
I have the following model:
# address.rb
class Address < ActiveRecord::Base
has_many :orders_for_cleaning, class_name: 'Order',
foreign_key: :cleaning_address_id
has_many :orders_for_billing, class_name: 'Order',
foreign_key: :billing_address_id
has_many :purchases_for_shipping, class_name: 'Purchase',
foreign_key: :shipping_address_id
has_many :purchases_for_billing, class_name: 'Purchase',
foreign_key: :billing_address_id
end
and I want to fetch all addresses of a certain Customer through his orders a purchases using eager_load:
# customer.rb
def addresses
Address.eager_load(:orders_for_cleaning,
:orders_for_billing,
:purchases_for_shipping,
:purchases_for_billing)
.where('orders.customer_id = ?
OR orders_for_billings_addresses.customer_id = ?
OR purchases.customer_id = ?
OR purchases_for_billings_addresses.customer_id = ?',
id, id, id, id)
end
This request generates the following query:
SELECT DISTINCT "addresses"."id" AS t0_r0,
"addresses"."latitude" AS t0_r1,
"addresses"."longitude" AS t0_r2,
"addresses"."house" AS t0_r3,
"addresses"."street" AS t0_r4,
"addresses"."city" AS t0_r5,
"addresses"."zip" AS t0_r6,
"addresses"."state" AS t0_r7,
"addresses"."country" AS t0_r8,
"addresses"."created_at" AS t0_r9,
"addresses"."updated_at" AS t0_r10,
"addresses"."street2" AS t0_r11,
"addresses"."custom_latitude" AS t0_r12,
"addresses"."custom_longitude" AS t0_r13,
"addresses"."name" AS t0_r14,
"addresses"."company" AS t0_r15,
"addresses"."archived" AS t0_r16,
"orders"."id" AS t1_r0,
"orders"."customer_id" AS t1_r1,
"orders"."cleaning_address_id" AS t1_r2,
"orders"."billing_address_id" AS t1_r3,
"orders"."start_at" AS t1_r4,
"orders"."finish_at" AS t1_r5,
"orders"."planned_at" AS t1_r6,
"orders"."started_at" AS t1_r7,
"orders"."finished_at" AS t1_r8,
"orders"."payed_price" AS t1_r9,
"orders"."status" AS t1_r10,
"orders"."pin" AS t1_r11,
"orders"."comment" AS t1_r12,
"orders"."created_at" AS t1_r13,
"orders"."updated_at" AS t1_r14,
"orders"."name" AS t1_r15,
"orders"."company" AS t1_r16,
"orders"."phone" AS t1_r17,
"orders"."coupon_code" AS t1_r18,
"orders"."coupon_discount" AS t1_r19,
"orders"."location_id" AS t1_r20,
"orders"."location_discount" AS t1_r21,
"orders"."job_count" AS t1_r22,
"orders"."bonus" AS t1_r23,
"orders"."organization_id" AS t1_r24,
"orders"."bill_id" AS t1_r25,
"orders"."cleaning_count_discount" AS t1_r26,
"orders"."created_by" AS t1_r27,
"orders"."price_group_id" AS t1_r28,
"orders"."travel_charge" AS t1_r29,
"orders"."departed_at" AS t1_r30,
"orders"."arrived_at" AS t1_r31,
"orders"."request_feedback" AS t1_r32,
"orders"."feedback_link" AS t1_r33,
"orders"."feedback_requested_at" AS t1_r34,
"orders"."rating" AS t1_r35,
"orders"."feedback" AS t1_r36,
"orders"."profit_center_code" AS t1_r37,
"orders"."feedback_received_at" AS t1_r38,
"orders"."reorder_bonus" AS t1_r39,
"orders"."paid_to" AS t1_r40,
"orders"."cleaner_id" AS t1_r41,
"orders"."station_id" AS t1_r42,
"orders"."cleaner_comment" AS t1_r43,
"orders_for_billings_addresses"."id" AS t2_r0,
"orders_for_billings_addresses"."customer_id" AS t2_r1,
"orders_for_billings_addresses"."cleaning_address_id" AS t2_r2,
"orders_for_billings_addresses"."billing_address_id" AS t2_r3,
"orders_for_billings_addresses"."start_at" AS t2_r4,
"orders_for_billings_addresses"."finish_at" AS t2_r5,
"orders_for_billings_addresses"."planned_at" AS t2_r6,
"orders_for_billings_addresses"."started_at" AS t2_r7,
"orders_for_billings_addresses"."finished_at" AS t2_r8,
"orders_for_billings_addresses"."payed_price" AS t2_r9,
"orders_for_billings_addresses"."status" AS t2_r10,
"orders_for_billings_addresses"."pin" AS t2_r11,
"orders_for_billings_addresses"."comment" AS t2_r12,
"orders_for_billings_addresses"."created_at" AS t2_r13,
"orders_for_billings_addresses"."updated_at" AS t2_r14,
"orders_for_billings_addresses"."name" AS t2_r15,
"orders_for_billings_addresses"."company" AS t2_r16,
"orders_for_billings_addresses"."phone" AS t2_r17,
"orders_for_billings_addresses"."coupon_code" AS t2_r18,
"orders_for_billings_addresses"."coupon_discount" AS t2_r19,
"orders_for_billings_addresses"."location_id" AS t2_r20,
"orders_for_billings_addresses"."location_discount" AS t2_r21,
"orders_for_billings_addresses"."job_count" AS t2_r22,
"orders_for_billings_addresses"."bonus" AS t2_r23,
"orders_for_billings_addresses"."organization_id" AS t2_r24,
"orders_for_billings_addresses"."bill_id" AS t2_r25,
"orders_for_billings_addresses"."cleaning_count_discount" AS t2_r26,
"orders_for_billings_addresses"."created_by" AS t2_r27,
"orders_for_billings_addresses"."price_group_id" AS t2_r28,
"orders_for_billings_addresses"."travel_charge" AS t2_r29,
"orders_for_billings_addresses"."departed_at" AS t2_r30,
"orders_for_billings_addresses"."arrived_at" AS t2_r31,
"orders_for_billings_addresses"."request_feedback" AS t2_r32,
"orders_for_billings_addresses"."feedback_link" AS t2_r33,
"orders_for_billings_addresses"."feedback_requested_at" AS t2_r34,
"orders_for_billings_addresses"."rating" AS t2_r35,
"orders_for_billings_addresses"."feedback" AS t2_r36,
"orders_for_billings_addresses"."profit_center_code" AS t2_r37,
"orders_for_billings_addresses"."feedback_received_at" AS t2_r38,
"orders_for_billings_addresses"."reorder_bonus" AS t2_r39,
"orders_for_billings_addresses"."paid_to" AS t2_r40,
"orders_for_billings_addresses"."cleaner_id" AS t2_r41,
"orders_for_billings_addresses"."station_id" AS t2_r42,
"orders_for_billings_addresses"."cleaner_comment" AS t2_r43,
"purchases"."id" AS t3_r0,
"purchases"."product_price_group_id" AS t3_r1,
"purchases"."customer_id" AS t3_r2,
"purchases"."shipping_address_id" AS t3_r3,
"purchases"."billing_address_id" AS t3_r4,
"purchases"."created_by" AS t3_r5,
"purchases"."token" AS t3_r6,
"purchases"."shipping" AS t3_r7,
"purchases"."coupon_code" AS t3_r8,
"purchases"."coupon_discount" AS t3_r9,
"purchases"."comment" AS t3_r10,
"purchases"."status" AS t3_r11,
"purchases"."purchased_at" AS t3_r12,
"purchases"."created_at" AS t3_r13,
"purchases"."updated_at" AS t3_r14,
"purchases"."purchase_invoice_id" AS t3_r15,
"purchases"."payment" AS t3_r16,
"purchases"."pickup" AS t3_r17,
"purchases"."tracking_number" AS t3_r18,
"purchases"."paper_invoice_sent_at" AS t3_r19,
"purchases_for_billings_addresses"."id" AS t4_r0,
"purchases_for_billings_addresses"."product_price_group_id" AS t4_r1,
"purchases_for_billings_addresses"."customer_id" AS t4_r2,
"purchases_for_billings_addresses"."shipping_address_id" AS t4_r3,
"purchases_for_billings_addresses"."billing_address_id" AS t4_r4,
"purchases_for_billings_addresses"."created_by" AS t4_r5,
"purchases_for_billings_addresses"."token" AS t4_r6,
"purchases_for_billings_addresses"."shipping" AS t4_r7,
"purchases_for_billings_addresses"."coupon_code" AS t4_r8,
"purchases_for_billings_addresses"."coupon_discount" AS t4_r9,
"purchases_for_billings_addresses"."comment" AS t4_r10,
"purchases_for_billings_addresses"."status" AS t4_r11,
"purchases_for_billings_addresses"."purchased_at" AS t4_r12,
"purchases_for_billings_addresses"."created_at" AS t4_r13,
"purchases_for_billings_addresses"."updated_at" AS t4_r14,
"purchases_for_billings_addresses"."purchase_invoice_id" AS t4_r15,
"purchases_for_billings_addresses"."payment" AS t4_r16,
"purchases_for_billings_addresses"."pickup" AS t4_r17,
"purchases_for_billings_addresses"."tracking_number" AS t4_r18,
"purchases_for_billings_addresses"."paper_invoice_sent_at" AS t4_r19
FROM "addresses"
LEFT OUTER JOIN "orders"
ON "orders"."cleaning_address_id" = "addresses"."id"
LEFT OUTER JOIN "orders" "orders_for_billings_addresses"
ON "orders_for_billings_addresses"."billing_address_id" = "addresses"."id"
LEFT OUTER JOIN "purchases"
ON "purchases"."shipping_address_id" = "addresses"."id"
LEFT OUTER JOIN "purchases" "purchases_for_billings_addresses"
ON "purchases_for_billings_addresses"."billing_address_id" = "addresses"."id"
WHERE (orders.customer_id = 3282 OR orders_for_billings_addresses.customer_id = 3282
OR purchases.customer_id = 3282 OR purchases_for_billings_addresses.customer_id = 3282)
this query takes about 100ms on my dataset.
But I don't need to load all join model columns just their foreign keys. If I execute stripped SQL query:
SELECT addresses.*,
orders.cleaning_address_id,
orders_for_billings_addresses.billing_address_id,
purchases.shipping_address_id,
purchases_for_billings_addresses.billing_address_id
FROM addresses
LEFT OUTER JOIN orders
ON orders.cleaning_address_id = addresses.id
LEFT OUTER JOIN orders orders_for_billings_addresses
ON orders_for_billings_addresses.billing_address_id = addresses.id
LEFT OUTER JOIN purchases
ON purchases.shipping_address_id = addresses.id
LEFT OUTER JOIN purchases purchases_for_billings_addresses
ON purchases_for_billings_addresses.billing_address_id = addresses.id
WHERE (orders.customer_id = 3282
OR orders_for_billings_addresses.customer_id = 3282
OR purchases.customer_id = 3282
OR purchases_for_billings_addresses.customer_id = 3282)
It returns the same results in only 50ms.
The question: is there a way to enforce ActiveRecord to select only necessary fields with eager_load? Adding select('addresses.*, <fk_fields...>) doesn't help.
eager_load is not what you are looking for based on your description. eager_load will load all the associations immediately. why not try using includes, references and select instead e.g.
def addresses
Address.includes(:orders_for_cleaning,
:orders_for_billing,
:purchases_for_shipping,
:purchases_for_billing).
references(:orders_for_cleaning,
:orders_for_billing,
:purchases_for_shipping,
:purchases_for_billing).
where("orders.customer_id = :id
OR orders_for_billings_addresses.customer_id = :id
OR purchases.customer_id = :id
OR purchases_for_billings_addresses.customer_id = :id",
{id: id}).
select("addresses.*,
orders.cleaning_address_id as cleaning_address_id,
orders_for_billings_addresses.billing_address_id as order_billing_address_id,
purchases.shipping_address_id as purchases_shipping_address_id,
purchases_for_billings_addresses.billing_address_id as purchases_billing_address_id")
end
I believe this will produce the query you are looking for and add methods for retrieval of cleaning_address_id,order_billing_address_id,purchases_shipping_address_id, and purchases_billing_address_id.
I have not tested this so I hope it helps if it does not let me know and I will remove it.
Update
Since the above code is still eager loading according to the OP. (of which I am not sure why and since the relationships are complex debugging personally seems difficult) I am suggesting building the query long hand.
relationship_joins = <<-SQL
LEFT OUTER JOIN orders
ON orders.cleaning_address_id = addresses.id
LEFT OUTER JOIN orders orders_for_billings_addresses
ON orders_for_billings_addresses.billing_address_id = addresses.id
LEFT OUTER JOIN purchases
ON purchases.shipping_address_id = addresses.id
LEFT OUTER JOIN purchases purchases_for_billings_addresses
ON purchases_for_billings_addresses.billing_address_id = addresses.id
SQL
Address.joins(relationship_joins).
where("orders.customer_id = :id
OR orders_for_billings_addresses.customer_id = :id
OR purchases.customer_id = :id
OR purchases_for_billings_addresses.customer_id = :id",
{id: id}).
select("addresses.*,
orders.cleaning_address_id as cleaning_address_id,
orders_for_billings_addresses.billing_address_id as order_billing_address_id,
purchases.shipping_address_id as purchases_shipping_address_id,
purchases_for_billings_addresses.billing_address_id as purchases_billing_address_id")
That should create the query you are requesting. It is not as pretty but sometimes you have to sacrifice beauty for functionality.
Related
Eager loading model instances using single SQL statement with Mobility table backend
Using Rails 7 and Mobility 1.2.9: # config/initializers/mobility.rb Mobility.configure do |config| config.plugins do backend :table active_record reader writer backend_reader query cache dirty presence end end # app/models/content.rb class Content < ApplicationRecord extend Mobility translates :slug, :title end How to find contents by slug, instantiate them into Content objects and query their translated attributes using one single SQL statement (for an unchanging locale)? The examples below with and without eager_loading run 2 SQL statements. > content = Content.i18n.find_by(slug: "foo") Content Load (0.7ms) SELECT "contents".* FROM "contents" INNER JOIN "content_translations" "content_translations_en" ON "content_translations_en"."content_id" = "contents"."id" AND "content_translations_en"."locale" = 'en' WHERE "content_translations_en"."slug" = 'foo' LIMIT $1 [["LIMIT", 1]] > content.title Content::Translation Load (0.3ms) SELECT "content_translations".* FROM "content_translations" WHERE "content_translations"."content_id" = $1 [["content_id", "..."]] > content = Content.i18n.eager_load(:translations).find_by(slug: "foo") SQL (0.6ms) SELECT DISTINCT "contents"."id" FROM "contents" LEFT OUTER JOIN "content_translations" ON "content_translations"."content_id" = "contents"."id" INNER JOIN "content_translations" "content_translations_en" ON "content_translations_en"."content_id" = "contents"."id" AND "content_translations_en"."locale" = 'en' WHERE "content_translations_en"."slug" = 'blog-1-en' LIMIT $1 [["LIMIT", 1]] SQL (0.8ms) SELECT "contents"."id" AS t0_r0, "contents"."created_at" AS t0_r1, "contents"."updated_at" AS t0_r2, ..., "content_translations"."id" AS t1_r0, "content_translations"."locale" AS t1_r1, "content_translations"."created_at" AS t1_r2, "content_translations"."updated_at" AS t1_r3, "content_translations"."slug" AS t1_r4, "content_translations"."title" AS t1_r5, "content_translations"."content_id" AS t1_r6 FROM "contents" LEFT OUTER JOIN "content_translations" ON "content_translations"."content_id" = "contents"."id" INNER JOIN "content_translations" "content_translations_en" ON "content_translations_en"."content_id" = "contents"."id" AND "content_translations_en"."locale" = 'en' WHERE "content_translations_en"."slug" = 'blog-1-en' AND "contents"."id" = $1 [["id", "..."]] > content.title # no DB statement, but there were 2 statements at instantiation NB: I do not want to pluck attributes but instead to create model instances.
Rails is breaking SQL query when modifying order
Context We have a Rails app that is retrieving conversations with the following raw SQL query: SELECT sub.*, profiles.status AS interlocutor_status FROM ( SELECT DISTINCT ON (conversations.id) conversations.id, conversation_preferences.unread_counter, left(messages.content, 50) AS last_message, posts.id AS post_id, messages.created_at AS last_activity_on, categories.root_name AS site_name, conversation_preferences.state, COALESCE(NULLIF(post_owner, 1234567), NULLIF(post_applicant, 1234567)) AS interlocutor_id FROM "conversations" LEFT OUTER JOIN "conversation_preferences" ON "conversation_preferences"."conversation_id" = "conversations"."id" LEFT OUTER JOIN "posts" ON "posts"."id" = "conversations"."post_id" LEFT OUTER JOIN "categories" ON "categories"."id" = "posts"."category_id" LEFT OUTER JOIN "messages" ON "messages"."conversation_id" = "conversations"."id" WHERE (post_applicant = 1234567 OR post_owner = 1234567) AND "conversation_preferences"."user_id" = 1234567 ORDER BY "conversations"."id" ASC, messages.created_at DESC ) sub LEFT OUTER JOIN users ON interlocutor_id = users.id LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id WHERE ("profiles"."status" != 'pending') AND (last_activity_on >= '2021-01-19 04:40:22.881985') AND (state = 'active') ORDER BY profiles.status, sub.unread_counter DESC, sub.last_activity_on DESC LIMIT 25 We generate this query using the following ActiveRecord code: def fetch distinct = Conversation.left_outer_joins(:preferences) .left_outer_joins(post: :category) .left_outer_joins(:messages) .where('post_applicant = :id OR post_owner = :id', id: current_user.id) .where(conversation_preferences: { user_id: current_user.id }) .select( <<-SQL.squish DISTINCT ON (conversations.id) conversations.id, conversation_preferences.unread_counter, left(messages.content, 50) AS last_message, posts.id AS post_id, messages.created_at AS last_activity_on, categories.root_name AS site_name, conversation_preferences.state, COALESCE(NULLIF(post_owner, #{current_user.id}), NULLIF(post_applicant, #{current_user.id})) AS interlocutor_id SQL ) .order(:id, 'messages.created_at DESC') Conversation.includes(post: :category) .from(distinct, :sub) .select('sub.*, profiles.status AS interlocutor_status') .joins('LEFT OUTER JOIN users ON interlocutor_id = users.id') .joins('LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id') .where.not('profiles.status' => :pending) .order('profiles.status, sub.unread_counter DESC, sub.last_activity_on DESC') end Problem We want to stop ordering by profiles.status. To do this, we naturally removed it from the last order statement: order('sub.unread_counter DESC, sub.last_activity_on DESC') That's the problem. Doing that is entirely breaking the generated query, that generate an error which is irrelevant here because we don't want the modified query (note how it is different from the 1st one): SELECT sub.*, profiles.status AS interlocutor_status, "conversations"."id" AS t0_r0, "conversations"."post_id" AS t0_r1, "conversations"."post_owner" AS t0_r2, "conversations"."post_applicant" AS t0_r3, "conversations"."created_at" AS t0_r4, "conversations"."updated_at" AS t0_r5, "posts"."id" AS t1_r0, "posts"."title" AS t1_r1, "posts"."description" AS t1_r2, "categories"."id" AS t2_r0, "categories"."name" AS t2_r1 FROM ( SELECT DISTINCT ON (conversations.id) conversations.id, conversation_preferences.unread_counter, left(messages.content, 50) AS last_message, posts.id AS post_id, messages.created_at AS last_activity_on, categories.root_name AS site_name, conversation_preferences.state, COALESCE(NULLIF(post_owner, 1234567), NULLIF(post_applicant, 1234567)) AS interlocutor_id FROM "conversations" LEFT OUTER JOIN "conversation_preferences" ON "conversation_preferences"."conversation_id" = "conversations"."id" LEFT OUTER JOIN "posts" ON "posts"."id" = "conversations"."post_id" LEFT OUTER JOIN "categories" ON "categories"."id" = "posts"."category_id" LEFT OUTER JOIN "messages" ON "messages"."conversation_id" = "conversations"."id" WHERE (post_applicant = 1234567 OR post_owner = 1234567) AND "conversation_preferences"."user_id" = 1234567 ORDER BY "conversations"."id" ASC, messages.created_at DESC ) sub LEFT OUTER JOIN "posts" ON "posts"."id" = "conversations"."post_id" LEFT OUTER JOIN "categories" ON "categories"."id" = "posts"."category_id" LEFT OUTER JOIN users ON interlocutor_id = users.id LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id WHERE ("profiles"."status" != 'pending') AND (last_activity_on >= '2021-01-19 05:04:06.084499') AND (state = 'active') ORDER BY sub.unread_counter DESC, sub.last_activity_on DESC LIMIT 25 I know without a bit of context it'll be hard to help us but if someone knows why ActiveRecord is changing the query after trying to just remove profiles.status from the order statement, that would be awesome. Thanks in advance NOTE: modifying the 1st raw SQL directly (from our postgres client) does works. The issue is not the first query, but maybe how ActiveRecord is handling it
Finally found a way to make it work using preload instead of includes. We wanted to avoid having seperate queries to load posts and categories but since performance is not affected by it, we don't mind it. Here is how it look like: Conversation.preload(post: :category) .from(distinct, :sub) .select('sub.*, profiles.status AS interlocutor_status') .joins('LEFT OUTER JOIN users ON interlocutor_id = users.id') .joins('LEFT OUTER JOIN profiles ON interlocutor_id = profiles.user_id') .where.not('profiles.status' => :pending) .order('sub.unread_counter DESC, sub.last_activity_on DESC') Which generates 3 queries: -- Query 1 SELECT sub.*, profiles.status AS interlocutor_status FROM ( SELECT DISTINCT .... -- Query 2 SELECT "posts".* FROM "posts" WHERE "posts"."id" IN (............) -- Query 3 SELECT "categories".* FROM "categories" WHERE "categories"."id" IN (..........) Thanks to everyone for the help in comments (max and Sebastian Palma)
Rails 4 - Eager loading with multiple ON conditions
I have an Article and each Article has exactly one User and either zero or one ArticleVote. My code is: #articles = Article .eager_load(:user) .eager_load(:article_vote) .where(article_votes: { user_id: session[:user_id]}) .order(created_at: :desc) Which produces the following SQL: SELECT `articles`.`article_id` AS t0_r0, `articles`.`user_id` AS t0_r1, `users`.`user_id` AS t1_r0, `article_votes`.`article_vote_id` AS t2_r0, `article_votes`.`article_id` AS t2_r1, `article_votes`.`user_id` AS t2_r2 FROM `articles` LEFT OUTER JOIN `users` ON `users`.`user_id` = `articles`.`user_id` LEFT OUTER JOIN `article_votes` ON `article_votes`.`article_id` = `articles`.`article_id` WHERE `article_votes`.`user_id` = 1 ORDER BY `articles`.`created_at` DESC The problem with this query is if no vote exists (which sometimes it won't), then no records are returned. What I really need is this (note the where is gone and the condition is moved to the left join): SELECT `articles`.`article_id` AS t0_r0, `articles`.`user_id` AS t0_r1, `users`.`user_id` AS t1_r0, `article_votes`.`article_vote_id` AS t2_r0, `article_votes`.`article_id` AS t2_r1, `article_votes`.`user_id` AS t2_r2 FROM `articles` LEFT OUTER JOIN `users` ON `users`.`user_id` = `articles`.`user_id` LEFT OUTER JOIN `article_votes` ON `article_votes`.`article_id` = `articles`.`article_id` and `article_votes`.`user_id` = 1 ORDER BY `articles`.`created_at` DESC I saw in the documentation I can do something like: has_one :article_vote, -> { where(user_id: 1) } Which will put it in the left join vs the where, but this doesn't let me specify the user_id at query time, which makes it not viable. Can this be done?
Avoid SQL`s JOIN when using ActiveRecord`s includes method?
I've this ruby code which cause postgresql to raise column "urls.id" must appear in the GROUP BY. Song. joins(:artist). references(:artist). where("artists.active = ?", true). group("songs.id"). includes(:urls) The problem is that rails is joining when adding includes(:urls) instead of running a separate query. Is it possible to force rails to run a second query to avoid this problem? In other words; I want rails to use SQL's JOIN when using ActiveRecord::Relation.joins and a separate query when using ActiveRecord::Relation.includes. Removing the where and the references method makes everything pass, but then I can't query against the artists table. The error message. SELECT "songs"."id" AS t0_r0, "songs"."artist_id" AS t0_r1, "songs"."title" AS t0_r2, "songs"."grade" AS t0_r3, "songs"."length" AS t0_r4, "songs"."gigs_count" AS t0_r5, "songs"."clicks" AS t0_r6, "songs"."album_cover_id" AS t0_r7, "songs"."created_at" AS t0_r8, "songs"."updated_at" AS t0_r9, "songs"."position" AS t0_r10, "songs"."services" AS t0_r11, "songs"."moved_id" AS t0_r12, "songs"."details_updated_at" AS t0_r13, "songs"."genres_updated_at" AS t0_r14, "urls"."id" AS t1_r0, "urls"."url" AS t1_r1, "urls"."service_id" AS t1_r2, "urls"."media_id" AS t1_r3, "urls"."media_type" AS t1_r4, "urls"."extra" AS t1_r5 FROM "songs" INNER JOIN "artists" ON "artists"."id" = "songs"."artist_id" LEFT OUTER JOIN "url_bridges" ON "url_bridges"."media_bridge_id" = "songs"."id" AND "url_bridges"."media_bridge_type" = 'Song' LEFT OUTER JOIN "urls" ON "urls"."id" = "url_bridges"."url_id" WHERE (artists.active = 't') AND "songs"."id" IN (944) GROUP BY songs.id PG::GroupingError: ERROR: column "urls.id" must appear in the GROUP BY clause or be used in an aggregate function LINE 1: ...AS t0_r13, "songs"."genres_updated_at" AS t0_r14, "urls"."id... ^ : SELECT "songs"."id" AS t0_r0, "songs"."artist_id" AS t0_r1, "songs"."title" AS t0_r2, "songs"."grade" AS t0_r3, "songs"."length" AS t0_r4, "songs"."gigs_count" AS t0_r5, "songs"."clicks" AS t0_r6, "songs"."album_cover_id" AS t0_r7, "songs"."created_at" AS t0_r8, "songs"."updated_at" AS t0_r9, "songs"."position" AS t0_r10, "songs"."services" AS t0_r11, "songs"."moved_id" AS t0_r12, "songs"."details_updated_at" AS t0_r13, "songs"."genres_updated_at" AS t0_r14, "urls"."id" AS t1_r0, "urls"."url" AS t1_r1, "urls"."service_id" AS t1_r2, "urls"."media_id" AS t1_r3, "urls"."media_type" AS t1_r4, "urls"."extra" AS t1_r5 FROM "songs" INNER JOIN "artists" ON "artists"."id" = "songs"."artist_id" LEFT OUTER JOIN "url_bridges" ON "url_bridges"."media_bridge_id" = "songs"."id" AND "url_bridges"."media_bridge_type" = 'Song' LEFT OUTER JOIN "urls" ON "urls"."id" = "url_bridges"."url_id" WHERE (artists.active = 't') AND "songs"."id" IN (944) GROUP BY songs.id I'm running PostgreSQL 9.3.2 on x86_64-unknown-linux-gnu, compiled by gcc-4.4.real (Ubuntu 4.4.3-4ubuntu5) 4.4.3, 64-bit.
Replace includes with preload, the documentation is not clear enough of it's internals but it will run a second query to eager load the relationship you specify. I've used it for your same use case with joins and group. Song. joins(:artist). references(:artist). where("artists.active = ?", true). group("songs.id"). preload(:urls) http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-preload
I would suggest a second query for the urls manually. Don't use the includes(:urls). #songs = Song.complex_query #urls = Url.where(:song_id => #songs.ids) #urls_by_song_id = #urls.group_by {|u| u.song_id} Using group_by will put the urls into a hash with the key being the song_id and the value is an array of urls. You would access the urls like this: #urls_by_song_id[song.id] It isn't as clean as the includes, but it has the same effect. This gives you access to the urls without the n+1 overhead.
How to access joined record from Rails LEFT OUTER JOIN
Howdy I found plenty of examples on how to use LEFT OUTER JOIN, but I can't seem to find a way to access what I have joined. Here is what I mean: List.featured. joins( "LEFT OUTER JOIN follows ON ( follows.followable_id = lists.id AND follows.followable_type = 'List' AND follows.user_id = #{current_user.id})" ).map { |list| list.follows # <-- returns all follows, not only the ones from current_user ... In the example I get the follows (it seems) with the join, but then how can I access them? The follows relation will just give me all follows for that list it seems. Or maybe my mind is fogged :) Thanks!
To eager load follows, you can call includes(): List.featured.includes(:follows).where(:follows => { :user_id => current_user.id }) It generates queries like this: SELECT "lists"."id" AS t0_r0, "lists"."is_featured" AS t0_r1, "lists"."created_at" AS t0_r2, "lists"."updated_at" AS t0_r3, "follows"."id" AS t1_r0, "follows"."followable_id" AS t1_r1, "follows"."followable_type" AS t1_r2, "follows"."user_id" AS t1_r3, "follows"."created_at" AS t1_r4, "follows"."updated_at" AS t1_r5 FROM "lists" LEFT OUTER JOIN "follows" ON "follows"."followable_id" = "lists"."id" AND "follows"."followable_type" = 'List' WHERE "lists"."is_featured" = 't' AND "follows"."user_id" = 1