Eager Loading Optimization in ruby on rails - ruby-on-rails

I have a product that has_many Variants. And a variant belongs to a product. I want to display the name of the product(which can be found in Product) and the price and quantity(which can be found in Variants).
Product table:
-id
-title
-description
Variants table:
- id
- is_active(boolean)
- price
- quantity
- product_id
This is how the table looks like. And this is my attempt
def index
#products =Product.all
#display = []
#products.each do |product|
#children = product.variants.where(is_active: true).order(:price).
select(:id,:price,:quantity).first
if #children.present?
#display << {
id: product.id,
title: product.title,
description: product.description,
price: #children.price,
quantity: #children.quantity,
variant_id: #children.id
}
end
end
#display = Kaminari.paginate_array(#display).page(params[:page])
end
I need to optimize this to the maximum. Which brings me to my first question. How can I optimize this better .
And my 2nd question why when I do #products = Product.all.includes(:variants) it will actually increase the loading time instead of lowering it since I do get the variants for every product in that iteration over the whole #products array(so it should count as an N+1, I get products for each product I get variants)?
Is spliting my array of #products in 4 and making 4 threads populate display a good idea?

Your code isn't using the eager loaded data which is why adding includes is slowing things down - it's just extra work.
In general if you call query methods ( where, order, etc) rails can't use the eager loaded data. Instead you can make a new association:
has_many :active_variants, -> { where(is_active: true).order(:price) }, class_name: "Variant"
Then eager load and use this association instead of the variants association.

You should write it as;
def index
#display = Varient.joins(:product).where(is_active: true).order(:price).select('products.id as id, products.title, products.description, price, quantity, variants.id as variant_id')
end

Related

Where in Spree is the prices table joined to the products for lists?

I modified Spree so that a product/variant can have more than one price. This means a product has a one_time price, a recurring_price and a consumption_price. For this I added a price_type field to the Price model. Now I have a problem with lists. The product gets listed three times because somewhere the prices are inner joined resulting in 3 entries in the list for the three different price types. Where do I fix this, meaning inner joining only price_type = one_time for lists (or in general until otherwise specified)?
I figured something out and it works for me so far:
Spree::Product.class_eval do
class << self
alias_method :orig_available, :available
def available(available_on = nil, _currency = nil)
orig_available(available_on, _currency).where("#{Spree::Price.quoted_table_name}.price_type = ?", Spree::Price.price_types[:one_time])
end
end
end

Selecting average and latest value of column on associated record using rails/activerecord

I have three models:
class Customer < ActiveRecord::Base
has_many :monthly_records
belongs_to :store
end
I need want to create an index view where I show the following fields:
customer.id
customer.name
store.name
average of monthly_records.grand total where monthly_records.month_start > ? AND monthly_records.month_start <= ?
monthly_records.grand_total for a specific month that the user will supply through the controller
Because this is an index and I want to show many rows at a time I want to do this in a single query and avoid the N+1 problem. I know how to do this with Customer.select.join.group if I take 1-3 and EITHER 4 or 5, but I can't figure out how to do all 5 at once.
I've found some pure SQL answers that look like they may work, but I'd prefer to keep this to activerecord if at all possible.
Using postgres 9.3.4 and rails 4.0.10.
Just guessing, Can you try:
Controller:
#customers = Customer.
select('customers.id,customers.name,monthly_records.grand_total,stores.name').
includes(:store, :monthly_records).
references(:stores, :monthly_records)
Try Customer\ to see if dot can start the next lines:
#customers = Customer\
.select('customers.id,customers.name,monthly_records.grand_total,stores.name')...
On view:
- #customers.each do |customer|
- records = customer.monthly_records.\
where('month_start > ? AND month_start <= ?', some_param,some_param)
= customer.id
= customer.name
= customer.store.name
= records.average(:grand_total)
= records.sum(:grand_total)
I think there is one set of records to be eager loaded (records variable). But this would be a start. Can you try? a let me know which tables have the N+1 problem with this approach.
Update: Eager loading monthly records for a given month
Rails gives you the possibility to eager load an association:
Model:
has_many :this_month_monthly_records,
->{ where('month_start > :start, month_start <= :start', start: your_month) },
class_name: 'MonthlyRecord'
Controller:
#customers = Customer.
select('customers.id,customers.name,monthly_records.grand_total,stores.name').
includes(:store, :this_month_monthly_records).
references(:stores, :monthly_records)
On view:
- #customers.each do |customer|
- records = customer.this_month_monthly_records
= customer.id
= customer.name
= customer.store.name
= records.average(:grand_total)
= records.sum(:grand_total)
The problem with this way is your_month variable. It's passed in params but I see no safe way to pass it to the model.

