startupbricks logo

Startupbricks

Database Design Mistakes Startups Make

Database Design Mistakes Startups Make

2025-01-16
6 min read
Technical Decision Making

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",
"email": "[email protected]"
}
}

Week 4:

json
{
"user": {
"name": "John",
"firstName": "John", // Added this
"email": "[email protected]",
"phone": "+1234567890", // Added this
"address": {
// Nested now
"street": "123 Main St",
"city": "NYC"
}
}
}

Month 3:

  • Some users have name, some have firstName/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:

prisma
model User {
id String @id @default(cuid())
email String @unique
firstName String
lastName String
phone String?
address Address?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
}
model Address {
id String @id @default(cuid())
street String
city String
state String?
zip String?
userId String
user 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
SELECT * FROM users WHERE email = '[email protected]';

Result: Scans all 1,000,000 rows → 500ms-2s per query

Query with index:

sql
CREATE INDEX idx_users_email ON users(email);
SELECT * FROM users WHERE email = '[email protected]';

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

sql
CREATE 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

sql
CREATE 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

sql
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_name VARCHAR(100), -- Duplicates user data
user_email VARCHAR(100), -- Duplicates user data
product_name VARCHAR(100), -- Duplicates product data
product_price DECIMAL(10,2), -- Duplicates product data
quantity 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

sql
CREATE 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 production
ALTER 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 migration
npx prisma migrate dev --name add_phone_to_users
# Rollback if needed
npx prisma migrate resolve --rolled-back add_phone_to_users
# Apply to production
npx 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
-- Bad
CREATE TABLE orders (
id VARCHAR(36), -- UUID as string
total VARCHAR(20), -- Money as string
created_at VARCHAR(30) -- Date as string
);
sql
-- Good
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
total DECIMAL(10,2), -- Proper money type
created_at TIMESTAMP -- Proper date type
);

2. Using BOOLEAN Flags Extensively:

sql
-- Bad for high-cardinality data
is_premium BOOLEAN,
is_admin BOOLEAN,
is_verified BOOLEAN
-- 100+ boolean flags → poor query performance
sql
-- Good: Use integer or lookup table
user_type INT, -- 0=regular, 1=premium, 2=admin
roles TEXT[] -- Array of roles

3. Storing JSON in Columns:

sql
-- Sometimes okay, but often overused
metadata 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:

sql
CREATE TABLE users (
email VARCHAR(100) NOT NULL, -- Must have email
created_at TIMESTAMP NOT NULL -- Must have created date
);

UNIQUE:

sql
CREATE TABLE users (
email VARCHAR(100) UNIQUE, -- One email per user
username VARCHAR(50) UNIQUE -- One username per user
);

CHECK:

sql
CREATE TABLE orders (
total DECIMAL(10,2) CHECK (total >= 0), -- Can't be negative
quantity INT CHECK (quantity > 0) -- Must be positive
);

DEFAULT Values:

sql
CREATE TABLE users (
status VARCHAR(20) DEFAULT 'active', -- Default status
created_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:

bash
npx 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:

bash
npx prisma migrate diff --from-empty --to-schema-dml

4. Create Rollback Plan:

sql
-- Write rollback migration
ALTER 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:


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.

Share: