rails-database-performance
Skill2 starsChanged 8 days agoMITView on GitHub
Use when reviewing or auditing a Rails app's database schema for missing indexes, slow query patterns, or database performance issues. Triggers on: schema review, slow queries, EXPLAIN ANALYZE output, missing index warnings, or any request to audit db/schema.rb.
In this repository24 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- rails-activestorageSkill
- rails-apiSkill
- rails-auditSkill
- rails-clean-test-outputSkill
- rails-controllersSkill
- rails-coreSkill
- rails-i18nSkill
- rails-jobsSkill
- rails-loggingSkill
- rails-mailersSkill
- rails-migrationsSkill
- rails-modelsSkill
- rails-multi-tenancySkill
- rails-performanceSkill
- rails-philosophySkill
- rails-project-setupSkill
- rails-securitySkill
- rails-stimulusSkill
- rails-styleSkill
- rails-testingSkill
- rails-turboSkill
- rails-upgradeSkill
- rails-viewcomponentsSkill
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 2 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 25
Every column ending in _id must have an index.
- Very specific: line 60
Check every model for scopes using where.
- Distinctive: line 89
Check models for transitions and scopes on these columns.
- Very specific: line 141
Check: email_address, email, username, token, reset_password_token, confirmation_token
- Very specific: line 160
If you find a count > 0 or .count call on a large association without a counter cache, consider adding one:
- Very specific: line 190
Fix: Use keyset/cursor pagination, e.g.
- Very specific: line 206
Run EXPLAIN ANALYZE on your slowest queries to spot sequential scans on large tables with sorts:
- Very specific: line 211
Look for: Sort Method: external merge Disk or Seq Scan — these indicate missing indexes.
- Distinctive: line 263
Keep each migration focused.
- Very specific: line 286
Not using EXPLAIN ANALYZE to verify the index is actually being used after adding it
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Rails Database Index & Query Performance Audit
Overview
A systematic checklist for auditing db/schema.rb and ActiveRecord models for missing indexes and query anti-patterns. Work through every section below — do not stop early.
How to Run This Audit
- Open
db/schema.rb - Open all model files (
app/models/**/*.rb) - Work through every checklist item below
- For each issue found: generate a migration, do not bundle unrelated indexes together
Checklist
1. Foreign Key Indexes
Every column ending in _id must have an index.
grep -n "_id" db/schema.rb | grep -v "index\|#"Cross-reference with:
grep -n "add_index\|t\.index" db/schema.rb | grep "_id"Fix:
add_index :table_name, :other_model_id2. Polymorphic Association Indexes
Any polymorphic association (_type + _id pair) needs a composite index on both columns together, not two separate indexes.
grep -n "_type" db/schema.rbFix:
add_index :comments, [:commentable_type, :commentable_id]3. Frequently Queried Scope Columns
Check every model for scopes using where. Each column in a where clause on a frequently-called scope is a candidate for an index.
grep -rn "scope.*where" app/models/Common patterns that need indexes:
where(published: true)→ index onpublishedwhere(featured: true)→ index onfeaturedwhere(active: true)→ index onactivewhere(status: ...)→ index onstatuswhere(account_id: ...)+ another column → consider composite index
Fix:
add_index :articles, :published
add_index :articles, :featured4. Status / State Columns
Columns named status, state, aasm_state, workflow_state that are queried with where need an index. These are often used in scopes like where(status: "published").
grep -n "status\|state\|aasm_state" db/schema.rbCheck models for transitions and scopes on these columns.
Fix:
add_index :posts, :status5. Sort / Order Columns
Any column used in ORDER BY should have an index so the database can read pre-sorted data instead of sorting at query time.
grep -rn "\.order(" app/models/ app/controllers/Common columns: created_at, updated_at, position, published_at, name.
Already indexed by default in many Rails setups: created_at, updated_at — verify they're actually present.
Fix:
add_index :articles, :published_at
add_index :items, :positionFor multi-column sorts, include sort direction:
add_index :articles, [:account_id, :created_at]6. Position / Sortable List Columns
Columns named position or sort_order for drag-and-drop ordered lists need an index.
grep -n "position\|sort_order\|rank" db/schema.rb7. Login / Authentication Columns
Any column used to look up users during authentication must have a unique index.
grep -rn "find_by.*email\|find_by.*username\|where.*email\|where.*username" app/models/Check: email_address, email, username, token, reset_password_token, confirmation_token
Fix:
add_index :users, :email_address, unique: true
add_index :users, :username, unique: true8. Counter Cache Columns
*_count columns added by counter_cache: true should have no index (they're read, not searched), but verify the parent model declares counter_cache: true.
grep -n "_count" db/schema.rb
grep -rn "counter_cache" app/models/If you find a count > 0 or .count call on a large association without a counter cache, consider adding one:
belongs_to :project, counter_cache: true9. Counting Anti-Pattern
Model.count performs a full table scan. Check controllers and models for count calls on large tables.
grep -rn "\.count\b" app/models/ app/controllers/ app/helpers/Replace:
items.count > 0→items.exists?items.count == 0→items.none?or!items.exists?- Frequently displayed counts →
counter_cache
10. Offset Pagination Anti-Pattern
LIMIT/OFFSET pagination degrades linearly — page 100 is ~100x slower than page 1.
grep -rn "\.offset\|paginate\|page(" app/models/ app/controllers/Fix: Use keyset/cursor pagination, e.g. with geared_pagination (see [[rails-performance]]):
@page = set_page_and_extract_portion_from(scope, per_page: [15, 30, 50])Or use range-based queries:
Fault.where("created_at > ? AND created_at < ?", 100.days.ago, 101.days.ago)11. Sorting Without Indexes
Any ORDER BY on an unindexed column causes the database to sort the full result set in memory.
Run EXPLAIN ANALYZE on your slowest queries to spot sequential scans on large tables with sorts:
EXPLAIN ANALYZE SELECT * FROM faults ORDER BY created_at DESC LIMIT 25;Look for: Sort Method: external merge Disk or Seq Scan — these indicate missing indexes.
12. Unbounded Append-Only Tables
Audit/versioning/logging tables grow forever by design and quietly become the largest tables in the database — slowing backups, bloating the buffer pool (hurting cache hit rates for the data that matters), and dragging out migrations.
grep -nE 'create_table "(versions|audits|.*_logs|.*_events|page_views|ahoy_)' db/schema.rb
grep -rn "paper_trail\|has_paper_trail\|audited" Gemfile app/models/ | headFor any hit, check whether anything bounds growth (cron/recurring job deleting old rows).
Fix (in order of preference):
- Retention policy — a recurring job that deletes rows older than the period anyone actually looks at:
PaperTrail::Version.where("created_at < ?", 1.year.ago).in_batches.delete_all - Move to a separate database via Rails multi-db. Audit data is a good fit: read rarely (admin-only), no performance-sensitive JOINs against it, and "best-effort" consistency is acceptable. Shrinks the primary, speeds up backups and upgrades.
Capacity Metrics & Monitoring
Two numbers tell you whether the database is the bottleneck before users do:
- Active (non-idle) connections to the primary — healthy target is ≤ ~50% of the database's CPU count. Sustained levels above that mean queries are queueing for CPU; more app servers will make it worse, not better.
- Per-controller p95 response time — aim for < 1 second on the top-traffic controllers. The overall average hides the handful of endpoints doing all the damage; rank controllers by p95 × throughput.
Install a database monitoring tool — index suggestions, captured EXPLAIN plans, and connection/CPU history beat re-running EXPLAIN ANALYZE by hand:
| Tool | Notes |
| pghero | Free, Rails-friendly, Postgres |
| pganalyze | Paid, Postgres, the most complete |
| Percona PMM | Open source, the strong MySQL option |
| Datadog DBM | Paid add-on if already on Datadog |
Generating Migrations
For each group of related indexes, generate a descriptive migration:
rails generate migration AddMissingIndexesToArticles
rails generate migration AddPolymorphicIndexToComments
rails generate migration AddAuthIndexesToUsersKeep each migration focused. Do not combine unrelated tables in one migration.
Quick Reference
| Column Pattern | Required Index |
*_id (foreign key) | Single index |
*_type + *_id (polymorphic) | Composite index on both |
Scope where columns | Single index |
status, state columns | Single index |
ORDER BY columns | Single index (with direction if needed) |
position, sort_order | Single index |
email, username (login) | Unique index |
*_token (auth tokens) | Unique index |
versions/audit/log tables | Retention policy or separate DB (no index fixes growth) |
Common Mistakes
- Adding separate indexes for polymorphic
_typeand_idinstead of a composite index - Forgetting that
email_addresson theuserstable should be unique - Indexing boolean columns with very low cardinality on small tables (not worth it — only index if the table is large and the
whereis frequent) - Not using
EXPLAIN ANALYZEto verify the index is actually being used after adding it
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

