Rails Optimization
Write code that makes it work first
Then (if needed) optimize
DB Indexes
class AddIndexesToTables < ActiveRecord::Migration
def change
add_index :authors_posts, :post_id
add_index :authors_posts, :author_id
add_index :content_references, :site_block_id
add_index :content_references, :post_id
add_index :posts, :category_id
add_index :posts, :author_id
end
end
Querying Tips
Vanilla Rails will take you a long way. But in every app, there are places where speed is a big deal. And when you find those places, reducing SQL calls is an easy place to start optimizing." -@justinweiss
source: https://blog.codeship.com/speed-up-activerecord/
Check Server Logs
Avoid this:
Processing by RestaurantsController#index as HTML
Restaurant Load (1.6ms) SELECT `restaurants`.* FROM `restaurants`
Review Load (1.2ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 1
Review Load (1.2ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 2
Review Load (1.1ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 3
Review Load (1.2ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 4
Review Load (1.2ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 5
Review Load (1.2ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 6
Review Load (1.2ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 7
Review Load (1.0ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 8
Review Load (1.0ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 9
Review Load (1.0ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 10
Go for this:
Restaurant Load (1.2ms) SELECT `restaurants`.* FROM `restaurants`
Review Load (3.0ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
@restaurants = Restaurant.all.includes(:reviews)
@restaurants = Restaurant.all
@restaurants.each do |thing|
"#{thing.review}"
n+1
- Stay away from n+1 with eager loading
- When you need optimization and don't care how you get your data, use more sql and less rails helpers
.find_by_sql()
.includes()
Using #blank? vs #empty? on an ActiveRecord Relation
User.where(screen_name: ['user1','user2']).blank?
behind the scenes:
1. Query the database for all user data
2. Load the users into an array
3. Check to see if the array size is zero
SELECT "users".* FROM "users" WHERE "users"."screen_name" IN ('user1','user2')
[<#User:0x007fbf6413c510>,<#User:0x007fbf65ab1c70>]
=> true
#blank?
source: http://hashrocket.com/blog/posts/rails-quick-tips-easy-activerecord-optimizations
#empty?
User.where(screen_name: ['user1','user2']).empty?
1. Query database for ONLY a count
SELECT COUNT(*) FROM "users" WHERE "users"."screen_name" IN ('user1','user2')
2. Check to see if the count is zero
=> true
behind the scenes:
So #blank? will load the entire array, then check to see if that array is empty. On the other hand, #empty? asks the db for a count, and checks to see if that number is zero.
#map vs #pluck on an ActiveRecord Relation
#map
User.where(email: ['jane@example.com', 'john@example.com']).map(&:screen_name)
behind the scenes:
1. Query the db for all user data
SELECT "users".* FROM "users" WHERE "users"."email" IN ('jane@example.com','john@example.com')
2. Load users into an array
[<#User:0x007fbf6413c510>,<#User:0x007fbf65ab1c70>]
3. Iterate over array to collect screen_names
['user1','user2']
#pluck
User.where(email: ['jane@example.com', 'john@example.com']).pluck(:screen_name)
behind the scenes:
1. Query the db for ONLY screen_names
SELECT "users"."screen_name" FROM "users"
WHERE "users"."email" IN ('jane@example.com','john@example.com')
2. Return those screen_names in an array
['user1','user2']
So #map will load an entire array, then iterate to collect the screen_names. Alternatively, #pluck asks the database for exactly what it needs and returns an array of just those items.
Caveat
Don't use #pluck if you're passing in a #where
emails = ['jane@example.com', 'john@example.com']
User.where(screen_name: User.where(email: emails).pluck(:screen_name)).empty?
(using #pluck with #where)
1. Query for just the emails
SELECT "users"."email" FROM "users" WHERE "users"."email" IN ('jane@example.com','john@example.com')
2. Query for the count
SELECT COUNT(*) FROM "users" WHERE "users"."screen_name" IN ('user1','user2')
3. Check if the count is zero
=> true
Instead use #select with a #where
emails = ['jane@example.com', 'john@example.com']
User.where(screen_name: User.where(email: emails).select(:screen_name)).empty?
1. Make one query for count with subquery
SELECT COUNT(*) FROM "users" WHERE "users"."screen_name" IN (
SELECT "users"."screen_name" FROM "users" WHERE "users"."email"
IN ('jane@example.com','john@example.com')
)
2. Check if the count is zero
=> true
Recap
1. Use #empty? or #any? instead of #blank or #present?
2. Never use #map on active record relations, use #pluck instead
3. If you're using #pluck to pass values to a #where, use #select instead
On ActiveRecord Associations:
Browser Caching
-Phil Karlton
ETags vs Last Modified/If Modified Since Headers
http://blog.pixelastic.com/
Cache Invalidation
Don't do it.
We have control over invalidating the client's cache. All we have to do is change the filename and the browser/server knows that the old file doesn't exist anymore and the new one should be downloaded.
Optimize All The Things
By mattspell
Optimize All The Things
- 1,198