LearnThatStack Ace your next interview

Database Design

Relational design on PostgreSQL, MySQL and SQL Server Checked Oct 2026 19 code snippets

The model

  • Start from the workflows and their queries. Entities become tables, and relationships become foreign keys or junction tables.
  • Normalize so each fact lives in one place. Denormalize on purpose, where reads need fewer joins.
  • Put the rules in the database. Constraints and foreign keys reject bad data from every application.
  • An index speeds up reads that filter, join or sort on its columns, and slows every write.
  • Replicas copy data for more reads and for failover. Shards split data that outgrows one server.
customerorderpaymentaddressorder_itemproduct
online storeEach arrow is a foreign key, from the child row to the parent it points at.
A customer has many addresses and orders, and an order can have many payments. Orders and products are many-to-many, so order_item links them.

DBMS, SQL & schema

what a database system does, and how it is layered

SQL command groups

Create, change or drop tablesDDL, the Data Definition Language, with CREATE, ALTER, DROP and TRUNCATE.
Read and change rowsDML, the Data Manipulation Language, with SELECT, INSERT, UPDATE and DELETE.
Grant or revoke accessDCL, the Data Control Language, with GRANT and REVOKE.
Keep or undo a transaction's changesTCL, the Transaction Control Language, with COMMIT, ROLLBACK and SAVEPOINT.

TrapTRUNCATE removes rows, but it is DDL, not DML.

Schema and its three levels

Give each user their own view of the dataThe external, or view, level.
Describe all the data and how it relatesThe conceptual, or logical, level, shared by all users.
Describe how the data is storedThe internal, or physical, level, with files and indexes.

TipA schema defines tables, columns, relationships and constraints. Each ANSI-SPARC level can change without affecting the one above.

Model the data

from entities and relationships to tables

From ER model to tables

Draw an entity, an attribute and a relationshipA rectangle, an oval and a diamond.
Name a link's cardinalityHow many instances can link on each side, written 1:1, 1:M or M:N.
Map an entity to a tableIts identifier becomes the primary key, and its attributes become columns.
Map one-to-one, like a user and their profileA foreign key in either table, or merge the tables.
Map one-to-many, like a customer and their ordersA foreign key on the many side, like orders.customer_id.
Map many-to-many, like students and coursesA junction table with a foreign key to each side.
Link a table to itselfA self-referencing key, like manager_id pointing at employee_id.

TipSplit a composite attribute, like an address, into columns. Give a multivalued one its own table.

Conceptual, logical and physical models

Agree on entities with the businessA conceptual model, with no technical detail.
Add attributes and keysA logical model, still independent of any product.
Build the model in one database engineA physical model, with tables, indexes and constraints.

TipWork top-down for a new system, and bottom-up from a legacy system's data. Complex systems mix both.

TrapIn the three-level architecture, conceptual and logical name one level. In data modeling, they are two models.

Design an e-commerce schema

List the core tablesCustomers, addresses, products, categories, orders, order items, payments and inventory.
Store prices and totalsunit_price DECIMAL(10, 2) NOT NULLUse DECIMAL, not a float, which stores some values only approximately.
Index a customer's orders by dateCREATE INDEX idx_orders_customer ON orders (customer_id, created_at);

TrapStore each item's unit price at the time of purchase. Otherwise a later price change rewrites past orders.

Keys & constraints

identify each row and keep bad data out

Kinds of keys

Identify each rowA primary key, unique and never NULL. A table has one.
Find each column set that could be the primary keyCandidate keys, unique and minimal. The ones you don't pick are alternate keys.
Name any column set that identifies a rowA super key, like the primary key plus extra columns.
Point at a row in another tableA foreign key. It may be NULL unless you add NOT NULL.
Keep values unique but allow NULLA unique key. PostgreSQL allows many NULL values, and SQL Server allows one.

TipA composite key is a primary key of several columns, like (order_id, product_id).

Natural or surrogate key

Use a value from the data as the key, like an ISBNA natural key. If it changes, every referencing table must change.
Use a generated value with no meaning as the keyA surrogate key, like an auto-increment id. It never has to change.
Generate ids anywhere without a central sequenceA UUID. Use time-ordered UUIDv7, since random v4 values scatter across the index.

