Claude-skill-registry ActiveRecord Query Patterns
Complete guide to ActiveRecord query optimization, associations, scopes, and PostgreSQL-specific patterns. Use when: (1) Writing database queries, (2) Designing model associations, (3) Creating migrations, (4) Optimizing query performance, (5) Debugging N+1 queries and GROUP BY errors. Trigger keywords: database, models, associations, validations, queries, ActiveRecord, scopes, migrations, N+1, PostgreSQL, indexes, eager loading
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/activerecord-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-activerecord-query-patterns && rm -rf "$T"
manifest:
skills/data/activerecord-patterns/SKILL.mdsource content
ActiveRecord Query Patterns
Query Decision Tree
What do I need? │ ├─ Find records by ID or attributes? │ ├─ Single record: find(id), find_by(attrs) │ └─ Multiple records: where(conditions) │ ├─ Access associated records? │ ├─ Just filtering? → joins(:association) │ └─ Loading data? → includes(:association) │ ├─ Aggregate data (count, sum, avg)? │ └─ GROUP BY query │ └─ REMEMBER: Every SELECT column must be in GROUP BY or aggregate │ ├─ Complex multi-step query? │ └─ Query Object pattern (app/queries/) │ ├─ Hierarchical/recursive data? │ └─ CTE (Common Table Expression) │ └─ Full-text search? └─ pg_search gem with tsvector indexes
NEVER Do This
NEVER use
includes with group:
# WRONG - PostgreSQL error Task.includes(:carrier).group(:status).count # RIGHT - Separate queries status_counts = Task.group(:status).count tasks = Task.where(status: status_counts.keys.first).includes(:carrier)
NEVER iterate without eager loading:
# WRONG - N+1 queries tasks = Task.all tasks.each { |t| puts t.carrier.name } # Query per task! # RIGHT - Eager load tasks = Task.includes(:carrier) tasks.each { |t| puts t.carrier.name } # Single query
NEVER load all records into memory:
# WRONG - Memory explosion Task.all.each { |task| process(task) } # RIGHT - Batch processing Task.find_each(batch_size: 1000) { |task| process(task) }
NEVER use
present? to check existence:
# WRONG - Loads all records Task.where(status: 'pending').present? # RIGHT - Efficient existence check Task.where(status: 'pending').exists?
NEVER forget indexes on foreign keys:
# WRONG - No index t.references :merchant, foreign_key: true, index: false # RIGHT - Always index foreign keys t.references :merchant, null: false, foreign_key: true # index: true is default
Model Template
class Task < ApplicationRecord # == Constants ============================================================== STATUSES = %w[pending in_progress completed].freeze # == Associations =========================================================== belongs_to :account belongs_to :merchant belongs_to :carrier, optional: true has_many :timelines, dependent: :destroy # == Validations ============================================================ validates :status, presence: true, inclusion: { in: STATUSES } validates :tracking_number, presence: true, uniqueness: { scope: :account_id } # == Scopes ================================================================= scope :active, -> { where.not(status: 'completed') } scope :for_carrier, ->(carrier) { where(carrier: carrier) } # == Callbacks ============================================================== before_validation :generate_tracking_number, on: :create # == Class Methods ========================================================== def self.search(query) where("tracking_number ILIKE ?", "%#{query}%") end # == Instance Methods ======================================================= def complete! update!(status: 'completed', completed_at: Time.current) end private def generate_tracking_number self.tracking_number ||= SecureRandom.hex(8).upcase end end
Eager Loading Quick Reference
| Method | Query Type | Use Case |
|---|---|---|
| Smart (auto-selects) | Default choice |
| Separate queries | Can't filter on association |
| LEFT JOIN | Need to filter on association |
| INNER JOIN | Filtering only, no data loading |
# Multiple associations Task.includes(:carrier, :merchant, :recipient) # Nested associations Task.includes(merchant: :branches) # Filter on association (requires references or use joins) Task.joins(:carrier).where(carriers: { active: true })
Scope Patterns
# Simple scopes scope :active, -> { where.not(status: 'completed') } scope :recent, -> { order(created_at: :desc) } # Parameterized scopes scope :by_status, ->(status) { where(status: status) } scope :created_after, ->(date) { where('created_at >= ?', date) } # Conditional (always returns relation) scope :by_status_if, ->(status) { where(status: status) if status.present? } # Chainable Task.active.recent.by_status('pending')
GROUP BY (PostgreSQL Critical)
Rule: Every non-aggregated SELECT column must appear in GROUP BY.
# CORRECT Task.group(:status).count Task.group(:status).sum(:amount) Task.group(:status, :task_type).count # CORRECT - Explicit select Task.select(:status, 'COUNT(*) as count', 'AVG(amount) as avg') .group(:status) # Date grouping Task.group("DATE(created_at)").count
Migration Quick Reference
class CreateTasks < ActiveRecord::Migration[7.1] def change create_table :tasks do |t| t.references :account, null: false, foreign_key: true t.string :tracking_number, null: false t.string :status, null: false, default: 'pending' t.decimal :amount, precision: 10, scale: 2 t.jsonb :metadata, default: {} t.timestamps t.index :tracking_number, unique: true t.index :status t.index [:account_id, :status] t.index :metadata, using: :gin end end end # Concurrent index (large tables) class AddIndex < ActiveRecord::Migration[7.1] disable_ddl_transaction! def change add_index :tasks, :status, algorithm: :concurrently end end
Performance Checklist
Before writing any query: [ ] Am I loading more columns than needed? → Use select/pluck [ ] Am I iterating and accessing associations? → Use includes [ ] Am I using GROUP BY? → Every SELECT column grouped or aggregated? [ ] Am I using includes with GROUP BY? → DON'T! Separate queries [ ] Will this query run on large table? → Check indexes exist [ ] Am I loading all records? → Use find_each for batches [ ] Am I checking existence? → Use exists? not present? [ ] Do indexes exist for WHERE/ORDER columns?
Enum Pattern
class Task < ApplicationRecord enum status: { pending: 0, in_progress: 1, completed: 2 }, _prefix: true # Generated methods: # task.status_pending? # task.status_completed! # Task.status_pending (scope) # Task.not_status_pending (scope) end
JSONB Quick Reference
# Migration add_column :tasks, :metadata, :jsonb, default: {} add_index :tasks, :metadata, using: :gin # Queries Task.where("metadata @> ?", { priority: 1 }.to_json) # Contains Task.where("metadata ->> 'key' = ?", 'value') # Extract as text Task.where("metadata ? 'key'") # Key exists
Debugging Queries
# Enable logging ActiveRecord::Base.logger = Logger.new(STDOUT) # Explain query plan Task.where(status: 'pending').explain(:analyze) # Use Bullet gem for N+1 detection # Gemfile: gem 'bullet', group: :development
References
Detailed patterns and examples in
references/:
- Association types, options, polymorphicassociations.md
- Basic queries, eager loading, subqueriesquery-patterns.md
- Scope patterns, query objectsscopes-query-objects.md
- Create table, safe migrations, JSONBmigrations.md
- Batch processing, counter caches, indexesperformance.md
- Composite keys, encryption, multi-dbrails7-8-features.md
- Enums, database views, CTEs, STIadvanced-patterns.md
- Full-text search, JSONB, arrayspostgresql-features.md