SaaS database design mistake #1: hardcoding "one plan = one set of features" into your schema instead of a permissions/entitlements table. You'll refactor this within 6 months either way.
Unique constraints across multiple columns (UNIQUE(user_id, email)) vs application-level uniqueness checks — only one of these actually guarantees anything under concurrency.
Your schema is documentation whether you intend it to be or not. Column names like data or value are schema debt disguised as flexibility.
Check constraints are underused. CHECK (price >= 0) at the DB level catches bugs that slip through 5 layers of app validation.
Migrations that add a NOT NULL column to a large table lock it in older Postgres versions. Add nullable → backfill → set NOT NULL in a separate migration.
Partitioning a table by date sounds simple until your queries don't filter by date and Postgres scans every partition anyway. Partition for your query patterns, not your intuition.
Isolation levels aren't academic. READ COMMITTED (Postgres default) can still give you race conditions on read-then-write logic. If money or inventory is involved, know what you're actually protected against.
A "created_by" column without a foreign key to users is a schema that trusts application code to never have a bug. It will.
N+1 queries aren't an ORM problem, they're a mental model problem. If your loop hits the DB, your DB design is doing the work your code should be doing.
Generating a schema from a prompt is the easy 20%. The other 80% is indexing strategy, constraint design, and migration planning — the part most "AI database generator" tools skip entirely.
Timestamps without timezone (timestamp instead of timestamptz) is the bug that doesn't show up until your first international user.
Index everything you filter on? No — every index you add slows down every write. The real skill is knowing which reads are worth that write-cost tradeoff.
Connection pooling isn't optional past a handful of concurrent users. PgBouncer in transaction mode + your ORM's pool settings fighting each other is a classic silent outage.
Most "bad" database schemas aren't badly designed — they're designed for a scale that no longer exists. Schema debt is usually a scaling problem wearing a design-mistake costume.
Hardcoding features per pricing plan means every future pricing change is a code deployment.
Here's the schema that fixes it — 3 tables, forever. 🧵
Now checking access is one query:
sql
SELECT * FROM plan_features
WHERE plan_id = ? AND feature_id = ?
Add a feature. Change a plan. Zero deploys.