TrapA surrogate key doesn't stop duplicate rows. Keep a UNIQUE constraint on the natural key too.

Constraints

Reject a price of zero or lessALTER TABLE products ADD CONSTRAINT chk_price CHECK (price > 0);
Reject a second user with the same emailALTER TABLE users ADD CONSTRAINT uk_email UNIQUE (email);

TipThe database rejects a bad write from every application, not only yours.

TrapA complex CHECK runs on every insert and update, so keep it cheap.

Referential integrity

Say what referential integrity meansEvery foreign key is NULL or matches an existing parent row, so no row is orphaned.
When a parent is deleted, delete or unlink its childrenON DELETE CASCADEON DELETE SET NULLCASCADE deletes them. SET NULL keeps them with a NULL key.
Block a parent delete while children existON DELETE RESTRICTNO ACTION, PostgreSQL's default, also blocks it. A deferrable one checks at commit.

TrapPostgreSQL doesn't index a foreign key for you. Without that index, each parent delete scans the child table.

Normalize & denormalize

store each fact once, then copy only on purpose

Normalization and normal forms

Say what normalization preventsUpdate anomalies. Each fact lives in one place, so you change it once.
Name the cost of normalizingMore tables and more joins, which can slow reads.
Fix a column that holds two phone numbers1NF needs atomic values. Give phones a table, one row per number.
Fix product_name in order items keyed by order and productIt breaks 2NF, since it depends on only part of the key. Move it to products.
Fix dept_name stored in employeesIt breaks 3NF, since it depends on dept_id, not the key. Move it to departments.
Reach BCNFEvery determinant, a column set that decides another column, must be a candidate key.

TipEach form needs the one before it. Only a table with a composite key can break 2NF.

When to denormalize

Decide when to denormalizeWhen reads far outnumber writes, and joins are too slow for your response time.
Skip the category join on product pagesStore the category name on each product row.

TipNormalize first. Denormalize only where measurements show a need.

TrapEvery update must now keep each copy in sync, so writes get more complex.

Indexes & query speed

find rows fast, and fix a slow query

How an index works

Find rows without scanning the whole tableAn index, usually a B-tree, kept sorted with a pointer to each row.
Tell a clustered index from a non-clustered oneA clustered index stores the rows in key order, one per table. Non-clustered ones point to the rows.
Name what an index costsExtra storage, and slower writes, since every index must change too.

When to add an index

Pick columns to indexWHERE, JOIN and ORDER BY columns, foreign keys, and columns with many distinct values.
Choose between indexing email and statusIndex email. A three-value status has low selectivity and barely narrows the search.
Know when to skip an indexOn small tables, write-heavy tables and columns that change often.

TipMeasure before you add an index, and drop the ones nothing uses.

Column order in a composite index

CREATE INDEX ix ON orders (customer_id, order_id);WHERE customer_id = 42uses the indexWHERE customer_id = 42 AND order_id = 1001uses the indexWHERE order_id = 1001leading column missing
Say which queries an index on (a, b, c) servesQueries that filter on its leading columns, the leftmost prefix rule.
Order the columns for a = 5 AND b > 42Equality columns first, then the range or sort column.

TipAn index on (a, b) also serves a alone, so a separate index on a is usually redundant.

Speed up a slow query

See how the database runs a queryEXPLAIN SELECT * FROM orders WHERE customer_id = 42;Look for a full table scan.
Filter one year so an index can helpWHERE OrderDate >= '2023-01-01' AND OrderDate < '2024-01-01'Sargable, meaning an index can serve it. YEAR(OrderDate) = 2023 can't.
Answer a query from the index aloneA covering index holds every selected column, so the table is never read.

TrapDon't reorder joins or WHERE conditions by hand. The optimizer picks the order itself.

Transactions & locking

keep data correct when many users write at once

ACID

Make a transfer all or nothingAtomicity. Every step succeeds, or the whole transaction rolls back.
Keep every constraint trueConsistency. The data is valid before and after.
Stop concurrent transactions from interferingIsolation. Each runs as if it were alone.
Keep committed data after a crashDurability. A committed change is permanent.

Isolation levels

