一键导入
performance-optimization
Performance optimization patterns for kw-app - N+1 query prevention, eager loading, caching strategies, and database optimization.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Performance optimization patterns for kw-app - N+1 query prevention, eager loading, caching strategies, and database optimization.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
ActiveRecord model patterns and best practices for kw-app - validations, associations, scopes, queries, and when to keep models thin.
kw-app mandatory pattern for service objects using dry-monads Result monad with Success/Failure and do-notation. Replaces deprecated custom Result classes.
Zero-downtime deployment with Kamal for kw-app (staging on Raspberry Pi ARM64, production on VPS x86_64). Covers deployment workflows, console access, and troubleshooting.
Service object architecture for kw-app using dry-monads Result monad. Covers when to use services, structure patterns, testing, and integration with controllers.
kw-app testing standards using RSpec, FactoryBot, and Docker. Covers test types, patterns, and best practices for TDD workflow.
| name | performance-optimization |
| description | Performance optimization patterns for kw-app - N+1 query prevention, eager loading, caching strategies, and database optimization. |
| allowed-tools | Read, Write, Edit, Bash |
Common performance patterns for Rails applications:
# ❌ N+1 Problem - 1 query + N queries
users = User.all # 1 query
users.each do |user|
puts user.posts.count # N queries (one per user)
end
# Total: 1 + N queries
# ✅ Solution - 2 queries total
users = User.includes(:posts) # 1 query + 1 eager load
users.each do |user|
puts user.posts.count # No query (already loaded)
end
# Total: 2 queries
Bullet Gem (already in kw-app):
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.console = true
Bullet.rails_logger = true
end
Manual Detection:
# In Rails console
ActiveRecord::Base.logger = Logger.new(STDOUT)
# Then run your code and watch the queries
User.all.each { |u| u.posts.count }
# Loads associations with separate queries
users = User.includes(:posts, :comments)
# Generates:
# SELECT * FROM users
# SELECT * FROM posts WHERE user_id IN (1,2,3...)
# SELECT * FROM comments WHERE user_id IN (1,2,3...)
When to use:
# Always uses separate queries (even with conditions)
users = User.preload(:posts).where(posts: { published: true })
# Generates:
# SELECT * FROM users
# SELECT * FROM posts WHERE user_id IN (1,2,3...)
When to use:
# Uses LEFT OUTER JOIN
users = User.eager_load(:posts).where(posts: { published: true })
# Generates:
# SELECT users.*, posts.* FROM users
# LEFT OUTER JOIN posts ON posts.user_id = users.id
# WHERE posts.published = true
When to use:
# Only loads users, not posts
users = User.joins(:posts).where(posts: { published: true })
# Generates:
# SELECT users.* FROM users
# INNER JOIN posts ON posts.user_id = users.id
# WHERE posts.published = true
When to use:
# ❌ N+1
@users = User.all
# In view: @users.each { |u| u.profile.name }
# ✅ Fixed
@users = User.includes(:profile)
# ❌ N+1
@users = User.all
# In view: @users.each { |u| u.posts.each { |p| p.comments.count } }
# ✅ Fixed
@users = User.includes(posts: :comments)
# ❌ N+1
@users = User.all
# In view: user.profile, user.posts, user.settings
# ✅ Fixed
@users = User.includes(:profile, :posts, :settings)
# ❌ N+1
@comments = Comment.all
# In view: comment.commentable (User or Post)
# ✅ Fixed
@comments = Comment.includes(:commentable)
# ❌ N+1 (loads all posts to count)
@users = User.includes(:posts)
# In view: user.posts.count
# ✅ Better - counter cache
class User < ApplicationRecord
has_many :posts
end
class Post < ApplicationRecord
belongs_to :user, counter_cache: true
end
# Migration:
add_column :users, :posts_count, :integer, default: 0
# In view: user.posts_count (no query!)
# ✅ Index foreign keys
add_index :posts, :user_id
# ✅ Index fields used in WHERE
add_index :users, :email
add_index :posts, :published_at
# ✅ Index unique fields
add_index :users, :email, unique: true
# ✅ Composite index for multiple conditions
add_index :posts, [:user_id, :published_at]
# ✅ Index for sorting
add_index :posts, :created_at
# In Rails console
ActiveRecord::Base.connection.tables.each do |table|
puts "Table: #{table}"
columns = ActiveRecord::Base.connection.columns(table)
foreign_keys = columns.select { |c| c.name.end_with?('_id') }
foreign_keys.each do |fk|
indexed = ActiveRecord::Base.connection.indexes(table).any? { |i| i.columns.include?(fk.name) }
puts " ⚠️ Missing index: #{fk.name}" unless indexed
end
end
<!-- app/views/posts/show.html.erb -->
<% cache @post do %>
<h1><%= @post.title %></h1>
<p><%= @post.body %></p>
<% end %>
<!-- Cache key: posts/123-20240115120000000000/... -->
Auto-expires when:
@post.updated_at changes<!-- app/views/posts/index.html.erb -->
<%= render partial: 'post', collection: @posts, cached: true %>
<!-- Each post cached separately -->
<!-- app/views/posts/show.html.erb -->
<% cache @post do %>
<h1><%= @post.title %></h1>
<% cache [@post, 'comments'] do %>
<% @post.comments.each do |comment| %>
<% cache comment do %>
<%= render comment %>
<% end %>
<% end %>
<% end %>
<% end %>
touch: true to expire parent:
class Comment < ApplicationRecord
belongs_to :post, touch: true
end
# When comment updates, post.updated_at also updates
# This expires the post cache
# In model or service
class User
def expensive_calculation
Rails.cache.fetch("users/#{id}/calculation", expires_in: 1.hour) do
# Expensive operation here
perform_complex_calculation
end
end
end
# First call: runs calculation and caches
# Subsequent calls: returns cached value
# Expire specific cache
Rails.cache.delete("users/#{user.id}/calculation")
# Expire by pattern (Redis)
Rails.cache.delete_matched("users/*/calculation")
# Clear all cache
Rails.cache.clear
# ❌ Loads all columns
users = User.all
# ✅ Only needed columns
users = User.select(:id, :email, :name)
# ❌ Instantiates objects
user_ids = User.all.map(&:id)
# ✅ Returns array directly
user_ids = User.pluck(:id)
# ✅ Multiple columns
emails_and_names = User.pluck(:email, :name)
# => [["user1@example.com", "User 1"], ...]
# ❌ Loads all records at once
User.all.each do |user|
user.process
end
# ✅ Loads in batches (default 1000)
User.find_each do |user|
user.process
end
# ✅ Custom batch size
User.find_each(batch_size: 500) do |user|
user.process
end
# ✅ With conditions
User.where(active: true).find_each do |user|
user.process
end
# ❌ Loads all records
if User.where(email: email).any?
# ...
end
# ✅ Only checks existence
if User.where(email: email).exists?
# ...
end
# SQL: SELECT 1 FROM users WHERE email = '...' LIMIT 1
# ❌ N jobs for N users
users.each do |user|
SendEmailJob.perform_later(user.id)
end
# ✅ 1 job with batch
SendBatchEmailJob.perform_later(users.pluck(:id))
# SendBatchEmailJob
class SendBatchEmailJob < ApplicationJob
def perform(user_ids)
users = User.where(id: user_ids).includes(:profile)
users.find_each do |user|
UserMailer.notification(user).deliver_now
end
end
end
# ❌ Serializes entire user object
NotificationJob.perform_later(user)
# ✅ Pass only ID
NotificationJob.perform_later(user.id)
# In job:
def perform(user_id)
user = User.find_by(id: user_id)
return unless user
# Process user
end
# config/environments/development.rb
# Log slow queries
config.active_record.verbose_query_logs = true
# Bullet for N+1
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
end
Check slow queries:
-- PostgreSQL
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
Rails logs:
# Find slow requests
grep "Completed 200" log/production.log | awk '{print $NF}' | sort -n | tail
Before deploying, check:
includes where neededselect or pluck)# ❌ Loads everything (unnecessary)
users = User.includes(:posts, :comments, :profile, :settings, :notifications)
# ✅ Only load what's needed
users = User.includes(:posts) # Only need posts
# ❌ Never expires
Rails.cache.fetch("stats") do
calculate_stats
end
# ✅ Has expiration
Rails.cache.fetch("stats", expires_in: 1.hour) do
calculate_stats
end
# ❌ N+1 in serializer
class UserSerializer
def posts
object.posts.map { |p| PostSerializer.new(p) }
end
end
# Controller has N+1:
@users = User.all # Need includes!
# ✅ Fixed
@users = User.includes(:posts)
/sidekiq| Problem | Solution |
|---|---|
| N+1 queries | includes, preload, eager_load |
| Slow counts | Counter cache |
| Large datasets | find_each, find_in_batches |
| Expensive views | Fragment caching |
| Slow calculations | Low-level caching |
| Missing indexes | add_index |
| Full column load | select, pluck |
Version: 2.0
Last Updated: 2024-01
Maintained By: kw-app team