I'm worried about the efficiency of this line in the controller of my Rails project
posts_list = Post.where(:title => params[:title])
If the number of "Posts" in the database grows, will the line become slow to execute? Is there any possible optimization?
It just fires this query,
select * from posts where title = params[:title]
You can index the title column in your migration file
add_index(:posts, :title)
Add index on title field can be a first approach :
class AddIndexTitleToPost < ActiveRecord::Migration
def change
add_index :posts, :title, uniq: false
end
end
And you can use find_each on your iteration for prevent your database growing
Post.where(title: params[:title]).find_each(batch_size: 10) do |post|
...
end
That all for applicative enhancement
Related
My latest migration runs very slowly (600 seconds) even though it is not doing much
I have a model that contains tags in a string format separated with commas like so :
Model.tags = "TAG1, TAG2, TAG3"
I want to create a new Tag model that has a has_and_belongs_to_many relation with my model
Here is the migration
def self.up
rename_column :pizzas, :tags, :tags_migration
create_table :tags do |t|
t.string :name
t.integer :count
t.timestamps
end
create_join_table :tags, :pizzas do |t|
t.index [:tag_id, :pizza_id]
end
Pizza.all.each do |pizza|
pizza_tags = pizza.tags_migration
unless pizza_tags.empty?
pizza_tags_array = pizza_tags.split(', ')
pizza_tags_array.each do |tag|
t = Tag.find_by(name: tag)
if t.nil?
t = Tag.new
t.name = tag
t.count = 1
else
t.count = t.count + 1
end
t.pizzas << pizza
t.save
pizza.tags << t
pizza.save
end
end
puts "pizza n" + pizza.id.to_s
end
end
I don't think this code is supposed to take this long ( i have around 2000 entries )
It looks like you may be holding quite a bit in memory (due to Pizza.all). A simple performance benefit would be to change Pizza.all.each to Pizza.find_each
A good point by #codenamev about find_each
You have n+1 query in Tag.find_by(name: tag)
And also here
-
t.pizzas << pizza
t.save
pizza.tags << t
pizza.save
You can run the whole thing in a transaction to commit all the changes at once (but it may cause locks)
I'm not sure about the implementation details but the code above could be cut in half as t.pizzas << pizza will assign both models. has_many ... :through association should handle this
Also, consider moving the update part outside of migration as it will lock the database for a while
I have a model named Awardunit. Awardunit have many Awardleaders. One Award unit can have one or many Award leaders.
If I get all the record or search and get a collection of records to a variable named awardunits how can I count the number of Awardleaders in all the units in this collection?
Here's what I did :
#leaders = 0
#awardunits.each do |unit|
#leaders = #leaders + unit.awardleaders.size
end
Again to count the disabled leaders I use this :
#disabledleaders = 0
#awardunits.each do |unit|
#disabledleaders = #disabledleaders + unit.awardleaders.where(disabled: true).size
end
If I use this, it will have to go through all the records every time the page loads. Isn't there a better way of doing this?
You can make counting the associations cheap by adding a counter cache:
class AwardUnit << ActiveRecord::Base
has_many :award_units
end
class AwardLeader << ActiveRecord::Base
belongs_to :award_unit, counter_cache: true
end
Now add a new column called award_leaders_count to your AwardUnit table in a new migration:
def change
add_column :award_units, :award_leaders_count, :integer, default: 0
AwardUnit.all.each do |unit|
AwardUnit.reset_counters(unit.id, :award_leaders)
end
end
Rails will now automatically cache the number of award_leaders for every AwardUnit and #my_award_unit.award_leaders.count will give you the count without running another database query.
By default, Rails counter_cache only works for all award_leaders. If you need to count only those award_leaders that have a condition, you will have to add your own counter_cache:
class AwardUnit << ActiveRecord::Base
has_many :award_units
end
class AwardLeader << ActiveRecord::Base
belongs_to :award_unit
scope :disabled, -> { where(disabled: true) }
after_save :update_counter_cache
after_destroy :update_counter_cache
def update_counter_cache
award_unit.update_attribute(:disabled_award_units_count, award_unit.award_leaders.disabled.count)
end
end
migration:
def change
add_column :award_units, :disabled_award_leaders_count, :integer, default: 0
AwardUnit.all.each do |unit|
unit.update_attribute(:disabled_award_units_count, unit.award_leaders.disabled.count)
end
end
Now, when you have an array of AwardUnits, getting their combined count of disabled award leaders is as simple as
#award_units = AwardUnit.limit(5).to_a # or a similar query
#award_units.inject(0){|sum,unit| sum + unit.disabled_award_leaders_count }
You can eager load Awardleaders when fetching Awardunits, so you don't have to execute a count query for every Awardunit, like this:
#awardunits = Awardunit.includes(:awardleaders).where('awardleaders.disabled = ?', true) # the rest of the query
Or, you can query the count directly like this:
#leaders = Awardleader.where('awardunit_id IN (?)', #awardunits.map(&:id)).where(:disabled => true).count
This will run Database query only once
Awardunit.includes(:awardleaders).map {|award_unit| award_unit.awardleaders.size}.inject(0){|sum,item| sum + item }
I can think of a few ways to do this, but I'm unsure what to choose..
I have the class Topic and I am trying to scope this so that I only return Topics if it has the associated object Reply or topic.replies as a count greater than 0.
Worst way to do this :
#topics.select{ | topic | topic.replies > 0 && topic.title == "Conversation" }
Ideally, I'd like to use a where scope.
scope = current_user.topics
scope = scope.joins 'left outer join users on topics.registration_id = registration_members.registration_id'
# scope = .. here I want to exclude any of these topics that have both the title "Conversations" and replies that are not greater than 0
I need to "append" these selections to anything else already selected. So my selection shouldn't exclude all others to just this selection. It's just saying that any Topic with replies less than one and also called "Conversation" should be excluded from the final return.
Any ideas?
Update
Sort of a half-hashed idea :
items_table = Arel::Table.new(scope)
unstarted_conversations = scope.select{|a| a.title == "Conversation" && a.replies.count > 0}.map(&:id)
scope.where(items_table[:id].not_in unstarted_conversations)
You can use something called count cache, basically what it does is add a field to the table and store in that field the total of "associates" of the specified type and is automatically updated.
Checkout this old screen/ascii cast: http://railscasts.com/episodes/23-counter-cache-column?view=asciicast
Here is something newer: http://hiteshrawal.blogspot.com/2011/12/rails-counter-cache.html
In your case would be as follow:
# migration
class AddCounterCacheToTopìc < ActiveRecord::Migration
def self.up
add_column :topics, :replies_count, :integer, :default => 0
end
def self.down
remove_column :topics, :replies_count
end
end
# model
class Replay < ActiveRecord::Base
belongs_to :topic, :counter_cache => true
end
Hope it help you.
I think it's safe to say everyone loves doing something like this in Rails:
Product.find(:all, :conditions => {:featured => true})
This will return all products where the attribute "featured" (which is a database column) is true. But let's say I have a method on Product like this:
def display_ready?
(self.photos.length > 0) && (File.exist?(self.file.path))
end
...and I want to find all products where that method returns true. I can think of several messy ways of doing it, but I think it's also safe to say we love Rails because most things are not messy.
I'd say it's a pretty common problem for me... I'd have to imagine that a good answer will help many people. Any non-messy ideas?
The only reliable way to filter these is the somewhat ugly method of retrieving all records and running them through a select:
display_ready_products = Product.all.select(&:display_ready?)
This is inefficient to the extreme especially if you have a large number of products which are probably not going to qualify.
The better way to do this is to have a counter cache for your photos, plus a flag set when your file is uploaded:
class Product < ActiveRecord::Base
has_many :photos
end
class Photo < ActiveRecord::Base
belongs_to :product, :counter_cache => true
end
You'll need to add a column to the Product table:
add_column :products, :photos_count, :default => 0
This will give you a column with the number of photos. There's a way to pre-populate these counters with the correct numbers at the start instead of zero, but there's no need to get into that here.
Add a column to record your file flag:
add_column :products, :file_exists, :boolean, :null => false, :default => false
Now trigger this when saving:
class Product < ActiveRecord::Base
before_save :assign_file_exists_flag
protected
def assign_file_exists_flag
self.file_exists = File.exist?(self.file.path)
end
end
Since these two attributes are rendered into database columns, you can now query on them directly:
Product.find(:all, :conditions => 'file_exists=1 AND photos_count>0')
You can clean that up by writing two named scopes that will encapsulate that behavior.
You need to do a two level select:
1) Select all possible rows from the database. This happens in the db.
2) Within Ruby, select the valid rows from all of the rows. Eg
possible_products = Product.find(:all, :conditions => {:featured => true})
products = possible_products.select{|p| p.display_ready?}
Added:
Or:
products = Product.find(:all, :conditions => {:featured => true}).select {|p|
p.display_ready?}
The second select is the select method of the Array object. Select is a very handy method, along with detect. (Detect comes from Enumerable and is mixed in with Array.)
I'm using Ruby on Rails and I'm storing business hours like this:
CREATE TABLE "business_hours" (
"id" integer NOT NULL PRIMARY KEY,
"business_id" integer NOT NULL FOREIGN KEY REFERENCES "businesses",
"day" integer NOT NULL,
"open_time" time,
"close_time" time)
(which came from the thread at:
Storing Business Hours in a Database )
Now I want to pull the hours out for each day of the week and display them, and I'm trying to find the best (or at least a good) way.
Should I just have a helper method that loops through getting the days (from 0..6) for a given business_id and assign it to a variable for the associated day? I feel like there must be a better way -- with an array, or something, but it's hurting my head thinking about it, because I also have a form of 'select's where any of the hours for a given business can be updated at once.
Thanks for any guidance!
Use the enum column plugin to declare the day field as a enum field.
class BusinessHours < ActiveRecord::Migration
def self.up
create_table :business_hours do |t|
t.integer :business_id, :null => false
t.enum :day, :limit =>[:sun, :mon, :tue, :wed, :thu, :fri, :sat], :nill => false
t.time :open_time, :null => false
t.time :close_time, :null => false
end
end
def self.down
drop_table :business_hours
end
end
Now when you do find on the BusinessHour model you will get the day as a string.
b = BusinessHour.find_by_business_id(2).first
p b.day.to_s.camelize #prints Sun/Mon/Tue etc.
You can use the enum_select and enum_radio form helpers to create list box/radio button group for the enum group:
Since the number of days in a week really is fixed, you can join the table 6 times (plus the original) and do a query for a single row. I'd probably just do a single query and loop through the rows though.
Have you considered serializing the business hours? Using serialization you are essentially storing objects in the database.
class BusinessHour < ActiveRecord::Base
serialize :hours
...
end
BusinessHour.create :business => #business, :hours =>
{:mon => [mon_start_time, mon_end_time], :wed => [wed_start_time, wed_end_time],
...}
Personally I would go with the bitwise approach described in linked question. All you really need to do to make it work is write new accessor methods.
It would be easier to find the business and use the associations to retrieve the business_hours rows.
Try this in your view
<% #business.business_hours.each do |hrs| %>
<%= hrs.day_name %>: Open-<%= hrs.open_time %> Close-<%= hrs.close_time %>
<%- end -%>
In your business_hour.rb model file, create a default scope to make sure the days are always listed in order. You can also create the day_name method to make it easier to display the day.
default_scope :order => 'day ASC'
def day_name
case self.day
when 0 then "Sun"
when 1 then "Mon"
...
end
end