READ UNCOMMITTEDRead changes nobody has committed yet
READ COMMITTEDThe default in most databases, PostgreSQL included.See only committed data
REPEATABLE READMySQL InnoDB's default. PostgreSQL also blocks phantoms here.Get the same row back when you read it again
SERIALIZABLEAct as if transactions ran one at a time
preventedcan happen

TipA dirty read sees uncommitted data. A non-repeatable read gets a changed row. A phantom read finds new rows.

Optimistic or pessimistic locking

Lock a row while you change itSELECT * FROM Account WHERE AccountID = 1 FOR UPDATE;Pessimistic. Other writers wait for your transaction.
Update only if nobody changed the row since you read itSET Version = Version + 1 WHERE AccountID = 1 AND Version = 2Optimistic. Each row carries a version number.

TipUse pessimistic locking for high contention, and optimistic when conflicts are rare.

TrapAn update that matches zero rows raises no error. Check the row count, then read again and retry.

Deadlocks

Spot a deadlockTwo transactions each wait for a lock the other holds.
Prevent deadlocks by designLock ordering. Every transaction locks in the same global order.
Let the database break a deadlockDeadlock detection rolls back one victim, and most databases use it. A lock timeout is simpler.

TrapThe victim gets an error. Your code must catch it and retry.

MVCC

reader Areads 1000reads 1000reads 1000writer Bwrites 900commitstime

A runs at Repeatable Read. Its reads keep seeing 1000, even after B commits 900, because B wrote a new row version. Neither waits for the other.

Let reads and writes run side by sideKeep several versions of each row. Reads and writes don't block each other.
Say which version a reader seesIts snapshot, per statement, or per transaction at Repeatable Read.

TipOld versions take storage until garbage collection removes them. PostgreSQL calls it VACUUM.

TrapWriters still lock the rows they change. A second writer to the same row waits.

Views, CTEs & triggers

queries and code the database keeps for you

Views and materialized views

Give a team only the columns it needsCREATE VIEW customer_contacts AS SELECT name, email FROM customers;
List what views give youSecurity, simpler queries, data independence and shared logic.
Insert or update through a viewOnly an updatable view, usually one table with no aggregates.
Store a slow query's resultA materialized view, for slow reports and frequently used joins. You can index it.
Refresh a materialized viewREFRESH MATERIALIZED VIEW monthly_sales;A full rebuild. Oracle can also refresh incrementally.

TrapA plain view stores no data, so it isn't faster. A materialized view is only as fresh as its last refresh.

Common table expressions

Name the steps of a complex queryA CTE, defined with WITH. The statement reads it like a table, as often as it needs.
Walk a tree, like an org chartWITH RECURSIVEAn anchor picks the top rows, then a recursive part joins back with UNION ALL.

TrapA CTE lasts for one statement. The next statement can't read it.

Triggers

Run code on every insert, update or deleteA DML trigger runs BEFORE, AFTER or INSTEAD OF the change. SQL Server has no BEFORE, and MySQL no INSTEAD OF.
React to schema changes or new sessionsA DDL trigger fires on CREATE, ALTER or DROP, and a logon trigger at session start. MySQL has neither.

TipUse triggers for audit logs and rules every application must follow.

TrapA trigger slows every write to its table. Keep heavy or risky work in the application.

OLTP, OLAP & warehouses

run the business, then analyze it somewhere else

OLTP or OLAP

Process orders and payments as they happenOLTP. Many short writes from many users, on current, normalized data.
Analyze sales over several yearsOLAP. Complex queries scan and aggregate history, often on denormalized tables.
Stop reports from slowing down transactionsRun them on a separate system, like a data warehouse.

TipOLTP is Online Transaction Processing. OLAP is Online Analytical Processing.

Star or snowflake schema

dateproductsalescustomerstore
starEach dimension is one flat table, one join away from the fact table.
dateproductsalescustomercategorystoredepartment
snowflakeThe product dimension splits into a chain of tables, so queries need more joins.
Store each sale's quantity and amountA fact table, with a foreign key to each dimension.
Pick a schema for most analyticsA star. Queries are simpler and usually faster, and Kimball recommends it.
Pick a snowflake schema insteadFor a big hierarchy that changes often, like product, category and department.

