optimize sql query rails - sql

On posts index page I list all posts this way:
posts_controller.rb
def index
#posts = Post.includes(:comments).paginate(:page => params[:page]).order("created_at DESC")
end
index.html.erb
<%= render #posts %>
_post.html.erb
<%= gravatar_for post.user, size:20 %>
<%= link_to "#{post.title}", post_path(post) %>
<%= time_ago_in_words(post.created_at) %>
<%= post.comments.count %>
<%= post.category.name if post.category %>
35 posts per page
When I first load the page in dev env,
rack-mini-profiler shows this time: 1441.1 ms
after a few reloads: ~700 ms
Can I somehow decrease this time and number of sql requests?
Here're rmp images if it helps:

You could decrease the number of sql queries by:
including user as well as comments, since you seem to be using that when displaying the gravatar
changing post.comments.count to post.comments.size
While size, count, length are synonymous for arrays, for active record relations or associations they are not the same:
length loads the association (unless it is already loaded) and returns the length of the array
count does a select count(*) query whether the association is loaded or not
size uses length if the association is loaded and count if not.
In your case the comments association is loaded, but because you are using count, it's not actually used

Further, you don't actually seem to be using the comments collection for anything other than printing the number of records. If that's indeed the case, use counter_cache (4.1.2.3) instead of querying for the comments (the number of comments will be available in the parent record Post).
Also consider a client side alternative to time_ago_in_words. It will also help if you later decide to cache the entire section/page.
And finally retrieve only the fields you're going to use. In this case, I can imagine the Post contains a large amount of text for the content and it's not used anywhere (but still needs to be transmitted from the DB).

Adding an index on the reference column (comments in your case) might help.
add_index :posts, :comment_id

Related

ActiveRecord Joins

