Here's what most founders get wrong about databases:
They think it's just storage.
"Let's just store the data and figure it out later."
Three months later, your app is slow. Queries take 10 seconds. Adding features requires database schema migrations that take days to test.
Database design isn't storage. It's the foundation your entire product is built on. Get it wrong, and everything suffers.
This guide covers 10 database design mistakes startups make—and how to avoid them.
Mistake #1: Choosing Wrong Database Type
Startups pick databases because of hype, not needs.
SQL vs NoSQL: The Real Choice
Choose SQL (PostgreSQL, MySQL) If:
- Your data has clear relationships (users → orders → items)
- You need transactions (financial data, inventory)
- You want ACID compliance (consistency matters)
- You need complex queries (joins, aggregations)
- You want mature tooling and documentation
Choose NoSQL (MongoDB, Redis) If:
- Your data is unstructured or document-heavy
- You need flexible schema (rapid iteration)
- You need high write throughput or horizontal scaling
- Your use case is specific (caching, sessions, queues)
- You're building MVP and uncertain about final schema
Startup Reality: 90% of startups should start with PostgreSQL. It's flexible enough for most use cases but structured enough to prevent chaos.
SQL or NoSQL Decision Framework
Need | SQL Better | NoSQL Better |
|---|---|---|
Clear relationships | ✅ | ❌ |
Complex queries/joins | ✅ | ❌ |
Flexible schema | ❌ | ✅ |
Horizontal scaling (sharding) | Hard | Easy |
Developer availability | ✅ | ⚠️ |
Learning curve | Easy | Medium |
Mistake #2: No Schema Planning
"Just save JSON and figure it out later."
This is the fastest way to create technical debt.
What Happens Without Schema Planning
Day 1:
json{"user": {"name": "John",}}
Week 4:
json{"user": {"name": "John","firstName": "John", // Added this"phone": "+1234567890", // Added this"address": {// Nested now"street": "123 Main St","city": "NYC"}}}
Month 3:
- Some users have
name, some havefirstName/lastName - Some have
phone, some don't - Addresses are nested objects, strings, or arrays
- Can't query reliably without complex workarounds
Solution: Design Schema First
Step 1: Define tables and relationships Step 2: Use ORM (Prisma, TypeORM, Sequelize) for migrations Step 3: Add indexes for common queries Step 4: Use constraints (NOT NULL, UNIQUE, FOREIGN KEY) for data integrity
Example with Prisma:
prismamodel User {id String @id @default(cuid())email String @uniquefirstName StringlastName Stringphone String?address Address?createdAt DateTime @default(now())updatedAt DateTime @updatedAt@@map("users")}model Address {id String @id @default(cuid())street Stringcity Stringstate String?zip String?userId Stringuser User @relation(fields: [userId], references: [id])@@map("addresses")}
Mistake #3: Missing Indexes
Indexes are like Google for your database. Without them, queries scan entire tables.
What Happens Without Indexes
Table: users with 1,000,000 rows
Query without index:
sql
Result: Scans all 1,000,000 rows → 500ms-2s per query
Query with index:
sqlCREATE INDEX idx_users_email ON users(email);
Result: Jumps directly to row → 1-5ms per query
Performance Impact: 100x-400x faster
Index Best Practices
Always Index:
- Foreign keys (user_id, order_id, etc.)
- Unique constraints (email, username)
- Common WHERE clauses (status, created_at, category)
- Columns used in ORDER BY or JOIN conditions
Don't Over-Index:
- Every column (indexes slow down writes)
- Columns with low cardinality (boolean flags with 90% same value)
- Columns rarely queried
Rule of Thumb: Indexes should cover 80%+ of your query patterns.
Mistake #4: Not Using Foreign Keys
Foreign keys enforce relationships and prevent orphaned data.
Example: Orders Without Foreign Keys
sqlCREATE TABLE orders (id SERIAL PRIMARY KEY,user_id INT,total DECIMAL(10,2)-- No foreign key constraint!);
What Happens:
- User deleted but their orders remain (orphaned data)
- Orders with non-existent user_ids
- Can't cascade deletes or updates
Solution: Add Foreign Keys
sqlCREATE TABLE orders (id SERIAL PRIMARY KEY,user_id INT REFERENCES users(id) ON DELETE CASCADE,total DECIMAL(10,2));
Benefits:
- Referential integrity enforced automatically
- Orphaned data prevented
- Cascading deletes (delete user → all orders deleted)
- Clearer data relationships
Mistake #5: Ignoring Normalization
Normalization reduces redundancy and prevents data anomalies.
Example: Non-Normalized Orders
sqlCREATE TABLE orders (id SERIAL PRIMARY KEY,user_name VARCHAR(100), -- Duplicates user datauser_email VARCHAR(100), -- Duplicates user dataproduct_name VARCHAR(100), -- Duplicates product dataproduct_price DECIMAL(10,2), -- Duplicates product dataquantity INT);
Problems:
- User name/email repeated in every order
- Product info repeated in every order
- Update user email → must update 100s of orders
- Delete product → all order data lost
Solution: Normalize Tables
sqlCREATE TABLE users (id SERIAL PRIMARY KEY,name VARCHAR(100),email VARCHAR(100) UNIQUE);CREATE TABLE products (id SERIAL PRIMARY KEY,name VARCHAR(100),price DECIMAL(10,2));CREATE TABLE orders (id SERIAL PRIMARY KEY,user_id INT REFERENCES users(id),product_id INT REFERENCES products(id),quantity INT,created_at TIMESTAMP DEFAULT NOW());
Benefits:
- No data duplication
- Single source of truth for users, products, orders
- Updates and deletes are simple and safe
Mistake #6: Using UUIDs for Everything
UUIDs are popular but overused.
When to Use UUIDs
Use UUIDs If:
- You need globally unique IDs across databases
- You're using NoSQL databases
- You need non-sequential IDs for security
- Your app is distributed across multiple servers
Don't Use UUIDs If:
- You're using relational database with single instance
- Performance is critical
- Storage size matters
Performance Comparison
Integer IDs (SERIAL/BIGSERIAL):
- Storage: 4-8 bytes
- Index size: Small
- Insert performance: Excellent (auto-increment)
- Fragmentation: Minimal
UUIDs:
- Storage: 16 bytes (4x larger)
- Index size: 4x larger
- Insert performance: Poor (random insertion causes fragmentation)
- Fragmentation: High
Startup Reality: For MVP with single database, auto-increment integers are better.
Mistake #7: Not Planning for Migration
Database migrations are inevitable. Plan for them.
Common Migration Problems
Hard-Coded Schema Changes:
sql-- Developer runs this manually in productionALTER TABLE users ADD COLUMN phone VARCHAR(20);
Problems:
- No way to roll back
- Different schema across environments
- Team doesn't know what changes exist
Solution: Use Migration Tools
Prisma Example:
bash# Create migrationnpx prisma migrate dev --name add_phone_to_users# Rollback if needednpx prisma migrate resolve --rolled-back add_phone_to_users# Apply to productionnpx prisma migrate deploy
Benefits:
- Version controlled migrations
- Rollback capability
- Consistent schema across environments
- Team collaboration on schema changes
Mistake #8: Ignoring Data Types
Using wrong data types causes issues later.
Common Data Type Mistakes
1. Using VARCHAR for Everything:
sql-- BadCREATE TABLE orders (id VARCHAR(36), -- UUID as stringtotal VARCHAR(20), -- Money as stringcreated_at VARCHAR(30) -- Date as string);
sql-- GoodCREATE TABLE orders (id SERIAL PRIMARY KEY,total DECIMAL(10,2), -- Proper money typecreated_at TIMESTAMP -- Proper date type);
2. Using BOOLEAN Flags Extensively:
sql-- Bad for high-cardinality datais_premium BOOLEAN,is_admin BOOLEAN,is_verified BOOLEAN-- 100+ boolean flags → poor query performance
sql-- Good: Use integer or lookup tableuser_type INT, -- 0=regular, 1=premium, 2=adminroles TEXT[] -- Array of roles
3. Storing JSON in Columns:
sql-- Sometimes okay, but often overusedmetadata JSONB -- Store arbitrary data
Use JSON Only If:
- Data structure is truly flexible
- You don't need to query within JSON
- Schema varies significantly per row
Better: Extract to proper columns and use foreign keys.
Mistake #9: Not Adding Constraints
Constraints prevent bad data at database level.
Essential Constraints
NOT NULL:
sqlCREATE TABLE users (email VARCHAR(100) NOT NULL, -- Must have emailcreated_at TIMESTAMP NOT NULL -- Must have created date);
UNIQUE:
sqlCREATE TABLE users (email VARCHAR(100) UNIQUE, -- One email per userusername VARCHAR(50) UNIQUE -- One username per user);
CHECK:
sqlCREATE TABLE orders (total DECIMAL(10,2) CHECK (total >= 0), -- Can't be negativequantity INT CHECK (quantity > 0) -- Must be positive);
DEFAULT Values:
sqlCREATE TABLE users (status VARCHAR(20) DEFAULT 'active', -- Default statuscreated_at TIMESTAMP DEFAULT NOW() -- Auto-set created date);
Benefits:
- Bad data rejected at database level
- App logic simplified (don't need to validate everywhere)
- Clear data contracts
Mistake #10: Not Testing Schema Changes
Deploying schema changes to production without testing is dangerous.
Safe Schema Change Process
1. Write Migration in Development:
bashnpx prisma migrate dev --name add_payment_status
2. Test in Staging:
- Run migration on staging database
- Run full test suite
- Manually test affected features
3. Review Migration SQL:
bashnpx prisma migrate diff --from-empty --to-schema-dml
4. Create Rollback Plan:
sql-- Write rollback migrationALTER TABLE orders DROP COLUMN payment_status;
5. Deploy to Production:
- Deploy during low-traffic period
- Monitor for errors
- Have rollback ready if issues
6. Monitor After Deployment:
- Check query performance
- Monitor error rates
- Verify data integrity
Database Design Checklist
Use this checklist before launching MVP:
Schema Design:
- Tables normalized (3NF)
- Foreign keys defined
- Indexes on common queries
- Proper data types used
- NOT NULL and UNIQUE constraints
- DEFAULT values where appropriate
Performance:
- Indexes cover 80%+ of queries
- No SELECT * (only select needed columns)
- Pagination implemented (LIMIT/OFFSET)
- N+1 queries identified and addressed
- Caching strategy defined (Redis, application-level)
Migration:
- Migration tool configured (Prisma, TypeORM, etc.)
- Rollback migrations written
- Schema changes tested in staging
- Deployment window planned
Integrity:
- Transactions used for multi-step operations
- Cascading deletes configured appropriately
- Data validation at database level
- Backup and restore procedures tested
Common Database Tools for Startups
ORM / Query Builders
- Prisma: Type-safe, excellent migrations, modern choice
- Drizzle: Lightweight, type-safe, SQL-first
- TypeORM: Mature, feature-rich, TypeScript-native
- Knex.js: SQL query builder, mature
Database Management
- pgAdmin / DBeaver: GUI for PostgreSQL
- Supabase: PostgreSQL hosting with built-in tools
- Neon: Serverless PostgreSQL
- Railway / Render: Easy PostgreSQL hosting
Monitoring & Performance
- Prisma Studio: Visualize and edit data
- pg_stat_statements: Built-in PostgreSQL query analyzer
- Datadog / New Relic: Database monitoring
- Explain.depert.com: Analyze EXPLAIN output
Related Reading
If you found this helpful, you might also enjoy:
- Why Startups Build Wrong Architecture - Architecture mistakes
- Technical Debt: When Fine vs Dangerous - Manage debt
- Architecture Decision Records for Startups - Document decisions
- How to Evaluate Codebase You Didn't Build - Review existing
Need Help with Database Design?
At Startupbricks, we've helped dozens of startups design databases that scale. We know what mistakes to avoid, how to plan schemas, and what indexes to add.
Whether you need:
- Full database schema design
- Performance optimization
- Migration planning and execution
- Database architecture review
Let's talk about building database that supports your growth.
Ready to design right? Download our free Database Design Checklist and start planning today.
