How to reduce database queries and is it Worth it? - ruby-on-rails

I currently have an app that lists flights from one location to another, its price and other information. I have implemented a search through a drop down list so it only shows flights either from a certain location, to a certain location or from and to a certain location, depending on how the user searches.
def index
#flights = Flight.all
#flights_source = #flights.select('DISTINCT source') #this line is used for options_from_collection_for_select in view
#flights_destination = #flights.select('DISTINCT destination') #this line is used for options_from_collection_for_select in view
if params[:leaving_from].present? && params[:going_to].blank?
#flights = Flight.where(:source => params[:leaving_from])
elsif params[:going_to].present? && params[:leaving_from].blank?
#flights = Flight.where(:destination => params[:going_to])
elsif params[:leaving_from].present? && params[:going_to].present?
#flights = Flight.where(:source => params[:leaving_from]).where(:destination => params[:going_to])
end
end
The problem is every time I want to add another search parameter, for example price, it's going to be another query. Is there a way to take Flight.all and search within the result and make a new hash or array with only the records that match the search terms, instead of doing a new query with select DISTINCT.
The closest thing I could come up with is somehow turning the result of Flight.all into a array[hash] and using that get the results for distinct source and destination. But not sure how to do that.
And finally would it be worth it to do this to reduce the number of database queries?
These are the current queries:
Flight Load (1.4ms) SELECT "flights".* FROM "flights"
User Load (1.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT 1 [["id", 2]]
Flight Load (1.4ms) SELECT DISTINCT source FROM "flights"
Flight Load (0.8ms) SELECT DISTINCT destination FROM "flights"
EDIT:
I changed the select distinct to
#flights_source = #flights.uniq.pluck(:source)
#flights_destination = #flights.uniq.pluck(:destination)
And used options_for_select instead of options_from_collection_for_select in the view. But the queries are still, I think this means I eliminated us much as I can, not sure though.
(0.8ms) SELECT DISTINCT "flights"."source" FROM "flights"
(0.6ms) SELECT DISTINCT "flights"."destination" FROM "flights"
Request Load (1.3ms) SELECT "requests".* FROM "requests"
Flight Load (1.0ms) SELECT "flights".* FROM "flights"
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT 1 [["id", 2]]

Related

Rails Query Multiple Params From Same Table

How can I search for multiple params? I have checkboxes in my view, so if multiple checkboxes are selected, I would like all the params selected to be chosen. I can currently only get the search to work with one param with code below.
There is a has_many to has_many association between car model and colour_collection model.
Controller:
#cars = car.joins(:colour_collections).where("colour_collections.name = ?", params[:colour_collection])
logs show this if two colours selected (e.g. red and green) creating duplicates in the resulting querie:
(0.7ms) SELECT COUNT(*) FROM "colour_collections"
ColourCollection Load (0.5ms) SELECT "colour_collections".* FROM "colour_collections"
Car Load (2.5ms) SELECT "cars".* FROM "cars" INNER JOIN "car_colour_collections" ON "car_colour_collections"."car_id" = "cars"."id" INNER JOIN "colour_collections" ON "colour_collections"."id" = "car_colour_collections"."colour_collection_id" WHERE "colour_collections"."name" IN ('Subtle', 'Intermediate') ORDER BY "cars"."created_at" DESC
CarAttachment Load (0.5ms) SELECT "car_attachments".* FROM "car_attachments" WHERE "car_attachments"."car_id" = $1 ORDER BY "car_attachments"."id" ASC LIMIT $2 [["car_id", 21], ["LIMIT", 1]]
CACHE (0.0ms) SELECT "car_attachments".* FROM "car_attachments" WHERE "car_attachments"."car_id" = $1 ORDER BY "car_attachments"."id" ASC LIMIT $2 [["car_id", 21], ["LIMIT", 1]]
CarAttachment Load (0.5ms) SELECT "car_attachments".* FROM "car_attachments" WHERE "car_attachments"."car_id" = $1 ORDER BY "car_attachments"."id" ASC LIMIT $2 [["car_id", 20], ["LIMIT", 1]]
CACHE (0.0ms) SELECT "car_attachments".* FROM "car_attachments" WHERE "car_attachments"."car_id" = $1 ORDER BY "car_attachments"."id" ASC LIMIT $2 [["car_id", 20], ["LIMIT", 1]]
If you want to search for multiple values in a single column for example
params[:colour_collection] = ['red','green','blue']
Then you would expect your query to look like this
SELECT * FROM cars c
INNER JOIN colour_collections s
WHERE s.name IN ('red','green','blue');
In this case the corresponding ActiveRecord statement would look like this
Car.
joins(:colour_collections).
where(colour_collections: { name: params[:colour_collection] })
Rails 5 comes with an or method but Rails 4 does not have the or method, so you can use plain SQL query in Rails 4.
In Rails 4 :
#cars = car.
joins(:colour_collections).
where("colour_collections.name = ? or colour_collections.type = ?", params[:colour_collection], params[:type])
In Rails 5 :
#cars = car.
joins(:colour_collections).
where("colour_collections.name = ?", params[:colour_collection]).or(car.joins(:colour_collections).where("colour_collections.type = ?", params[:type]))
Depending on whether you want to use OR or AND. There are multiple ways of achieving this but simple example is
Article.where(trashed: true).where(trashed: false)
the sql generated will be
SELECT * FROM articles WHERE 'trashed' = 1 AND 'trashed' = 0
Foo.where(foo: 'bar').or.where(bar: 'bar') This is norm in Rails 5 or simply
Foo.where('foo= ? OR bar= ?', 'bar', 'bar')
#cars = car.joins(:colour_collections).where("colour_collections.name = ?", params[:colour_collection]).where("cars.make = ?", params[:make])
More discussion on chaining How does Rails ActiveRecord chain "where" clauses without multiple queries?

How to lock a parent record in Rails/ActiveRecord?

When there is a single parent record associated with multiple child records, using row locking on the parent record is an obvious way to ensure consistency. However, I cannot seem to find a clean way to do this in ActiveRecord.
For example, say we have two models: Order and OrderProduct.
class Order < ActiveRecord::Base
has_many :order_products
...
end
class OrderProduct < ActiveRecord::Base
belongs_to :order
...
end
Updating an OrderProduct affects the overall state of the Order, so we want to make sure only one transaction is updating an Order at any given time.
If we're trying to achieve this when editing an OrderProduct, the cleanest way in ruby I can see is:
def edit
product = OrderProduct.find params[:id]
Order.transaction do
product.order.lock!
# Make sure no changes have occurred while we were waiting for the lock
product.reload
# Do stuff...
product.order.some_method
end
end
However this if rather inefficient with SQL queries, producing:
SELECT "order_products".* FROM "order_products" WHERE "order_products"."id" = $1 LIMIT 1 [["id", "2"]]
SELECT "orders".* FROM "orders" WHERE "orders"."id" = 2 LIMIT 1
SELECT "orders".* FROM "orders" WHERE "orders"."id" = $1 LIMIT 1 FOR UPDATE [["id", 2]]
SELECT "order_products".* FROM "order_products" WHERE "order_products"."id" = $1 LIMIT 1 [["id", 2]]
SELECT "orders".* FROM "orders" WHERE "orders"."id" = 2 LIMIT 1
We can reduce the number of queries by changing the to something along the lines of:
def edit
product = OrderProduct.find params[:id]
Order.transaction do
order = Order.find product.order_id, lock: true
# Make sure no changes have occurred while we were waiting for the lock
product.reload
# Cache the association
product.order = order
# Do stuff...
product.order.some_method
end
end
which produces better SQL:
SELECT "order_products".* FROM "order_products" WHERE "order_products"."id" = $1 LIMIT 1 [["id", "2"]]
SELECT "orders".* FROM "orders" WHERE "orders"."id" = 2 LIMIT 1 FOR UPDATE [["id", 2]]
SELECT "order_products".* FROM "order_products" WHERE "order_products"."id" = $1 LIMIT 1 [["id", 2]]
However the code is messier.
Is there a cleaner way of doing this with ActiveRecord? Calling product.order = order just to get the association cached seems a little dangerous.
For a simple Rails way of locking, check out http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html
.lock.load
Is what you are looking for.
probably?

Adding a scope to a has_many through association in Rails

I have a Project and User models joined by a Membership model. I want to retrieve a project's members except one user.
project.members.where.not(id: current_user)
Like this answer, I want to use a scope:
class User < ActiveRecord::Base
scope :except, ->(user) { where.not(id: user) }
end
But this doesn't work.
p.members.except(User.find(1))
User Load (1.0ms)
SELECT "users".* FROM "users"
WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
User Load (0.4ms)
SELECT "users".* FROM "users"
INNER JOIN "memberships" ON "users"."id" = "memberships"."user_id"
WHERE "memberships"."project_id" = $1 [["project_id", 2]]
As you can see this results in two queries, not one. And returns all the members, not taking into account the exclusion.
Why doesn't this work?
Try renaming the scope to something else, like except_for or all_except. Name except is already used by active_record
http://api.rubyonrails.org/classes/ActiveRecord/SpawnMethods.html#method-i-except
Also you get 2 queries because you are doing User.find(1) which results in the first query.

Distinct Records with joins and order

I have a simple relationship between User and Donations in that a user has many donations, and a donation belongs to a user. What I'd like to do is get a list of users, ordered by the most recent donations.
Here's what I'm trying:
First I want to get the total number of uniq users, which is working as expected:
> User.joins(:donations).order('donations.created_at').uniq.count
(3.2ms) SELECT DISTINCT COUNT(DISTINCT "users"."id") FROM "users" INNER JOIN "donations" ON "donations"."user_id" = "users"."id"
=> 384
Next, when I remove the count method, I get an error that "ORDER BY expressions must appear in select list":
> User.joins(:donations).order('donations.created_at').uniq
User Load (0.9ms) SELECT DISTINCT "users".* FROM "users" INNER JOIN "donations" ON "donations"."user_id" = "users"."id" ORDER BY donations.created_at
PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
LINE 1: ...ON "donations"."user_id" = "users"."id" ORDER BY donations....
Then I tried fixing the Postgres error by explicitly setting the SELECT clause which at first glance appears to work:
> User.select('DISTINCT "users".id, "users".*, "donations".created_at').joins(:donations).order('donations.created_at')
User Load (17.6ms) SELECT DISTINCT "users".id, "users".*, "donations".created_at FROM "users" INNER JOIN "donations" ON "donations"."user_id" = "users"."id" ORDER BY donations.created_at
However, the number of records returned does not take into account the DISTINCT statement and returns 692 records:
> _.size
=> 692
How do I get the expected number of results (384) while also sorting by the donation's created_at timestamp?
Try this:
User.select('users.*,MAX(donations.created_at) as most_recent_donation').
joins(:donations).order('most_recent_donation desc').group('users.id')
I suppose an user has many donations, this would select the most recent created donation and would select distinct users filtering by their id.
I have not tested this though.

Rails 3.1: Optimizing many_to_many through join table in single query?

User has many Tracks, through Favorite. Favorite has some extra per-user meta-data about the related track, and the whole thing is returned as a json blob using custom :as_public hashing method.
Even though I'm accessing the related objects using a JOIN, I'm making hundreds of very basic SELECT track FROM tracks WHERE track.id='1' queries. I want to optimize this lookup.
users_controller:
def show
#user = User.find(params[:id])
respond_to do |format|
format.html # index.html.erb
format.json { render json: #user.to_json(:methods => [:favorites_as_public_tracks]) }
end
end
user.rb
def favorites_as_public_tracks
favorites.joins(:track).sort_by(&:created_at).map(&:as_public_track)
end
favorite.rb
class Favorite < ActiveRecord::Base
belongs_to :user
belongs_to :track
#Grabs some stuff from Favorite, merging it with the public data from Track
def as_public_track
track.public_attributes.merge(public_attributes_for_merging_onto_track)
end
# This stuff gets added onto track.to_json and used by javascript
def public_attributes_for_merging_onto_track
return {
:favorite_id => id,
:from_service => from_service,
:favorited_at => created_at,
:collection_name => "#{collection_name}, #{from_service}"
}
end
def public_attributes
private_attrs = [:user_id]
attributes.reject {|key, val| private_attrs.include? key.to_sym }
end
end
track.rb
def public_attributes
private_attrs = [] #[:id]
attributes.reject {|key, val| private_attrs.include? key.to_sym }
end
The SQL that gets run when I access the user's favorites as public tracks is:
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", "1"]]
Favorite Load (7.8ms) SELECT "favorites".* FROM "favorites" INNER JOIN "tracks" ON "tracks"."id" = "favorites"."track_id" WHERE "favorites"."user_id" = 1 ORDER BY "favorites".created_at DESC
Track Load (1.3ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 1 LIMIT 1
Track Load (0.5ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 2 LIMIT 1
Track Load (0.3ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 3 LIMIT 1
Track Load (0.3ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 4 LIMIT 1
Track Load (0.5ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 5 LIMIT 1
Track Load (0.2ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 6 LIMIT 1
Track Load (0.2ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 7 LIMIT 1
Track Load (0.2ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 8 LIMIT 1
How do I do this without making hundreds of SELECT track where track.id='...' queries?
Thanks!
If you have a list that stays the same for all instances, consider making it a class method rather than an instance method. See if you can make the public attributes and private attributes lists as class methods. Otherwise, to build these lists for each instance, you will get a hit for each record.
Usually, :include :tracks in the finds can fix it, but I don't think that is the issue here.
Good luck!

Resources