Better Performance on Associations - ruby-on-rails

Right now I have a table called Campaigns that has many Hits, if I call say:
Campaign.find(30).hits
Which takes 4 seconds, or 4213 ms.
If I call this instead:
campaign = Campaign.find(30)
campaign.hits.count
Does it still load all of the hits, then count? Or does it see I am counting and avoids loading all of the hits? (Which is currently 300,000+ rows).
I am trying to figure out a smart way to load/count my hits. I am thinking about adding a method to my Campaign.rb model, like:
def self.total_hits
find :first, :select => 'COUNT(id) as hits', :conditions => ["campaign_id = ?", self.id]
end
I know that query won't load from the hits table, but that is just an example of counting it from a self made query, apposed to Ruby on Rails doing this for me.
Would this memcache query be more effecient? (I have it running, but doesn't seem to be any better/faster/slower, just the same speed.)
def self.hits
Rails.cache.fetch("Campaign_Hits_#{self.campaign_id}", :expires_in => 40) {
find(:first, :select => 'COUNT(id) as hits', :conditions => ["campaign_id = ?", self.campaign_id]).hits
}
end
Any suggestions would be great!

How about:
Campaign.find(30).hits.count
You might also consider adding the following in hit.rb (assuming a one-to-many relationship between campaigns and hits).
belongs_to :campaign, :counter_cache => true
You then need a column in the campaigns table called hits_count. This will avoid hitting hits altogether if you're only getting the count.
You can check the API for the full rundown.

My ActiveRecord might be a little rusty, so forgive me if so, but IIRC Campaign.find(30).hits is at least two separate queries. How does Campaign.find(30, :include => [ :hits ]).hits do? That should perform a single query.

Related

Perform find on an array of hashes to avoid database find

The find methods are very convenient for retrieving records, and I'm frequently using :include to prefetch referenced records to avoid expensive db accesses.
I have a case where I retrieve all sales by a salesperson.
#sales = Sales.find(:all,
:include => [:salesperson, :customer, :batch, :product],
:conditions => {:salesperson_id => someone},
:order => :customer_id)
I then want to slice and dice the returned records based on what was returned. For instance, I want to produce a report for all the sales made by this salesperson at a particular store, which we know is a subset of the previously returned data.
What I'd like to do is,
#storeSales = #sales.find_by_store(store_id)
...and retrieve this subset from the array held in memory as a new array, rather than achieve the same thing by performing a find on the database again. After all, #sales is just a array of Sales objects, so it doesn't seem unreasonable that this should be supported.
However, it doesn't seem that there's a convenient way to do this, is there? Thanks.
If you are using Rails 3, #sales will be an AREL criteria object. What you can do is as follows:
#sales = Sales.find(:all,
:include => [:salesperson, :customer, :batch, :product],
:conditions => {:salesperson_id => someone},
:order => :customer_id)**.all**
Now #sales is an instance of an Array of Sales model objects. Getting a subset of the array objects is now easy using the select method:
#my_product_sales = #sales.select { |s| s.product == my_product_criteria }
Upon using select method you will now have #sales being the full result set and #my_product_sales being the subset based on the collect criteria.

Rails 3 select random follower query efficiency

I have a method that selects 5 random users who are following a certain user, and adds them to an array.
Relationship.find_all_by_followee_id( user.id ).shuffle[0,4].each do |follower|
follower = User.find(follower.user_id)
array.push follower
end
return array
I'm wondering, is this an efficient way of accomplishing this? My main concern is with the find_all_by_followee_id call. This returns a list of all the relationships where the specified user is being followed (this could be in the 100,000s). And then I shuffle that entire list, and then I trim it to the first 5. Is there a more efficient way to do this?
You can try this:
Relationship.find_all_by_followee_id( user.id, :order => 'rand()', :limit => 5 ) do |follower|
follower = User.find(follower.user_id)
array.push follower
end
return array
Btw, this will work with MySql. If you are using PostgreSQL or anything else you may need to change the rand() with any valid random function that your DB supports.
Some minor changes to make it a little more clean:
return Relationship.find_all_by_followee_id( user.id, :order => 'rand()', :limit => 5 ).collect {|follower| User.find(follower.user_id) }
You can also use a join in there in order to prevent the 5 selects but it won't make much difference.
Edit1:
As #mike.surowiec mentioned.
"Just for everyones benefit, translating this to the non-deprecated active record query syntax looks like this:"
Relationship.where(:followee_id => user.id).order( "random()" ).limit( 5 ).collect {|follower| User.find(follower.user_id) }

