anonymus_rex
anonymus_rex

Reputation: 579

Avoid joins on include when sorting by parent and child fields

Context

I'm using ransack and kaminari gems on a Ruby on Rails controller to query and sort records from the following model structure:

Order
- id
- code
- ship_date

OrderDetail
- id
- order_id
- product
- qty

Problem

I need to get all Orders with each associated OrderDetails sorted by this criteria orders.ship_date desc, orders.code desc, order_details.qty desc.

When I sort just by Orders field, it works, does one query per each table and gives me the expected page records.

Order.includes(:order_details).ransack().result.order({ship_date: :desc, code: :desc}).page(1).per(10)
# Order Load (131.7ms)  SELECT "orders".* FROM "orders" ORDER BY "orders"."ship_date" DESC, "orders"."code" DESC LIMIT $1 OFFSET $2  [["LIMIT", 10], ["OFFSET", 0]]
# OrderDetail Load (131.6ms)  SELECT "order_details".* FROM "order_details" WHERE "order_details"."order_id" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)  [["order_id", 2955], ["order_id", 2956], ["order_id", 2954], ["order_id", 2953], ["order_id", 2952], ["order_id", 2917], ["order_id", 2918], ["order_id", 2919], ["order_id", 2920], ["order_id", 2921]]

The problem happens when I include order_details.qty to the sort criteria. It does two queries joining Orders with OrderDetails in each one of them and only gives 4 records, instead of the 10 expected.

Order.includes(:order_details).ransack().result.order({ship_date: :desc, code: :desc, "order_details.qty": :desc}).page(1).per(10)
# SQL (142.3ms) SELECT DISTINCT "orders"."ship_date" AS alias_0, "orders"."code" AS alias_1, "order_details"."qty" AS alias_2, "orders"."id" FROM "orders" LEFT OUTER JOIN "order_details" ON "order_details"."order_id" = "orders"."id" ORDER BY "orders"."ship_date" DESC, "orders"."code" DESC, "order_details"."qty" DESC LIMIT $1 OFFSET $2  [["LIMIT", 10], ["OFFSET", 0]]
# SQL (267.7ms) SELECT orders.id AS t0_r0, "orders"."code" AS t0_r1, "orders"."company_id" AS t0_r2, "orders"."customer_id" AS t0_r3, "orders"."ship_date" AS t0_r4, "orders"."centre_id" AS t0_r5, "orders"."created_at" AS t0_r6, "orders"."updated_at" AS t0_r7, "orders"."remote_id" AS t0_r8, "orders"."comment" AS t0_r9, "order_details"."id" AS t1_r0, "order_details"."order_id" AS t1_r1 "order_details"."product" AS t1_r2, "order_details"."qty" AS t1_r4 FROM "orders" LEFT OUTER JOIN "order_details" ON "order_details"."order_id" = "orders"."id" WHERE "orders"."id" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ORDER BY "orders"."ship_date" DESC, "orders"."code" DESC, "order_details"."qty" DESC  [["id", 2955], ["id", 2956], ["id", 2954], ["id", 2954], ["id", 2954], ["id", 2953], ["id", 2953], ["id", 2953], ["id", 2953], ["id", 2953]]

Expected result

I need to avoid joins to improve queries performance, so it should do only 2 queries, applying each sort criteria on it's respective query. Example:

# Order.includes(:order_details)... <-- need this statement
# So I could get records using queries like this:
# SELECT * FROM orders ORDER BY ship_date DESC, code DESC LIMIT 10 OFFSET 0
# SELECT * FROM order_details WHERE order_id IN (...) ORDER BY qty DESC

Edit/SOLUTION

Thanks to @max's comment, I realize that the problem was with the use of include. As found in this answer and complemmenting with @max's comment, include will use preload only if the the associations are included as they are and have no other references in the statement.

So in my case, to call the associations as they are and remove references, I added a default order on the association.

class Order < ApplicationRecord
  has_many :order_details, -> { order(qty: :desc) }
end

Now this is my new line and the records are preloaded and ordered by orders.ship_date desc, orders.code desc, order_details.qty desc, using only 2 simple queries with no joins.

Order.includes(:order_details).ransack().result.order({ship_date: :desc, code: :desc}).page(1).per(10)
# Order Load (136.5ms)  SELECT "orders".* FROM "orders" ORDER BY "orders"."ship_date" DESC, "orders"."code" DESC LIMIT $1 OFFSET $2  [["LIMIT", 10], ["OFFSET", 0]]
# OrderDetail Load (138.4ms)  SELECT "order_details".* FROM "order_details" WHERE "order_details"."order_id" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ORDER BY "order_details"."pending_qty" DESC, "order_details"."qty" DESC  [["order_id", 3089], ["order_id", 3090], ["order_id", 3088], ["order_id", 3087], ["order_id", 3085], ["order_id", 3086], ["order_id", 3083], ["order_id", 3084], ["order_id", 3082], ["order_id", 3049]]

Upvotes: 0

Views: 32

Answers (0)

Related Questions