How to sort through an associated attribute on two levels at once?

This is a simple ruby question I believe. In my app, I have Product model that has_many Reviews. Each Review has an attribute of an "overall" rating which is an integer.
What I want to do is display the top ten Products based on the average of their overall ratings. I've already gotten this to work, BUT, I also want to sort Products that have the SAME overall rating by a secondary aggregate attribute, which would be how MANY reviews that Product has. Right now, if I have 3 products with the same average overall rating, they seem to be displayed in random order.
So far my code is:
Controller
#best = Product.has_reviews.get_best_products(10)
Product Model
scope :has_reviews, joins{reviews.outer}.where{reviews.id != nil}
def self.get_best_products(number)
sorted = self.uniq
sorted = sorted.sort { |x, y| y.reviews.average("overall").to_f <=> x.reviews.average("overall").to_f }
sorted.first(number)
end
I've tried this for my model code:
def self.get_best_products(number)
sorted = self.uniq.sort! { |x, y| x.reviews.count.to_f <=> y.reviews.count.to_f }
sorted = sorted.sort { |x, y| y.reviews.average("overall").to_f <=> x.reviews.average("overall").to_f }
sorted.first(number)
end
...but it does not do what I want it to do. I am just iterating through the #best array using each in my view.
---UPDATE
OK now I am trying this:
Controller:
#best = Product.get_best_products(6)
Model:
def self.get_best_products(number)
self.joins{reviews}.order{'AVG(reviews.overall), COUNT(reviews)'}.limit(number)
end
But I am getting this error:
PGError: ERROR: column "products.id" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT "products".* FROM "products" INNER JOIN "reviews" ...
I am using the Squeel gem btw to avoid having direct SQL code in the model.
----UPDATE 2
Now I added the 'group' part to my method but I am still getting an error:
def self.get_best_products(number)
self.joins{reviews}.group('product.id').order{'AVG(reviews.overall), COUNT(reviews)'}.limit(number)
end
I get this error:
PGError: ERROR: missing FROM-clause entry for table "product"
LINE 1: ...eviews"."product_id" = "products"."id" GROUP BY product.i...
product.rb
scope :best_products, (lambda do |number|
joins(:reviews).order('AVG(reviews.overall), COUNT(reviews)').limit(number)
)
products_controller.rb
Product.best_products(10)
This makes sure everything happens in the database, so you won't get records you don't need.
If I got it right here is my idea of how I would do it:
As products has many reviews and reviews has an overall attribute I would add a reviews_counter column to the products table that will increment with each added review, this way you'll be able to gain a little more db performance as you don't have to count all the products reviews to get the most reviewed one.
Now you'll get the products ordered by reviews_counter:
#best_products = Products.order("reviews_counter desc")
and next you'll get the reviews for each product ordered by overall:
<% for prod in #best_products %>
<%= prod.reviews.order("overall desc") %> # can do all this or more in helper
<% end %>
also ordering this way, if you have 3 reviews with the same overall you can one more order() statement and sort it by name or id or whatever you like so they don't display in random order.
This is just my idea of how I would do it, I worked recently on an app that required something similar and we just added a counter_field to our model, it's not illegal to do so :)
p.s. it's not very clear for me how many records you would want to display for each so you'll just need to add .limit(5) for exemple to get only the first 5 reviews of a product.

Finding records which belong to multiple models with HABTM

My Track model has_and_belongs_to_many :moods, :genres, and :tempos (each of which likewise has_and_belongs_to_many :tracks).
I'm trying to build a search "filter" where users can specify any number of genres, moods, and tempos which will return tracks that match any conditions from each degree of filtering.
An example query might be
params[:genres] => "Rock, Pop, Punk"
params[:moods] => "Happy, Loud"
params[:tempos] => "Fast, Medium"
If I build an array of tracks matching all those genres, how can I select from that array those tracks which belong to any and all of the mood params, then select from that second array, all tracks which also match any and all of the tempo params?
I'm building the initial array with
#tracks = []
Genre.find_all_by_name(genres).each do |g|
#tracks = #tracks | g.tracks
end
where genres = params[:genres].split(",")
Thanks.
I'd recommend using your database to actually perform this query as that would be a lot more efficient.
You can try to join all these tables in SQL first and then using conditional queries i.e. where clauses to first try it out.
Once you succeed you can write it in the Active Record based way. I think its fairly important that you write it in SQL first so that you can properly understand whats going on.
This ended up working
#tracks = []
Genre.find_all_by_name(genres).each do |g|
g.tracks.each do |t|
temptempos = []
tempartists = []
tempmoods = []
t.tempos.each do |m|
temptempos.push(m.name)
end
tempartists.push(t.artist)
t.moods.each do |m|
tempmoods.push(m.name)
end
if !(temptempos & tempos).empty? && !(tempartists & artists).empty? && !(tempmoods & moods).empty?
#tracks.push(t)
end
end
end
#tracks = #tracks.uniq

