data-modeling — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited data-modeling (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.
The data model outlives the code that uses it and is the most expensive thing to change later. A schema designed in the abstract — "normalized because textbooks say so" or "denormalized because it's faster" — breaks under real load or corrupts data under concurrent writes. Model from how the data is actually read and written, enforce integrity where the database can, index for the queries you run, and verify before the table has millions of rows.
Pairs with [[migration-path]] for schema changes in production, [[perf-budget]] to measure before and after query work, [[interface-design]] for API/event shapes that expose the model, [[caching-strategy]] only after the query and indexes are right, and [[hardening]] when modeling auth, tenancy, or sensitive fields.
Skip as the primary skill for pure application logic with no persistence change. Still apply if the change affects query patterns, N+1 in ORMs, or data passed to callers.
Work in order. Later steps assume access patterns are written down.
Before drawing tables, list the reads and writes the feature needs. For each, note:
| Question | Why it matters |
|---|---|
| How often? (per second, per day, batch) | Drives indexing and denormalization |
| Latency target? (interactive vs background) | Drives cache vs query shape |
| Consistency need? (exact vs eventually OK) | Drives denormalization and replicas |
| Cardinality? (one row, thousands, millions) | Drives pagination and partition strategy |
| Who calls it? (API, job, report, admin) | Drives separate read models if needed |
Read: order by id for checkout UI — ~500/s, p95 < 50ms, exact, 1 row
Read: user's recent orders — ~200/s, p95 < 100ms, exact, 20 rows paginated
Write: create order — ~50/s, must be atomic with inventory decrement
Batch: daily revenue by region — nightly, minutes OK, approximate OKIf you can't list patterns, you're not ready to model. Decompose the feature ([[work-planning]]) until each step has concrete data access.
Natural keys (email, SKU) belong in unique constraints, not always as PKs.
or JSON unless the store and access pattern truly require it.
Shared mutable tables across teams become coupling points.
deleted_at), archival, and retention up front —not when the table is full.
Name tables and columns in the domain language used by callers. Consistency beats cleverness.
Don't default to relational for everything — but don't abandon constraints without a reason.
| Workload | Typical fit | Watch out for |
|---|---|---|
| Transactional CRUD, joins, integrity | Relational (PostgreSQL, MySQL) | Over-normalized hot reads |
| Document-shaped reads, flexible schema | Document (MongoDB, Firestore) | Duplicated data without sync story |
| Append-only events, audit, replay | Event log / WAL table | Rebuilding state from events |
| Analytics, aggregates over huge history | Columnar / OLAP / warehouse | Don't run reports on the OLTP primary |
| Full-text, geo, graph traversals | Specialized index or store | Don't fake it in generic JSON |
Rule: OLTP (user-facing reads/writes) and heavy analytics usually need separate paths — ETL, CDC, or read replicas — not one schema tortured to serve both.
The database is the last line of defense. App validation is bypassable, racy, and inconsistent across callers.
Apply at the schema level:
timestamptz not string dates; numeric for money; bounded varchar where ithelps; jsonb only when shape is truly variable and you accept query cost.
null" forever.
store doesn't support them.
status IN (...), end_date >= start_date).-- Prefer: illegal states rejected by the DB
status TEXT NOT NULL CHECK (status IN ('pending', 'paid', 'cancelled')),
amount_cents BIGINT NOT NULL CHECK (amount_cents >= 0)
-- Not: status TEXT, amount_cents BIGINT — "the app checks it"Sensitive fields (PII, tokens): model encryption, hashing, or separation at design time ([[hardening]]). Don't widen a table with sensitive columns "for convenience."
Start normalized to avoid update anomalies — one copy of truth, consistent updates.
Denormalize only when:
When you denormalize, document:
Denormalization without a sync story becomes silent data corruption.
Every index speeds specific reads and slows every write on that table (plus storage cost).
WHERE, JOIN, and ORDER BY from step 1.WHERE status = 'active').-- Query: WHERE tenant_id = ? AND created_at > ? ORDER BY created_at DESC
CREATE INDEX idx_orders_tenant_created ON orders (tenant_id, created_at DESC);
-- Not: separate indexes on tenant_id and created_at when the query always uses bothDon't index low-cardinality columns alone (boolean flags) unless combined in a composite that matches a real query. Don't "index everything to be safe."
After adding indexes, verify the planner uses them on realistic volume (step 9).
Kill N+1. Fetch related data in one round trip — join, IN (...) batch, or ORM include/prefetch. A query per row in a loop dominates latency at scale ([[perf-budget]]).
Paginate unbounded results. Never SELECT * from a growing table without a limit.
| Approach | Use when | Avoid when |
|---|---|---|
Keyset / cursor (WHERE id > ? ORDER BY id LIMIT) | Large tables, stable sort key | Sort column changes often |
Offset (OFFSET n) | Small offsets, admin UIs | Deep pages on huge tables |
Select only needed columns. Wide rows multiply I/O and memory — especially with SELECT * and ORM defaults.
Batch writes for bulk inserts/updates; avoid per-row commits in loops.
Lock awareness on hot rows:
SKIP LOCKED / FOR UPDATE SKIP LOCKED patterns.Schema changes in production are migrations, not edits ([[migration-path]]).
for history when rows never delete.
of connections.
Never ship a breaking schema change without caller inventory and a rollback story for data, not just code.
"It works on three test rows" hides full table scans, sort spills, and lock queues.
Before claiming done:
EXPLAIN (ANALYZE, BUFFERS) in PostgreSQL; equivalent in your engine.Look for sequential scans on large tables, bad join order, filesort, missing index use.
"page 1000," not just page 1.
([[observability]]).
A cache ([[caching-strategy]]) is not a substitute for a bad plan. Fix the query and indexes first.
New feature schema
expected row count for the hot table.
Slow query in production
lock wait. Fix the dominant cost; measure again.
Read-heavy, rarely updated
([[caching-strategy]]).
Write-heavy ingest
Don't over-index "just in case."
Multi-tenant
tenant_id on every tenant-scoped row; composite indexes lead with tenant_id. Never queryacross tenants without an explicit, audited reason. Row-level security or app scoping — pick one model and enforce consistently ([[hardening]]).
Reporting and analytics
model, different indexes, different SLAs.
PII and retention
billions of rows nightly without a plan.
SELECT * by default; read the SQL.WHERE/JOIN/ORDER BYSELECT * on wide tables in hot pathstenant_id in indexes for tenant-scoped queries~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.