Ok, so, if I do a User.joins(:session_users), I only get the attributes of users table.
How do I get the attributes of both tables in ActiveRecord way, i.e., not SQL?
EDIT ONE
Ok, based on the first answer, I'm trying to have it displayed.
So, this is the method written in Users Controller
def blah
#users = User.includes(:session_users)
#users.each do |user|
user.session_users
end
end
Then I have this in the users view blah.html.erb
<%= #users.session_users %>
And this in the routing section:
match "/users/blah" => "users#blah"
I think you want includes instead of joins. See http://railscasts.com/episodes/181-include-vs-joins for more info. This should fetch columns for both,
users = User.includes(:session_users)
users.each do |user|
user.session_users
end
Note, this still performs 2 SQL queries.
Edit
Updated answer assumes that a user has_many :session_users
Routes:
# config/routes.rb
get '/users/blah' => 'users#blah'
Controller:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def blah
#users = User.includes(:session_users)
end
end
View:
# app/views/users/blah.html.erb
<% #users.each do |user| %>
<%= user.name %> // assumes user has a name attribute
<% user.session_users.each do |session_user| %>
<%= session_user.attributes %> // prints all the attributes
<%= session_user.created_at %> // assumes the user has a created_at attribute
<% end %>
<% end %>
If you really need to add fields from a joined table to the yielded objects, you can add select:
User.joins("INNER JOIN stolen_passwords
ON users.social_security_number=stolen_passwords.ssn")
.select("*").find_each do |user|
logger.debug {
"A #{user.full_name} has a password hash #{user.password_hash}"
}
end
Here imaginary user.full_name is an instance method of User and user.password_hash comes from the stolen_passwords table. You could also limit the queried/returned fields by listing them in the call to select explicitly.
We sometimes use this in rake tasks that enrich the database from or compare it against third party data sources: we would join our tables with provided tables and generate a 'side-by-side' CSV with columns from both. Plain SQL would work just as well, but going via active record often allows to reuse familiar scopes, or methods that perform calculations in ruby.
Caveat
Unfortunately the fields coming from the joined tables will not be cast to appropriate ruby types, they will all be strings (which is especially logical if using SQL string for the joins). But it is easy to cast the joined attributes with something like:
module CastJoinedColumns
def cast_joined_columns joined_record
columns_hash.each do |column_name, column|
if joined_record.attributes.include?(column_name)
joined_record[column_name] = column.type_cast(joined_record[column_name])
end
end
end
end
This module is meant to be extended into a model appearing on the right side of the join and the method be called with a joined record. It might misbehave and should be improved for the cases where the same column name appears in multiple tables, but is an ok starting point (works perfectly for us with third party data sources using column names guaranteed not to clash with our own).

How can I create an array of objects from a list of associated checkboxes?

First of all, I've done a fair amount of looking around, and while questions get around answers, I have a problem I think is somewhat unique. I have a list of checkboxes generated with the following code:
<% for student in Student.find(:all) %>
<div>
<%= check_box_tag "user[student_ids][]", student.id, current_user.students.include (student) %>
<%= student.name %>
</div>
<% end %>
After clicking the 'update' button at the bottom, I need each of the checked boxes to be placed into an array. I then plan on iterating over the array and doing some work on each of the checked names. I am having a hard time, however, with the process of getting these names all into an array. I really am not sure which of the standard web actions this kind of work should be (i.e, post, get, etc.), so I don't know how to set up a route. Even if I could set up a route to a controller, how would I get the checked students into an array of Student objects?
Thanks ahead of time for your help!
The full answer to your question depends on a variety of things, for example, what you are trying to do with the submitted array, etc (which would determine whether POST, GET, PUT or DELETE should be used.) Without knowing more information with respect to your code base, if you throw the following code into a form_for in one of your controller's already restful routes, you should be able to see the array of checked names:
<%= current_user.students.include(student).each do |student| %>
<div>
<%= check_box_tag "student_names[]", student.name %> <%= label_tag student.name %>
</div>
<% end %>
Then, when the user hits submit, the params hash will show student_names = [].
And make sure your attributes are accessible as needed.
On a side note, check out Railscasts pro episode from last week. Pretty much exactly explains what you are trying to do. It's a subscription service, though.
I managed to solve my problem in a less-than-satisfying way. Here is the code I ended up using:
current_user.students.delete_all
if(params.has_key? :user)
params[:user][:student_ids].each do |i|
current_user.students<<(Student.find(i))
end
end
Because the number of students I'm managing is not ever larger than 100, this operation isn't as bad as it looks. I'm deleting all of the associations already present, and then cycling through all passed parameters. I then find the student object with the passed parameter id and add it to the current_user's User-Student join table.
I hope this helps someone down the line!

Ordering records when using has_and_belongs_to_many relation in Rails 3?

I am, for the first time, trying to use a HABTM relationship in my Rails application. The following image shows the models and their relations to each other:
Currently I am displaying all members within a region with the following:
<% #region.members.each do |member| %>
<%= link_to member.name, member %>
<% end %>
I am trying to sort/order the list of members by their respective level. I then would like to have the members ordered in descending alphabetical order.
So, the members_controller code is currently the default:
#members = Member.all
I can order the results into alphabetical order:
#members = Member.order("name DESC").all
but I can't figure out how to use a related model's data to order the member records. There are likely going to be three levels; Charity, Basic and Subscription. I am planning on having a block at the top of the page which only shows subscription members. The subscription members should not then be shown again on the page.
#subscribed_members = Member.where(:level == 1).order("name DESC").all
but, as you can see, I have no idea how to filter that block by level.
Can anyone point me in the right direction, or maybe to a tutorial for this? I've read through the Rails guide for HABTM relationships but it only seems to cover the models.
EDIT
After trying the suggestion, I now have the following:
#members = Member.all
#subscribedmembers = Member.include(:levels)
.where("levels.name == 'subscriber'")
How do I use that method in the regions view?
<% #region.subscribedmembers.each do |member| %>
<%= member.name %>
<% end %>
That won't work because it's looking for a related model called subscrivedmembers which doesn't exist.
For this kind of task, i recommend you to take a look at joining models with ActiveRecord. The methods are include and joins. Lemme demonstrate with a piece of code:
#subscribed_members = Member.include(:levels)
.where("members.url == 'test_url'")
.order("levels.name DESC")
If you check your console after this query is run, you can see the include makes a SQL join with the two tables. Here i am assuming member :has_many :levels, so you include the :levels table and use a prefixed column name on your order clause.
It is not that hard once you get the idea, so i encourage you to try these two methods on the console and check the results.
EDIT
Create a scope with you query code, then use it on any place. Example:
# On your model
scope :ultimate_level, include(:levels).where("name = 'test'").order("levels.name DESC")
# On your controller
#subscribed = Member.ultimate_level
# On your view
<% #subscribed.each do |s| %>
You can create different scopes too, each one that makes a single operation, and then reuse on our controllers, etc.
Obs.: Check the syntax, i didn't test the code myself.

How to add column filter options in view (doing it "the rails way")

I search for a working solution for a rather simple problem, but could not find a good explanation.
What I currently have (working) is an index view which contains:
a form to enter a new element and
a paginated list of existing elements (using will_paginate).
For the list I am interested in only part of the data, thus I am trying to add a form with filter options and I would like to store the forms content in a cookie (which should be replaced with an per user object stored in the database, but i do not have users yet). What I cannot figure out is how to get the values from the form stored in a cookie (and vice versa) and how to use it together with will_paginated.
What I currently tried to do as a first step is to create an #filter object in my controller and adding the filter form for this object, setting the form options to use the index controller again. This leads to selected filter parameters passed in the params hash to the index controller (visible in the url). But this solution has some drawbacks. first the filters are gone as soon as I change the view (e.g. by creating a new element) and second the #filter object should be the cookie instead.
Here is the code I have so far:
View-partial for filter:
<%= form_for(#filter, :url => {:action => "index"}, :html => {:method => :get}) do |f| %>
<div class="field">
<%= f.label :german %><br />
<%= f.check_box :german %>
</div>
<div class="actions">
<%= f.submit "Filter" %>
</div>
<% end %>
Controller:
def index
#word = Word.new
#filter = Word.new(params[:word])
#words = Word.paginate(:page => params[:page]).order('word')
# ....
Can anybody help me? How is such a functionality (filtering results) done in other applications?
So the answer to the question is, use a where clause to include only the matching records in your result.
#words = Word
.where("german = ?", params[:word][:german] != 0")
.order('word')
.paginate(:page => params[:page])
This is a new Rails syntax called Active Relation (AREL for short), which should generally replace the older find and find_by methods. It has several benefits that can improve performance, notably that the SQL it executes (which you can see in your logs) only occurs when it is referenced, not when it is declared. This give you neat ways of defining partial relations (even as named scopes) that you can build up to create simpler statements that combine together.
The order of the various clauses doesn't matter -- AREL will generate the same SQL, but generally I like to follow the order of the underlying SQL,
where
joins
group
order
limit
offset
(limit and offset are handled in your case by the pagination tool).

Does MongoID do a separate query for .count(true)?

I have a ruby on rails 3 project in which I query for a certain number of objects by using a .limit(3) . Then, in my view, I loop through these objects. After that, if there are 3 objects in the view, I display a "load more" button. Here is the view code:
<% #objects.each do |object| %>
<%= render object._type.pluralize.underscore + '/teaser', :object => object %>
<% end %>
<% if #objects.size(true) == 3 %>
#load more link here
<% end %>
The size(true) is passed a boolean to ensure that mongoID takes into account the .limit and .offset on my query (otherwise it returns the total number of objects that matched, regardless of the limit / offset). Here are the relevant development log lines:
MONGODB project_development['system.indexes'].insert([{:name=>"_public_id_1", :ns=>"project_development.objects", :key=>{"_public_id"=>1}, :unique=>true}])
MONGODB project_development['objects'].find({:deleted_at=>{"$exists"=>false}}).limit(3).sort([[:created_at, :desc]])
#some rendering of views
MONGODB project_development['system.indexes'].insert([{:name=>"_public_id_1", :ns=>"project_development.objects", :key=>{"_public_id"=>1}, :unique=>true}])
MONGODB project_development['$cmd'].find({"count"=>"objects", "query"=>{:deleted_at=>{"$exists"=>false}}, "limit"=>3, "fields"=>nil})
My question is: does MongoID do a separate query for my #objects.size(true)? I imagine the ['$cmd'] might indicate otherwise, but I'm not sure.
I don't think so, there was a pull request month ago to add aliases for :size, :length to :count to avoid re-running queries. You can check that.