Activerecord opitimization - best way to query all at once?

I am trying to achieve by reducing the numbers of queries using ActiveRecord 3.0.9. I generated about 'dummy' 200K customers and 500K orders.
Here's Models:
class Customer < ActiveRecord::Base
has_many :orders
end
class Orders < ActiveRecord::Base
belongs_to :customer
has_many :products
end
class Product < ActiveRecord::Base
belongs_to :order
end
when you are using this code in the controller:
#customers = Customer.where(:active => true).paginate(page => params[:page], :per_page => 100)
# SELECT * FROM customers ...
and use this in the view (I removed HAML codes for easier to read):
#order = #customers.each do |customer|
customer.orders.each do |order| # SELECT * FROM orders ...
%td= order.products.count # SELECT COUNT(*) FROM products ...
%td= order.products.sum(:amount) # SELECT SUM(*) FROM products ...
end
end
However, the page is rendered the table with 100 rows per page. The problem is that it kinda slow to load because its firing about 3-5 queries per customer's orders. thats about 300 queries to load the page.
There's alternative way to reduce the number of queries and load the page faster?
Notes:
1) I have attempted to use the includes(:orders), but it included more than 200,000 order_ids. that's issue.
2) they are already indexed.
If you're only using COUNT and SUM(amount) then what you really need is to retrieve only that information and not the orders themselves. This is easily done with SQL:
SELECT customer_id, order_id, COUNT(id) AS order_count, SUM(amount) AS order_total FROM orders LEFT JOIN products ON orders.id=products.order_id GROUP BY orders.customer_id, products.order_id
You can wrap this in a method that returns a nice, orderly hash by re-mapping the SQL results into a structure that fits your requirements:
class Order < ActiveRecord::Base
def self.totals
query = "..." # Query from above
result = { }
self.connection.select_rows(query).each do |row|
# Build out an array for each unique customer_id in the results
customer_set = result[row[0].to_i] ||= [ ]
# Add a hash representing each order to this customer order set
customer_set << { :order_id => row[1].to_i, :count => row[2].to_i, :total => row[3].to_i } ]
end
result
end
end
This means you can fetch all order counts and totals in a single pass. If you have an index on customer_id, which is imperative in this case, then the query will usually be really fast even for large numbers of rows.
You can save the results of this method into a variable such as #order_totals and reference it when rendering your table:
- #order = #customers.each do |customer|
- #order_totals[customer.id].each do |order|
%td= order[:count]
%td= order[:total]
You can try something like this (yes, it looks ugly, but you want performance):
orders = Order.find_by_sql([<<-EOD, customer.id])
SELECT os.id, os.name, COUNT(ps.amount) AS count, SUM(ps.amount) AS amount
FROM orders os
JOIN products ps ON ps.order_id = os.id
WHERE os.customer_id = ? GROUP BY os.id, os.name
EOD
%td= orders.name
%td= orders.count
%td= orders.amount
Added: Probably it is better to create count and amount cache in Orders, but you will have to maintain it (count can be counter-cache, but I doubt there is a ready recipe for amount).
You can join the tables in with Arel (I prefer to avoid writing raw sql when possible). I believe that for your example you would do something like:
Customer.joins(:orders -> products).select("id, name, count(products.id) as count, sum(product.amount) as total_amount")
The first method--
Customer.joins(:orders -> products)
--pulls in the nested association in one statement. Then the second part--
.select("id, name, count(products.id) as count, sum(product.amount) as total_amount")
--specifies exactly what columns you want back.
Chain those and I believe you'll get a list of Customer instances only populated with what you've specified in the select method. You have to be careful though because you now have in hand read only objects that are possibly in in invalid state.
As with all the Arel methods what you get from those methods is an ActiveRecord::Relation instance. It's only when you start to access that data that it goes out and executes the SQL.
I have some basic nervousness that my syntax is incorrect but I'm confident that this can be done w/o relying on executing raw SQL.

Resources