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

  1. Be descriptive: Use clear, meaningful annotations
  2. Include context: Add method names or controller actions
  3. Keep it concise: Don't make annotations too long
  4. 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.

Rails 6 adds ActiveRecord::Relation#annotate - Abhay Nikam