Rails 6 adds ActiveRecord::Relation#annotate
Rails 6 introduced the annotate method to ActiveRecord::Relation, allowing you to add SQL comments to generated queries. This is useful for debugging, logging, and tracking query origins.
What is annotate?
The annotate method adds SQL comments to your queries, which can help with:
- Debugging: Identify where queries originate
- Logging: Track query performance and usage
- Monitoring: Monitor specific queries in production
- Documentation: Document query purpose
Basic Usage
Simple Annotation
Add a comment to a query:
User.where(active: true).annotate("Fetching active users")
Generated SQL:
SELECT "users".* FROM "users" WHERE "users"."active" = $1 /* Fetching active users */
Multiple Annotations
Chain multiple annotations:
User.where(active: true)
.annotate("Fetching active users")
.annotate("For dashboard display")
Generated SQL:
SELECT "users".* FROM "users" WHERE "users"."active" = $1 /* Fetching active users */ /* For dashboard display */
Real-World Examples
Tracking Query Origins
Annotate queries to track where they come from:
class UsersController < ApplicationController
def index
@users = User.where(active: true)
.annotate("UsersController#index")
.limit(20)
end
end
Performance Monitoring
Use annotations for performance tracking:
class Post < ApplicationRecord
scope :recent, -> {
where('created_at > ?', 1.week.ago)
.annotate("Post.recent scope")
}
end
Debugging Complex Queries
Annotate complex query chains:
User.joins(:posts)
.where(posts: { published: true })
.annotate("Finding users with published posts")
.group('users.id')
.annotate("Grouping by user")
.having('COUNT(posts.id) > ?', 5)
.annotate("Having more than 5 posts")
Use Cases
Identifying Slow Queries
Use annotations to identify problematic queries in logs:
# In your application
User.includes(:posts, :comments)
.annotate("User dashboard query")
.where(active: true)
# In logs, you'll see:
# User Load (123.4ms) /* User dashboard query */
Query Tagging
Tag queries for monitoring:
class SearchService
def self.search_users(query)
User.where("name ILIKE ?", "%#{query}%")
.annotate("SearchService#search_users: #{query}")
end
end
Documentation
Document query purpose:
class ReportGenerator
def active_users_report
User.where(active: true)
.annotate("ReportGenerator#active_users_report")
.select(:id, :name, :email)
.annotate("Selecting minimal fields for report")
end
end
Advanced Usage
Dynamic Annotations
Use dynamic values in annotations:
def find_users_by_role(role)
User.where(role: role)
.annotate("find_users_by_role(#{role})")
end
Conditional Annotations
Add annotations conditionally:
def find_users(include_inactive: false)
relation = User.all
relation = relation.where(active: true) unless include_inactive
relation = relation.annotate("include_inactive: #{include_inactive}")
relation
end
Integration with Monitoring
APM Tools
Annotations appear in APM tools:
# New Relic, Datadog, etc. will show annotations
User.where(active: true)
.annotate("critical_path: user_dashboard")
Query Logging
Annotations appear in Rails query logs:
# Development log output:
# User Load (45.2ms) SELECT "users".* FROM "users" WHERE "users"."active" = $1 /* critical_path: user_dashboard */
Best Practices
- Be descriptive: Use clear, meaningful annotations
- Include context: Add method names or controller actions
- Keep it concise: Don't make annotations too long
- Use consistently: Establish patterns for your team
Common Patterns
Controller Actions
class PostsController < ApplicationController
def index
@posts = Post.published
.annotate("#{controller_name}##{action_name}")
.limit(20)
end
end
Service Objects
class UserSearchService
def self.search(query, filters = {})
User.all
.annotate("UserSearchService.search")
.where("name ILIKE ?", "%#{query}%")
.merge(apply_filters(filters))
end
end
Scopes
class Post < ApplicationRecord
scope :recent, -> {
where('created_at > ?', 1.week.ago)
.annotate("Post.recent")
}
scope :popular, -> {
where('likes_count > ?', 100)
.annotate("Post.popular")
}
end
Limitations
- Annotations don't affect query performance
- They're only visible in SQL logs and monitoring tools
- Too many annotations can clutter logs
Conclusion
Rails 6's annotate method provides a simple way to add SQL comments to your queries. This feature is invaluable for debugging, monitoring, and understanding query origins in production applications. By consistently using annotations, you can make your application's query behavior more transparent and easier to debug.