agentleFS
mickzijdelmickzijdel/rails-toolkit/skills/rails-database-performance/SKILL.md

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.

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
  1. Very specific:

    Every column ending in _id must have an index.

    line 25
  2. Very specific:

    Check every model for scopes using where.

    line 60
  3. Distinctive:

    Check models for transitions and scopes on these columns.

    line 89
  4. Very specific:

    Check: email_address, email, username, token, reset_password_token, confirmation_token

    line 141
  5. Very specific:

    If you find a count > 0 or .count call on a large association without a counter cache, consider adding one:

    line 160
  6. Very specific:

    Fix: Use keyset/cursor pagination, e.g.

    line 190
  7. Very specific:

    Run EXPLAIN ANALYZE on your slowest queries to spot sequential scans on large tables with sorts:

    line 206
  8. Very specific:

    Look for: Sort Method: external merge Disk or Seq Scan — these indicate missing indexes.

    line 211
  9. Distinctive:

    Keep each migration focused.

    line 263
  10. Very specific:

    Not using EXPLAIN ANALYZE to verify the index is actually being used after adding it

    line 286

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

  1. Open db/schema.rb
  2. Open all model files (app/models/**/*.rb)
  3. Work through every checklist item below
  4. 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_id

2. 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.rb

Fix:

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 on published
  • where(featured: true) → index on featured
  • where(active: true) → index on active
  • where(status: ...) → index on status
  • where(account_id: ...) + another column → consider composite index

Fix:

add_index :articles, :published
add_index :articles, :featured

4. 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.rb

Check models for transitions and scopes on these columns.

Fix:

add_index :posts, :status

5. 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, :position

For 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.rb

7. 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: true

8. 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: true

9. 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/ | head

For any hit, check whether anything bounds growth (cron/recurring job deleting old rows).

Fix (in order of preference):

  1. 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
  2. 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:

ToolNotes
pgheroFree, Rails-friendly, Postgres
pganalyzePaid, Postgres, the most complete
Percona PMMOpen source, the strong MySQL option
Datadog DBMPaid 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 AddAuthIndexesToUsers

Keep each migration focused. Do not combine unrelated tables in one migration.


Quick Reference

Column PatternRequired Index
*_id (foreign key)Single index
*_type + *_id (polymorphic)Composite index on both
Scope where columnsSingle index
status, state columnsSingle index
ORDER BY columnsSingle index (with direction if needed)
position, sort_orderSingle index
email, username (login)Unique index
*_token (auth tokens)Unique index
versions/audit/log tablesRetention policy or separate DB (no index fixes growth)

Common Mistakes

  • Adding separate indexes for polymorphic _type and _id instead of a composite index
  • Forgetting that email_address on the users table 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 where is frequent)
  • Not using EXPLAIN ANALYZE to verify the index is actually being used after adding it

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.