Rails 3 refuses to eager load

I'm working in Rails 3.0.7.
I have some many-to-many and some one-to-many associations that fail to eager load.
My associations are:
Person has_many :friends
Person has_many :locations through=> :location_histories
Location belongs_to :location_hour
Location_hour has_many :locations
In my controller I have the following:
#people = Person.includes([[:locations=>:location_hour],:friends]).where("(location_histories.current = true) AND (people.name LIKE ? OR friends.first_name LIKE ? OR friends.last_name LIKE ? OR (friends.first_name LIKE ? AND friends.last_name LIKE ?))").limit(10).all
Then in my view I have:
<% #people.each do |person| %>
<tr>
<td><%= link_to person.name, person %></td>
<td><%= link_to person.friends.collect {|s| [s.full_name]}.join(", "), person.friends.first %></td>
<td><%= link_to person.locations.current.first.name, person.locations.current.first %></td>
</tr>
<% end %>
locations.current is a scope defined as:
scope :current, lambda {
where("location_histories.current = ?", true)
}
This works as expected and first generates 2 database calls: one to get a list of person ids and then a big database call where everything is properly joined. THE PROBLEM is that after that there are n database calls along the lines of:
SELECT 'friends'.* from 'friends' WHERE ('friends'.person_id = 12345)
So for each iteration of the loop in the view. Needless to say this takes a while.
I thought that .all would force eager loading. Anyone have an idea what's going on here?
This spends over 3 seconds on ActiveRecord. Way too long.
I would greatly appreciate any and all suggestions.
Thanks.
OK. Finally Solved.
I needed to call both joins and includes. I've also had to remove the :locations_current association from the join. It was creating some chaos by attempting to
... LEFT OUTER JOIN `locations` ON `location_histories`.current = 1 ...
Which of course is not a valid association. It seems that the 'current' clause was being carried over into the JOINS.
So now I have the following, which works.
#people = Person.joins([[:locations=>:location_hour],:friends]).includes([[:locations=>:location_hour],:friends]).where("(location_histories.current = true) AND (people.name LIKE ? OR friends.first_name LIKE ? OR friends.last_name LIKE ? OR (friends.first_name LIKE ? AND friends.last_name LIKE ?))")
IN SUMMARY:
1) I needed to use both Joins and Includes for eager loading and proper interpretation of the .while() conditions
2) I needed to keep associations with conditions (i.e. :current_locations) out of the joins clause.
Please correct me if this seems like a glaring mistake to you. It seems to work though. This brings down the Active Record time to just under 1 sec.
Is it common to combine joins and includes?
Thanks!
I've figured out PART of the problem (though there is still some unintended behavior).
I had several scopes such as locations.current, defined as indicated above.
I have moved this logic to the association. So in my Person model I now have
has_many :current_locations, :source => :location, :through => :location_histories, :conditions => ["`location_histories`.current = ?", true]
And I call
Person.current_locations.first
instead of
Person.locations.current.first.
So now the includes do eager load as expected.
The problem is that this messed up the search. For some reason now everything seems to hang when I include a where clause. Things first get dramatically slower with each table that I add to the include and by the time I've included all the necessary tables it just hangs. No errors.
I do know this: when I add symbols to the where clause Rails does an outer join during the query (as explained here), which is what's expected. But why does this cause the whole thing to collapse?
(A minor problem here is that I need string comparisons. In order to get a proper join I call .where as
.where(:table =>{:column=> 'string'})
which is equivalent to
table.column = 'string'
in SQL but I need
table.column LIKE '%string%'
Oddly, for me, I get the following behavior:
# fails to eager load tags--it only loads one of the tags for me, instead of all of them.
Product.find(:all, :conditions => ["products_tags.tag_id IN (?)", 2], :include => [:tags])
but this succeeds
Product.find(:all, :conditions => ["products_tags.tag_id IN (?)", 2], :include => [:tags], :joins => [:tags])
It's like the query on the inner join table is messing up the eager loading somehow. So your answer may be right. But there may be something odd going on here. (Rails 2.3.8 here).

Is it possible to delete_all with inner join conditions?

I need to delete a lot of records at once and I need to do so based on a condition in another model that is related by a "belongs_to" relationship. I know I can loop through each checking for the condition, but this takes forever with my large record set because for each "belongs_to" it makes a separate query.
Here is an example. I have a "Product" model that "belongs_to" an "Artist" and lets say that artist has a property "is_disabled".
If I want to delete all products that belong to disabled artists, I would like to be able to do something like:
Product.delete_all(:joins => :artist, :conditions => ["artists.is_disabled = ?", true])
Is this possible? I have done this directly in SQL before, but not sure if it is possible to do through rails.
The problem is that delete_all discards all the join information (and rightly so). What you want to do is capture that as an inner select.
If you're using Rails 3 you can create a scope that will give you what you want:
class Product < ActiveRecord::Base
scope :with_disabled_artist, lambda {
where("product_id IN (#{select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})")
}
end
You query call then becomes
Product.with_disabled_artist.delete_all
You can also use the same query inline but that's not very elegant (or self-documenting):
Product.where("product_id IN (#{Product.select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})").delete_all
In Rails 4 (I tested on 4.2) you can almost do how OP originally wanted
Application.joins(:vacancy).where(vacancies: {status: 'draft'}).delete_all
will give
DELETE FROM `applications` WHERE `applications`.`id` IN (SELECT id FROM (SELECT `applications`.`id` FROM `applications` INNER JOIN `vacancies` ON `vacancies`.`id` = `applications`.`vacancy_id` WHERE `vacancies`.`status` = 'draft') __active_record_temp)
If you are using Rails 2 you can't do the above. An alternative is to use a joins clause in a find method and call delete on each item.
TellerLocationWidget.find(:all, :joins => [:widget, :teller_location],
:conditions => {:widgets => {:alt_id => params['alt_id']},
:retailer_locations => {:id => #teller_location.id}}).each do |loc|
loc.delete
end

Rails - Find results from two join tables

I have have 3 Tables of data and 2 Join Tables connecting everything. I'm trying to figure out a way to query the results based on the condition that the join table data is the same.
To explain, I have User, Interest, and Event Tables. These tables are linked through an HABTM relationship (which is fine for my needs since I dont need to store any other fields) and joined through two join tables. So i also have a UsersInterests table with (user_id, interest_id) and a EventsInterests table with (event_id, interest_id).
The problem comes when trying to query all the Events where the users interests match the events interests.
I thought it would look something like this...
#events= Event.find(:all, :conditions => [#user.interests = #event.interests])
but I get the error
"undefined method `interests' for nil:NilClass", Is there something wrong with my syntax or my logic?
You're problem is that either #user or #event is undefined. Even if you define them, before executing this statement, the conditions option supplied is invalid, [#user.interests = #event.interests].
This named scope on events should do the trick
class Event < ActiveRecord::Base
...
named_scope :shares_interest_with_user, lambda {|user|
{ :joins => "LEFT JOIN events_interests ei ON ei.event_id = events.id " +
"LEFT JOIN users_intersets ui ON ui.interest_id = ei.interest_id",
:conditions => ["ui.user_id = ?", user], :group_by => "events.id"
}
end
#events = Event.shares_interest_with_user(#user)
Given Event <-> Interest <-> User query all the Events where the users interests match the events interests (so the following will find all such Events that this event's interest are also interests of at least one user).
First try, the simplest thing that could work:
#events = []
Interest.all.each do |i|
i.events.each do |e|
#events << e if i.users.any?
end
end
#events.uniq!
Highly inefficient, very resource hungry and cpu intensive. Generates lots of sql queries. But gets the job done.
Second try should incorporate some complicated join, but the more I think about it the more I see how vague your problem is. Be more precise.
Not sure I completely follow what you are trying to do. If you have one user and you want all events that that user also has interest in then something like:
Event.find(:all, :include => [:events_interests], :conditions => ['events_interests.interest_id in (?)', #user.interests.collect(&:id)])
should probably work.

Resources