rails-database-performance — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited rails-database-performance (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
db/schema.rbapp/models/**/*.rb)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_idAny 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]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 publishedwhere(featured: true) → index on featuredwhere(active: true) → index on activewhere(status: ...) → index on statuswhere(account_id: ...) + another column → consider composite indexFix:
add_index :articles, :published
add_index :articles, :featuredColumns 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, :statusAny 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]Columns named position or sort_order for drag-and-drop ordered lists need an index.
grep -n "position\|sort_order\|rank" db/schema.rbAny 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*_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: trueModel.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?counter_cacheLIMIT/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)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.
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):
PaperTrail::Version.where("created_at < ?", 1.year.ago).in_batches.delete_allTwo numbers tell you whether the database is the bottleneck before users do:
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 |
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.
| 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) |
_type and _id instead of a composite indexemail_address on the users table should be uniquewhere is frequent)EXPLAIN ANALYZE to verify the index is actually being used after adding it~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.