TrapA star repeats values like category names. Changing one means updating many rows.

Scaling, CAP & NoSQL

split and copy data, and choose what to give up

Partition a big table

Split a big table on one serverPartitioning. Queries still see one table, and the engine routes each row.
Split rows, like orders by dateHorizontal partitioning, by range, list or hash of the partition key.
Split columns, or a database by featureVertical partitioning splits a table's columns. Functional partitioning splits a database by feature.
Read only the partitions a query needsPartition pruning. It needs a filter on the partition key.
Remove a year of old orders fastDrop that year's partition instead of deleting rows.

TipPartition at millions of rows, or when a table outgrows memory.

TrapA query that skips the partition key reads every partition. Too many partitions also slow planning.

Shard across servers

Spread data across several serversSharding. The app, middleware or a router like mongos sends each query to its shard.
Shard by ranges of the keyRange sharding, like users A to M on one shard. Uneven data makes hot spots.
Spread rows evenly across shardsHash sharding. Range queries become hard.
Add a shard without moving most of the dataConsistent hashing. Only part of the data moves.

TipDirectory sharding maps keys with a lookup service, which can become a bottleneck.

TrapPick the shard key from your queries. Cross-shard joins and transactions are slow and complex.

Replication

Send writes to one server and reads to manySingle-leader replication. The primary is a single point of failure.
Accept writes on several serversMulti-leader replication must resolve conflicts. Leaderless, as in Cassandra, lets any replica take writes.
Choose whether a write waits for its replicasSynchronous is consistent but slower. Asynchronous is faster but eventually consistent.

TipOlder docs say master-slave. MySQL now says source and replica.

TrapAn asynchronous replica lags behind. A read from it right after a write may miss that write.

CAP theorem

Define C, A and PEvery read gets the latest write or an error. Every working node answers. The system survives lost messages.
Choose what to give up during a network partitionCP refuses some requests to stay consistent. AP answers, maybe with stale data.
Make reads see the latest writeUse majority writes and linearizable reads in MongoDB, QUORUM in Cassandra, and ConsistentRead in DynamoDB.

TipPACELC adds that with no partition, systems trade latency against consistency.

TrapDon't say "pick two of three". No distributed system can rule out partitions, so you choose C or A during one.

SQL or NoSQL

Pick NoSQLFor horizontal scaling, or a structure that changes too often for a fixed schema.
Pick a relational databaseFor complex joins, and money or orders that need strong consistency.
Name the four NoSQL familiesDocument like MongoDB, key-value like Redis, column-family like Cassandra, and graph like Neo4j.

TrapACID no longer settles the choice. MongoDB supports multi-document transactions across collections and shards.

Pools, backups & security

run the database safely in production

Connection pooling

Stop opening a connection per requestUse a connection pool. It keeps connections open and lends them out again.
Size the poolBase it on the server's capacity, not the user count. A few dozen connections can serve thousands.

TipHikariCP suggests twice the server's CPU cores, plus the disks that serve reads. Count zero disks when the active data is cached.

TrapAlways return the connection to the pool, even when a query fails. Otherwise the pool runs out.

Backup and recovery

Set the recovery targetsRPO is the most data you can lose. RTO is the longest downtime you can accept.
Copy changes since the last backupIncremental. It is the smallest, but a restore needs the full backup and every incremental.
Copy changes since the last full backupDifferential. A restore needs the full backup and the latest differential.
Restore to a point in timeTake log backups too. SQL Server needs its full recovery model for that.

TipFollow the 3-2-1 rule. Keep 3 copies, on 2 kinds of media, with 1 offsite.

TrapA backup you have never restored may not work. Test restores on a schedule.

Database security

Give the app only the access it needsGRANT SELECT, INSERT, UPDATE ON app_db.orders TO 'app_user'@'localhost';Least privilege, often through roles. The example is MySQL syntax.
Stop SQL injectionUse parameterized queries. User input never runs as SQL.
Protect data on disk and on the networkEncrypt it at rest and in transit, and rotate the keys.

TrapTest copies of production data leak too. Mask the data in non-production environments.

Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 39 questions it links, not just the 5 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.