Code is forgiving — you can rewrite it freely. A schema with real data in it is not, which is why it deserves a different level of care than most other early decisions.
1Add, Don't Rewrite, Whenever Possible
Adding a new nullable column or a new table is a low-risk, reversible change. Renaming a column or restructuring a relationship that real production data already depends on is high-risk and hard to reverse. Defaulting to additive changes keeps most day-to-day schema evolution cheap.
2Nullable Columns Enable Safe, Gradual Rollouts
The same gradual-rollout thinking from feature flags applies to schema changes: a new column that's nullable (or has a safe default) lets old and new application code coexist against the same database during a deploy, instead of requiring a risky, coordinated, all-at-once cutover.
3Step-by-Step Breakdown
Code is comparatively cheap to change; a schema with real production data in it is not. Renaming a column or splitting a table means writing and running a migration against live data, often with downtime or careful sequencing — which is exactly why schema decisions deserve more upfront care than most other early decisions.
Two habits keep a schema cheap to evolve: prefer additive changes (new nullable columns, new tables) over destructive ones (renaming, dropping) when possible, and default new columns to nullable or with a safe default so old code and new code can both run against the same schema during a rollout.
Why does making a new database column nullable (or giving it a safe default) matter during a rollout?
- →It doesn't matter, nullability is just a style preference
- →It lets old code (that doesn't know about the new column) and new code (that does) both run correctly against the same schema during a gradual deploy, without breaking either
- →It makes the database run faster
- →It's required by all SQL databases
Over-normalizing a schema for relationships you're not sure exist yet is a form of premature optimization — a slightly denormalized, simpler schema that's easy to understand and query is often the better bet while you're still learning what the real data relationships are.
Why might over-normalizing a schema early (before you're sure of the real relationships) be a mistake?
- →Normalization is always wrong regardless of context
- →It encodes assumptions about relationships you might not actually have right yet, and those assumptions are expensive to undo once real data exists
- →Normalized schemas always perform worse
- →It has no downside, more normalization is always safer
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Store User-Facing Text Fields With Room for Real-World Length
A common accessibility and usability failure comes from schema-level constraints (overly short varchar limits) that later force truncated, confusing text in the UI — think about realistic real-world content length when defining text field constraints, not just the shortest example you tested with.
// Too tight: name VARCHAR(20) -- truncates real names
// Safer: name VARCHAR(255) or TEXTSEO Implications
- 1
Target 'schema design for fast-moving products' rather than academic database normalization theory
Readers here want practical rollout-safety and change-cost heuristics, not a formal normal-forms lecture aimed at database administrators.
Best Practices
Write Migrations as Small, Reversible Steps
Break schema changes into small steps that can each be safely rolled back independently (add column -> backfill -> switch reads -> remove old column later) rather than one large, all-or-nothing migration — this mirrors the same incremental-and-reversible thinking used in feature rollouts.
Frequent Bugs
Adding a required (non-null, no default) column in the same migration that deploys code depending on it, breaking any still-running old code instances during the rollout.
Add new columns as nullable or with a safe default first, deploy code that can handle both states, backfill existing rows, and only make the column required in a later migration once all code paths are updated.
Real-World Examples
The Coordinated Cutover That Wasn't Needed
A team needed to add a required 'status' field to an orders table. Instead of a risky simultaneous code-and-schema deploy, they added it as nullable with a default, deployed code that set it going forward, backfilled old rows over a few days, then made it required in a follow-up migration — no downtime, no coordinated cutover.
-- Step 1: ALTER TABLE orders ADD COLUMN status TEXT DEFAULT 'pending';
-- Step 2: backfill existing rows
-- Step 3 (later): ALTER TABLE orders ALTER COLUMN status SET NOT NULL;