database-engineering
Skill3 starsChanged 2 months agoMITView on GitHub
Senior database engineering — schema design, constraints, indexing, query optimisation, N+1 elimination, transactions and isolation, migrations, multi-tenancy and row-level security, backups, point-in-time recovery and restore drills, retention and deletion. Use when designing or changing a schema, writing or reviewing migrations, when the user says "slow query", "database design", "add an index", "N+1", "my database is slow", "migration", "data model", "schema", "backup", "restore", "postgres", "mysql", "mongodb", "ORM", "deadlock", "connection pool" or "row level security"; and as a mandatory pass in any project audit. By Devleck.
We split this skill into the rules it gives an agent
7 rules. 0 are common, 4 are distinctive, and 3 are very specific to this project.
0
Common
Most of the 3 edited versions we read keep these.
4
Distinctive
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 17
What are the constraints?: Not in the ORM — in the database.
- Distinctive: line 23
What happens to a row when the user asks to be deleted?: If nobody can answer, that is a compliance finding as well as a design one.
- Very specific: line 58
Every table gets created_at and updated_at.
- Very specific: line 106
Building indexes on live tables: use CREATE INDEX CONCURRENTLY (Postgres) or the equivalent online path.
- Distinctive: line 114
Read the plan, do not guess.
- Distinctive: line 161
Keep transactions short. Long transactions block vacuum, hold locks, and bloat.
- Distinctive: line 173
Expand/contract for anything breaking: add the new structure, backfill in batches, dual-write, migrate reads, then remove the old.
“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Database Engineering
The schema outlives the code, the framework, and usually the company. Application bugs are fixed with a deploy; data-model mistakes are fixed with a migration project, and data loss is fixed with nothing at all.
The five questions that open every review
- What are the constraints? Not in the ORM — in the database.
SELECTagainst the catalogue, not the model file. - Which foreign keys are unindexed? Almost always the top performance finding, and it never shows up in development.
- What does the hottest query actually do?
EXPLAIN ANALYZE, not intuition. - When was a backup last restored? Not "do backups exist" — restored.
- What happens to a row when the user asks to be deleted? If nobody can answer, that is a compliance finding as well as a design one.
Schema design
Constraints belong in the database. The ORM is not the only writer — a migration script, an admin tool, an analytics job, a psql session or a future service will write directly. A rule enforced only in application code is a rule that will be broken.
- Primary key on every table.
- Foreign keys with an explicit
ON DELETE—CASCADE,RESTRICTorSET NULLchosen per relationship. CopyingCASCADEeverywhere is how a user deletion silently removes audit records. NOT NULLwherever the business says required. Nullable-by-default columns push the check into every consumer, forever.UNIQUEon what is genuinely unique, including composite uniqueness.CHECKfor invariants: non-negative amounts, valid ranges, valid enum values,end_date > start_date.- Enums as a database type or a lookup table with a foreign key — not a free string column with a comment.
Types matter more than they look
| Data | Use | Never |
| Money | NUMERIC(19,4) or integer minor units | Float. Ever |
| Timestamps | timestamptz, stored UTC | Naive local time |
| Identifiers | UUIDv7 / ULID for public ids | Sequential integers exposed publicly — they leak volume and enable enumeration |
| Text | text with a CHECK on length | Arbitrary varchar(255) chosen by habit |
| JSON | jsonb, for genuinely schemaless data only | JSON as an escape from schema design |
| Booleans | boolean NOT NULL DEFAULT false | Nullable three-state booleans |
Every table gets created_at and updated_at. Tables holding personal data get
a documented retention rule and a deletion path — decide this at design time, not
at DSAR time.
references/schema-design.md covers normalisation, denormalisation, soft
deletes, audit tables and temporal data.
Indexing
The rules
- Index every foreign key. The database does not do it for you (except MySQL), and unindexed FKs turn joins and cascading deletes into table scans.
- Index columns used in
WHERE,JOIN, andORDER BYon hot paths. - Composite index column order follows selectivity and usage: equality
predicates first, then range, then sort. An index on
(a, b)serves queries filtering ona, or onaandb— not onbalone. - Partial indexes for queries over a subset (
WHERE deleted_at IS NULL) — much smaller, much faster. - Covering indexes (
INCLUDE) to avoid heap lookups on hot reads. - Every index costs write throughput and storage forever. Find and drop the unused ones.
-- Postgres: unindexed foreign keys
SELECT c.conrelid::regclass AS table, a.attname AS column
FROM pg_constraint c
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
WHERE c.contype = 'f'
AND NOT EXISTS (
SELECT 1 FROM pg_index i
WHERE i.indrelid = c.conrelid AND a.attnum = i.indkey[0]
);
-- Indexes that cost writes and return nothing
SELECT relname, indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_stat_user_indexes WHERE idx_scan = 0 ORDER BY pg_relation_size(indexrelid) DESC;
-- The queries actually worth optimising (requires pg_stat_statements)
SELECT calls, round(mean_exec_time::numeric,2) AS mean_ms,
round(total_exec_time::numeric,2) AS total_ms, left(query, 100)
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 15;Optimise by total time, not mean. A 5ms query run 2,000,000 times costs more than a 4-second report run twice a day.
Building indexes on live tables: use CREATE INDEX CONCURRENTLY (Postgres)
or the equivalent online path. A plain CREATE INDEX takes a lock that will stop
your application.
Query optimisation
Read the plan, do not guess.
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) <your query>;| In the plan | Means | Do |
Seq Scan on a large table | No usable index | Add one, or accept if the table is small |
Rows Removed by Filter large | Index not selective enough | Composite or partial index |
| Estimated rows far from actual | Stale statistics | ANALYZE; check autovacuum |
Nested Loop over many rows | Bad join strategy from bad estimates | Fix statistics; consider rewriting |
Sort with external merge Disk | Sort spilling to disk | Index the sort order, or raise work_mem |
Hash Join with high memory | Fine, usually | Watch memory under concurrency |
N+1 is the most common performance defect in application code, and it is invisible in the source — you find it in the query log for one page load, not by reading the handler.
Look at: the ORM's query log for a single request.
If you see the same query shape N times with different parameters, that is it.
Fix with: eager loading (select_related / prefetch_related / with / include /
Preload / JOIN FETCH), or a dataloader for GraphQL.Enable your ORM's strict mode in development so N+1 raises rather than passes:
Laravel Model::preventLazyLoading(), Rails bullet, Django
django-debug-toolbar, Hibernate's lazy-init exceptions.
Transactions
- Boundaries wrap the business operation, not each individual write. A multi-step operation where partial state is invalid must be one transaction.
- Never make an external HTTP call inside a transaction. It holds a connection for the duration of someone else's outage and is a classic cause of pool exhaustion.
- Know your isolation level and what it does not prevent. Read Committed (the
usual default) allows non-repeatable reads and does not prevent lost updates on
read-modify-write. Use
SELECT ... FOR UPDATE, an atomic update (SET n = n - 1 WHERE n >= 1), or a unique constraint. - Race conditions: the correct fix is almost always a database constraint or an atomic operation, not application-level locking. Test with concurrent requests — this class of bug never appears in serial testing.
- Retry serialization failures with backoff, at the transaction boundary.
- Keep transactions short. Long transactions block vacuum, hold locks, and bloat.
Migrations
- Versioned, in the repository, applied by the pipeline, never by hand in production.
- Reversible, with the down path tested. Where a reverse is genuinely impossible, document why and define the forward-fix.
- Test against production-sized data. A migration that takes 50ms on 1,000 rows can take 40 minutes and an exclusive lock on 50,000,000.
- Expand/contract for anything breaking: add the new structure, backfill in batches, dual-write, migrate reads, then remove the old. Three deploys, zero downtime.
- Backfills run in batches with a sleep, not one statement across a whole table.
- Adding a
NOT NULLcolumn with a default rewrites the table on older engines — check what your version does before running it on a hot table. - Separate schema migrations from data migrations.
- Migrations run as an explicit pipeline step before the application deploy, with a documented procedure for a mid-migration failure.
references/migrations.md has the zero-downtime patterns per operation.
Multi-tenancy
Shared schema with a tenant column is the default. Enforce it in the database:
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (organization_id = current_setting('app.current_org')::uuid);Application-only filtering means one forgotten WHERE organization_id = ? is a
cross-tenant breach — and that is exactly the line a tired engineer omits.
Row-level security makes the failure impossible rather than unlikely.
Watch the aggregate leaks: counts, search indexes, autocomplete, exports, generated reports and cached fragments frequently bypass the tenant filter.
Survivability — the part nobody tests
An untested backup is not a backup. It is a configuration that reports success.
- Automated, scheduled, off-site, encrypted, with defined retention.
- Point-in-time recovery available, not just nightly snapshots — a nightly snapshot means up to 24 hours of data loss.
- A restore drill performed within the last 90 days, with the measured recovery time recorded. Until that has happened, recovery capability is unknown.
- RPO and RTO stated as numbers, and verified against the drill.
- Backups tested for usability, not just restorability — does the app run against the restored copy?
- Restore access is itself controlled and audited (a backup is a full copy of your data; who can download it?).
- Deletion propagates to backups per a documented, defensible policy.
Also: connection pooling sized deliberately (an app with 100 workers and a 100-connection database will exhaust it); slow-query logging on; autovacuum monitored; replication lag alerted; disk-space alerting well before full.
Reporting a database finding
State the measurement, not the impression.
### Missing index on `invoices.organization_id` — P1
**Evidence** EXPLAIN ANALYZE on the invoice list query:
Seq Scan on invoices (cost=0..48210 rows=1 width=412)
(actual time=0.03..612.4 rows=41 loops=1)
Rows Removed by Filter: 1,284,013
**Impact** 612ms on the most-visited authenticated page, growing linearly
with total invoice count across all tenants.
**Fix** CREATE INDEX CONCURRENTLY idx_invoices_org
ON invoices (organization_id, created_at DESC);
**Verify** Re-run EXPLAIN ANALYZE; expect an Index Scan and < 5ms.References
references/schema-design.md— modelling, keys, types, soft delete, audit, temporal datareferences/indexing-and-queries.md— index types, plan reading, engine-specific diagnosticsreferences/migrations.md— zero-downtime patterns per operationreferences/resilience.md— backups, PITR, restore drills, replication, poolingreferences/data-lifecycle.md— retention, deletion, anonymisation, and the DSAR pipeline
Other versions5 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 5
- caiaffa/claude-code-ultimate-engineering-system★ 17same name
- BejeweledMe/codex-pro-agent-skills★ 7same name
- weirdgme/Claude-code-infrastructure-showcase★ 4same name
- foreturn/product-studio★ 3same name
- rogue-dev-studio/ai-agents-rogue★ 2same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository21 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- accessibility-auditSkill
- ai-securitySkill
- api-contractsSkill
- cloud-securitySkill
- devops-platformSkill
- engineering-standardsSkill
- observability-sloSkill
- pentest-methodologySkill
- performance-engineeringSkill
- php-hardeningSkill
- privacy-complianceSkill
- project-auditSkill
- project-blueprintSkill
- release-readinessSkill
- security-hardeningSkill
- security-incident-responseSkill
- senior-engineerSkill
- testing-strategySkill
- web-presence-auditSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
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.

