This page is for anyone facing a DBMS round, from campus placements to backend and data roles. Most interviews open with keys and the relational model, move through normalization up to BCNF, then test transactions, isolation levels and locking, and finish with indexes, views, triggers and when NoSQL fits. Senior rounds add a real incident and a judgement call. Writing queries is covered on the SQL page; this one is about how databases work and how to design them. Each question shows what the interviewer is checking, the shape of a strong answer and a short answer you can say out loud.
Search all questions by round, difficulty and level, or save the ones you want to practise.
DBMS: any software that stores and retrieves data with controlled access, concurrency, backup and recovery.
RDBMS: a DBMS built on the relational model: tables of rows and columns, linked by keys, queried with SQL.
What it adds: declared constraints like primary and foreign keys, joins across tables, and transactions.
"A DBMS is any software that manages data for you: storing it, letting many users read and write it safely, controlling who can see what, and handling backup and recovery. An RDBMS is one kind of DBMS that follows the relational model. Data sits in tables, each row is identified by a key, and tables relate to each other through foreign keys. You query it with SQL, and the database enforces rules you declare, like uniqueness or referential integrity. So every RDBMS is a DBMS, but not the other way round. A document store, a key-value store or an old hierarchical system are all DBMSs that aren't relational. PostgreSQL, MySQL and Oracle are typical relational ones."
Saying an RDBMS is just a DBMS with more tables, without mentioning keys, relationships or constraints.
Cartesian product: every row of A paired with every row of B; a join is conceptually a filter on it.
Theta and equi-join: keep the pairs that meet a condition; an equi-join uses equality.
Natural join: an equi-join on every column with the same name, keeping one copy of those columns.
Outer join: also keeps unmatched rows from one or both sides, filling the gaps with nulls.
"I think of the Cartesian product as the starting point: every row of one table paired with every row of the other. A join is that product filtered by a condition. If the condition is any comparison, it's a theta join. If it's equality, like the order's customer_id matching the customer's id, it's an equi-join, which is what most inner joins are. A natural join is an equi-join on every column the two tables share by name, keeping one copy of those columns. It's risky in real schemas, because adding a column with a common name, like created_at, silently changes the join. Outer joins keep rows that found no match. A left outer join keeps every customer, even those with no orders, and fills the order columns with nulls. A full outer join keeps unmatched rows from both sides."
Not knowing that an outer join fills unmatched columns with nulls, or trusting natural joins in production without seeing the risk.
Super key: any set of columns that uniquely identifies a row, even with extra columns thrown in.
Candidate key: a minimal super key; remove any column and it stops being unique.
Primary and alternate: the candidate you pick as the main identifier is the primary key; the rest are alternate keys.
"Take a Student table with student_id, email, dept_id, a roll number that's unique within a department, and name. Any set of columns that uniquely identifies a row is a super key, so student_id alone is one, and so is student_id plus name, even though name adds nothing. A candidate key is a minimal super key: student_id, email, and the pair dept_id plus roll number are all candidates, because dropping any column from them breaks uniqueness. From those I pick one as the primary key, usually student_id, since it's short and never changes. The primary key can't be null and there's only one per table. The other candidates become alternate keys, and I'd still put unique constraints on them so the database enforces them."
Treating super key and candidate key as the same thing, or saying a table can have several primary keys.
Rule: a child's foreign key value must match an existing parent key, or be null if nulls are allowed.
On delete: restrict or no action blocks the delete, cascade removes the children, set null clears the link.
Choice: cascade for data owned by the parent, like order lines; block it for records you must keep.
"A foreign key says the value in this column must exist as a key in the parent table. So an order can't point to customer 42 unless customer 42 exists. The database checks it when a child row is inserted or updated, and when a parent row is deleted or its key changes. When a parent is deleted I have a few choices. Restrict, or no action, refuses the delete while children still point at it. Cascade deletes the children too, which suits data that means nothing without the parent, like the lines of an order. Set null keeps the child and clears the link, which fits an employee whose manager has left. I choose based on what the data means, and I'm careful with cascade because one delete can quietly remove a lot of rows."
CREATE TABLE order_items (
order_id INT NOT NULL REFERENCES orders(order_id) ON DELETE CASCADE,
product_id INT NOT NULL REFERENCES products(product_id) ON DELETE RESTRICT,
qty INT NOT NULL,
PRIMARY KEY (order_id, product_id)
);
Saying foreign keys are just documentation, or using cascade everywhere without thinking about what gets deleted.
Composite: two or more columns together are the identity, common in link tables like enrollment of students in courses.
Surrogate: a generated ID with no business meaning; short, stable and easy for other tables to reference.
Both: with a surrogate, keep a unique constraint on the natural columns so duplicates can't sneak in.
"A composite key fits when the combination of columns really is the identity. An enrollment table linking students to courses is the classic case: the pair student_id and course_id is naturally unique, so I'd make it the primary key, and that also stops the same student being enrolled twice. I'd add a surrogate, like an auto-generated id, when the natural key is wide, might change, or lots of other tables will reference it, because every child would otherwise have to carry all those columns. The trap with surrogates is forgetting the business rule. If I give enrollment its own id, I still add a unique constraint on student_id plus course_id, or two identical enrollments can slip in with different ids."
Adding a surrogate ID and then no unique constraint on the natural key, so duplicate rows get in.
Entities: Member, Book as the title, Copy as the physical item, and Loan.
Cardinality: a book has many copies; a copy has many loans over time; a member has many loans.
Mapping: one-to-many puts a foreign key on the many side; many-to-many becomes its own table.
"First I'd separate Book from Copy, because the library owns several physical copies of one title and it lends copies, not titles. Book has the isbn, title and so on. Copy has a copy_id and a foreign key to its book. Member has a member_id and contact details. Members and copies are many-to-many over time, so I'd model that as a Loan entity with its own attributes: member, copy, loan date, due date and return date. When I map to tables, each one-to-many becomes a foreign key on the many side, so copies carry the isbn and loans carry member_id and copy_id. Authors and books are many-to-many, so that needs a book_author table. Finally I'd make sure a copy can have only one open loan, meaning one with no return date."
Putting books and copies in one table, or storing a comma-separated list of borrowed books inside the member row.
Setup: one wide table repeating facts, like student, course and instructor phone in every enrollment row.
Anomalies: can't add a course with no students; must change the phone in many rows; deleting the last student loses the course.
Fix: split the table so each fact is stored exactly once.
"Picture one table with student_id, student_name, course_id, course_name and instructor_phone, one row per enrollment, keyed on student plus course. An insertion anomaly: I can't record a new course until some student enrolls, because the key needs a student. An update anomaly: the instructor's phone is repeated in every enrollment row, so changing it means updating many rows, and if I miss one the table now disagrees with itself. A deletion anomaly: if the last student drops a course, deleting that row also wipes out everything we knew about the course. All three come from storing more than one fact in a row. Splitting into Student, Course and Enrollment tables stores each fact once, and the anomalies go away."
Saying normalization is mainly about saving disk space, missing that the real cost of redundancy is data that contradicts itself.
1NF: atomic values, no lists or repeating groups inside a cell.
2NF: 1NF, and no non-key column depends on only part of a composite key.
3NF: 2NF, and no non-key column depends on another non-key column.
"Say I start with an Orders table holding order_id, order_date, customer_id, customer_city, and one cell listing several products. For 1NF, no lists in a cell, so I make one row per order and product, keyed on order_id plus product_id, with a quantity column. Now 2NF: product_name depends only on product_id, and the date and customer depend only on order_id. Those are partial dependencies on part of the key, so product details move to a Product table and order-level details to an Order table, leaving OrderItem with order_id, product_id and quantity. Then 3NF: in Order, customer_city depends on customer_id, not directly on order_id. That's a transitive dependency, so city moves to a Customer table. Now each table describes one thing."
Reciting each normal form by rote but getting stuck the moment you're handed a real table to normalize.
BCNF rule: for every non-trivial dependency X to Y, X must be a super key.
3NF gap: 3NF also allows X to Y when Y is part of some candidate key; BCNF does not.
Trade-off: a BCNF decomposition can always be lossless, but it may lose a dependency that then can't be checked in one table.
"BCNF says that for every non-trivial functional dependency, the left side must be a super key. 3NF is a bit looser: it also allows the dependency if the right side is a prime attribute, meaning part of some candidate key. The classic example is a Teaching table with student, course and instructor. Each instructor teaches only one course, so instructor determines course. A student takes a course from one instructor, so student plus course determines instructor. The candidate keys are student plus course, and student plus instructor. Instructor to course passes 3NF because course is part of a key, but instructor isn't a super key, so it breaks BCNF. Splitting into instructor-course and student-instructor is lossless, but the rule that a student has one instructor per course can no longer be checked inside one table."
Saying BCNF is just 'a stricter 3NF' without being able to point at the kind of dependency it forbids.
Must-haves: attributes that never appear on the right side of any dependency must be in every key; here A and C.
Closure: compute the closure of that set; if it reaches every attribute, it's a super key.
Minimal: check no smaller subset works and whether any other key exists.
"I start with attributes that never appear on the right-hand side of a dependency, because nothing can derive them, so they must be part of every key. Here that's A and C. Then I take the closure of A and C together. From A I get B. Now I have B and C, so BC gives me D. D gives me E. So the closure of AC is all five attributes, which makes AC a super key. It's minimal, because A alone only reaches B, and C alone reaches nothing new. And since every key has to contain both A and C, AC is the only candidate key. Knowing that, I can also see the relation isn't in 2NF, because B depends on A alone, which is only part of the key."
Closure of {A, C}:
start {A, C}
A -> B {A, B, C}
BC -> D {A, B, C, D}
D -> E {A, B, C, D, E} all attributes, so AC is a key
Guessing keys without computing a closure, or calling a super key like ABC a candidate key.
Why: hot reads that join many tables or re-aggregate the same data on every request.
How: copy a column into a child table, keep a stored total, or build summary tables for reporting.
Cost: slower and more complex writes, more storage, and the job of keeping copies in sync.
"I denormalize when reads matter much more than writes and the normalized design forces the same expensive joins or sums on every request. A common example is storing the order total on the order row instead of adding up the order lines each time. Copying the customer's name onto an invoice is another, and there it's actually more correct, because an invoice should show the name as it was when it was issued. Reporting schemas, like star schemas in a data warehouse, are denormalized on purpose too. The cost is that one fact now lives in two places, so every write must update both, through the application, a trigger or a scheduled job, and if one path forgets, the data drifts. So I normalize first, measure, and denormalize only the spots that are proven slow."
Denormalizing up front for speed without measuring, or with no plan for keeping the duplicated data consistent.
Atomicity: the debit and the credit both happen, or neither does.
Consistency: the transaction moves the data from one valid state to another; declared rules still hold.
Isolation: concurrent transactions don't see each other's half-finished work.
Durability: once committed, the change survives a crash.
"A transfer is two writes: take the amount out of account A and add it to account B. Atomicity means both happen or neither does. If the server dies after the debit, the database rolls it back so the money doesn't just vanish. Consistency means every rule holds before and after, like a balance can't go below zero, and the total across the two accounts is unchanged. Isolation means another transaction running at the same time, say a balance report, never sees the state where A has been debited but B hasn't been credited yet. Durability means that once I get the commit back, the transfer survives a crash or power cut, because the database has already written it to its log on disk."
Describing consistency as just 'the data is correct', or not knowing what happens to a half-done transfer after a crash.
Dirty read: reading another transaction's uncommitted change, which may still roll back.
Non-repeatable read: reading the same row twice and getting different values because someone committed in between.
Phantom: re-running a range query and getting rows that weren't there before.
Lost update: two transactions read the same value, both write, and one write silently overwrites the other.
"A dirty read is when T1 updates a price and T2 reads the new price before T1 commits. If T1 rolls back, T2 acted on a value that never officially existed. A non-repeatable read is when T1 reads a row, T2 updates it and commits, and T1 reads it again in the same transaction and sees a different value. A phantom is about sets of rows: T1 counts today's orders, T2 inserts a new order and commits, and T1's second count is higher. No existing row changed; a new one appeared. A lost update is when two people read a stock count of ten, each subtracts one and writes nine, and one sale disappears. The standard isolation levels are defined by which of the first three they allow."
Mixing up non-repeatable reads and phantoms, which are about a changed row versus a changed set of rows.
Read uncommitted: dirty reads are possible; rarely a good idea.
Read committed: only committed data is visible, but a row can change between two reads.
Repeatable read: rows you've read stay the same; the standard still allows phantoms.
Serializable: the result is as if transactions ran one at a time; safest, with the most blocking or retries.
"Read uncommitted lets a transaction see other people's uncommitted changes, so dirty reads can happen. Read committed only shows committed data, but if I read a row twice someone may have changed it in between. Repeatable read guarantees rows I've read won't change under me, though the standard still allows phantom rows in range queries. Serializable is the strictest: the outcome is as if transactions ran one after another. Each step up costs concurrency, through more locking or more transactions aborted and retried. For a typical web app I'd keep the database's default, which in many engines is read committed, and protect the few risky spots, like decrementing stock, with a row lock or a serializable transaction for just that operation. Real engines also differ, and some give more protection than the standard minimum."
Saying 'always use serializable to be safe' with no mention of its cost, or not knowing the default level of the database you use.
Conflict: two operations from different transactions on the same item, at least one of them a write.
Definition: swapping only non-conflicting neighbours can turn the schedule into some serial order.
Test: build a precedence graph with an edge Ti to Tj when Ti's conflicting operation comes first; no cycle means serializable.
"A schedule is the interleaved order in which several transactions' reads and writes actually run. Two operations conflict if they come from different transactions, touch the same data item, and at least one is a write, so read-write, write-read or write-write. A schedule is conflict serializable if I can reorder it by swapping only non-conflicting neighbours and end up with a serial schedule. The practical test is a precedence graph: one node per transaction, and an edge from Ti to Tj whenever an operation of Ti conflicts with a later operation of Tj. If the graph has no cycle, the schedule is conflict serializable, and a topological order of the graph gives the equivalent serial order. If there's a cycle, like T1 before T2 on X but T2 before T1 on Y, it isn't."
Schedule: R1(X) W2(X) R2(Y) W1(Y)
R1(X) before W2(X) -> edge T1 -> T2
R2(Y) before W1(Y) -> edge T2 -> T1
Cycle T1 -> T2 -> T1, so not conflict serializable
Counting two reads of the same item as a conflict, or not knowing how to draw the precedence graph.
Write-ahead log: a change is logged before the data page is written, and the commit record is flushed to disk before success is reported.
Redo: after a crash, replay committed changes that may not have reached the data files.
Undo: roll back changes from transactions that never logged a commit.
Checkpoints: limit how much of the log recovery has to read.
"The key idea is the write-ahead log. Before a changed data page is written to disk, the log record describing that change must already be on disk, and a transaction only counts as committed once its commit record has been flushed. The data pages themselves can be written lazily later. In the classic approach, recovery after a crash reads the log. For committed transactions whose changes may not have reached the data files, it redoes them, which gives durability. For transactions that never logged a commit, it undoes any of their changes that did reach disk, which gives atomicity. Checkpoints record a known point so recovery doesn't replay the log from the very beginning. This is also why commits are fast: appending to a log sequentially is cheaper than forcing scattered data pages to disk."
Saying the database writes every change straight into the data files at commit, or having no idea what the log is for.
Shared lock: for reading; many transactions can hold it on the same item.
Exclusive lock: for writing; only one holder, and no other lock can be held on the item meanwhile.
Two-phase locking: a growing phase that only acquires locks, then a shrinking phase that only releases them.
Strict version: hold exclusive locks until commit or abort, which avoids cascading rollbacks.
"A shared lock is taken for reading. Several transactions can hold it on the same row at once, since readers don't interfere with each other. An exclusive lock is for writing, and while one transaction holds it, nobody else can hold any lock on that item. Two-phase locking is the rule that a transaction first only acquires locks, the growing phase, and once it releases any lock it can never acquire another, the shrinking phase. That rule on its own guarantees a conflict serializable schedule. Basic two-phase locking can still let others read data that later rolls back, causing cascading aborts, so real systems use strict two-phase locking and hold exclusive locks until commit or abort. The catch is that it doesn't prevent deadlocks, so the database still has to detect or avoid them."
Confusing two-phase locking with two-phase commit, or thinking it prevents deadlocks.
Cause: T1 holds a lock T2 needs while T2 holds a lock T1 needs.
Detection: the database keeps a wait-for graph, finds the cycle, and rolls back a victim.
Prevention: wait-die or wound-wait use transaction age; in code, lock rows in a consistent order and keep transactions short.
"Say transaction one updates account A and then tries to update account B, while transaction two updates B and then tries A. Each holds an exclusive lock the other needs, so neither can move. Most databases detect this with a wait-for graph, where an edge means one transaction is waiting on another. When they find a cycle, they pick a victim, often the one that's cheapest to undo, roll it back and return an error, so the other can finish. Textbook prevention schemes like wait-die and wound-wait use transaction timestamps to decide who waits and who aborts, so a cycle never forms. In application code, the practical fixes are to always touch rows in the same order, say by ascending account id, keep transactions short, and retry when you get a deadlock error."
Saying the database simply hangs forever, or that adding indexes or servers is the fix for deadlocks.
Name it: a check-then-act race; both requests see the seat as free before either writes.
Pessimistic: lock the seat row while checking and booking, so the second request waits.
Optimistic: one conditional update that only succeeds if the seat is still free, then check the rows changed.
Backstop: a unique constraint so the same seat can't be booked twice.
"This is a check-then-act race. Both requests read that the seat is free, both decide to book, and both write. The fix is to make the check and the write one atomic step. One way is pessimistic: inside a transaction, select the seat row for update, which takes a row lock, so the second request waits until the first commits and then sees the seat is taken. The other way is optimistic: a single update that books the seat only where it's still free, then I check how many rows changed. If it's zero, someone got there first and I tell the user. Under light contention the optimistic version holds locks only for a moment and scales well. Either way I'd back it with a unique constraint on flight and seat wherever bookings are stored, so even a bug elsewhere can't create a double booking."
UPDATE seats
SET booked_by = 1042, booked_at = CURRENT_TIMESTAMP
WHERE flight_id = 77 AND seat_no = '14C' AND booked_by IS NULL;
-- 1 row changed: booked. 0 rows: someone else got it first.
Proposing a check in application code followed by an insert, which has the same race, or locking the whole table.
Wide and shallow: each node is a page holding many keys, so even huge tables need only a few page reads.
Linked leaves: all entries sit in sorted leaves linked together, so range scans and sorted output are cheap.
Versus hash: a hash index is fast for exact matches but can't serve ranges or ordering.
"Indexes live on disk, and the expensive part is reading a page, so the structure should touch as few pages as possible. A binary search tree has only two children per node, so it gets very deep. A B+ tree node is a whole page holding many keys, often hundreds, so the tree is wide and shallow, and finding one row among millions might take three or four page reads, with the top levels usually cached in memory. In a B+ tree the entries sit only in the leaves, and the leaves are linked in sorted order. So after finding the start of a range, like all orders in March, I just walk along the leaves. A hash index is great for exact matches, but it scatters keys, so it can't answer a range or return sorted output. The tree also stays balanced as rows come and go."
Saying an index makes every query faster, or not being able to explain why a hash index can't serve a range query.
Clustered: decides the stored order of the rows; its leaf level is the table data itself.
Non-clustered: a separate structure holding the key and a pointer back to the row.
Count: one clustered index per table, since rows can be stored in only one order; many non-clustered ones.
"A clustered index defines the order in which the table's rows are actually stored. Its leaf level is the data itself, so a table can have only one, because rows can only be physically ordered one way. Engines differ: MySQL's InnoDB clusters the table on its primary key, while PostgreSQL keeps rows in a heap, so all its indexes are non-clustered. A non-clustered index is a separate structure: it keeps the indexed values in sorted order plus a pointer back to the full row, either a row address or the clustered key. A table can have many of these. The difference shows up on lookups. With a non-clustered index the database finds the key, then has to go and fetch the rest of the row, unless the index already contains every column the query needs. That's called a covering index, and it skips the extra fetch."
Saying a table can have several clustered indexes, or thinking clustered just means the index is stored in the same file.
Cost: every index slows inserts, updates and deletes and takes disk and memory.
Evidence: index for the queries you actually run; start from the slowest and most frequent, and read their plans.
Better indexes: one well-ordered composite index often beats several single-column ones; drop what goes unused.
"I'd agree with the goal and push back on the method. Every index is another structure the database has to update on each insert, update and delete, so on a busy orders table it directly slows writes, and it takes disk and memory. Single-column indexes on everything also tend to go unused. For a query that filters on customer and status and sorts by date, one composite index on those columns in the right order does far more than three separate ones. A column like status with only a few values rarely helps on its own. So I'd suggest we pull the slowest and most frequent queries, look at their execution plans, and add a small number of indexes that serve them. Then after a week we check the index usage numbers and drop anything that isn't pulling its weight."
Agreeing to index everything, or refusing any new index without looking at the actual queries.
View: a saved query with no data of its own; it runs against the base tables each time.
Materialized view: stores the result; fast to read, but stale until it's refreshed.
Updates: simple single-table views are usually updatable; views with grouping, distinct or complex joins generally aren't.
"A view is a named, stored query. It holds no data; when I select from it, the database runs the query against the base tables, so it's always current. I use views to hide complex joins, to keep a stable interface while tables change underneath, and for security, like exposing only some columns to a reporting user. A materialized view actually stores the result, so reads are fast, which suits expensive summaries. The catch is it goes out of date and has to be refreshed, on a schedule or on demand, depending on the database. On updates, a simple view over one table with no grouping or distinct can usually be written through, and the change lands in the base table. Views with aggregates or complex joins generally can't be, unless you add something like an instead-of trigger."
Saying a normal view stores a copy of the data, or that it makes the underlying query faster by itself.
Stored procedure: named code you call explicitly; can run several statements as one operation.
Function: returns a value and can be used inside a query.
Trigger: fires automatically before or after an insert, update or delete.
Caution: triggers are hidden side effects, hard to see, test and debug.
"A stored procedure is named code in the database that I call explicitly. It can run many statements and is handy for a multi-step operation that should live next to the data. A function returns a value, so I can use it inside a query, in the select list or the where clause, and databases usually limit what side effects it can have. A trigger isn't called at all; it fires by itself when rows are inserted, updated or deleted. I'd use a trigger for things that must happen no matter which application writes the data, like writing an audit row or stamping an updated_at column. Teams are wary because triggers are hidden. Someone runs a simple update and three other tables change, bulk loads slow down, and none of it shows in the application code. So I keep triggers small and documented."
Not knowing that a trigger fires on its own, or building core business logic out of chained triggers nobody can trace.
Relational: defined schema, joins, constraints and multi-row transactions; fits connected, consistency-critical data.
NoSQL families: document, key-value, wide-column and graph, each built for a particular access pattern.
Decide by: data shape, query patterns, consistency needs and scale, not by fashion.
"I start from the data and how it'll be read. If it's highly connected, like customers, orders, payments and stock, and I need joins, constraints and transactions across rows, a relational database is the natural fit. NoSQL isn't one thing. Document stores suit records that are read and written as a whole, like a product with nested attributes. Key-value stores are great for caches and sessions. Wide-column stores handle very high write volumes, and graph databases suit relationship-heavy queries. Many NoSQL systems scale out across machines more easily, often by giving up joins or strict consistency. For most business apps my default is relational, because modern relational databases scale further than people expect and many handle semi-structured data in JSON columns. I'd pick NoSQL for a specific access pattern, not because it sounds more scalable."
Saying NoSQL means no schema at all, or that it is always faster and more scalable than a relational database.
Three properties: consistency (every read sees the latest write), availability (every request to a live node gets an answer), partition tolerance (the system keeps running when the network splits).
Real choice: partitions will happen, so during one you choose between consistency and availability.
Beyond CAP: with no partition, there's still a trade-off between latency and consistency.
"CAP is about a data store spread over several machines. Consistency here means every read sees the most recent write, as if there were only one copy. Availability means every request to a working node gets a non-error answer. Partition tolerance means the system keeps running when messages between nodes are lost. The theorem says you can't have all three while a partition is happening. Since networks do fail, partition tolerance isn't really optional, so the real choice is what happens during a partition: refuse or delay some requests to stay consistent, or keep answering and accept that some reads may be stale. A ledger of account balances leans consistent; a shopping cart or a social feed might lean available and reconcile later. With no partition you can have both, though stronger consistency usually costs latency."
Saying 'pick any two of three' as if a real distributed system could simply drop partition tolerance.
Clarify: which part is slow, at what load, and is it the database at all?
Scale first: fix queries and indexes, add caching, read replicas and partitioning.
Fit: orders, payments and stock need transactions and constraints; move only the parts that suit another model.
"I'd start by asking what problem we're actually seeing, because 'doesn't scale' usually means one slow page or one hot table, and sometimes it isn't the database at all. For an ordering system, the core data, orders, payments and stock, needs multi-row transactions and constraints, which is exactly what a relational database does well. Moving that to a store with weaker guarantees means rebuilding those guarantees in application code, and that's where bugs like overselling come from. Before any rewrite I'd check query plans and indexes, add caching for hot reads, add read replicas for read-heavy traffic, and consider partitioning very large tables. If one piece fits another model better, like session data in a key-value store, I'd move just that piece. And I'd propose measuring first, so we decide on numbers, not on a slogan."
Either agreeing to a full rewrite on a hunch, or dismissing NoSQL outright without asking what the bottleneck is.
Context: what the system stored and the main things it had to answer.
Good call: one decision that paid off, and why.
Regret: what hurt later, how you fixed it, and the habit you keep now.
"In my final-year project I designed the schema for a hostel booking system: students, rooms, beds and bookings. One decision that held up was making a bed its own table instead of just a capacity number on the room. When the warden asked to see who was in which bed and to block beds for repairs, it was a small change. The decision I'd change was storing booking status as free text. Different screens wrote Confirmed with different capitalisation, and the reports undercounted. I fixed it with a status lookup table and a foreign key, plus a script to clean the old rows. Now, any column that holds a fixed set of values gets a check constraint or a lookup table from day one, because cleaning bad data later costs far more than preventing it."
Claiming the design had no flaws, or describing a schema with no mention of keys or constraints.
Symptom: how the problem showed up and how you confirmed it in the data.
Root cause: why the database let it in: a missing constraint, a race or a bad import.
Fix: clean the existing rows, add the constraint, and handle the error in code.
"At my last company, support noticed some customers were getting two welcome emails. I checked the users table and found pairs of rows with the same email, created a fraction of a second apart. The sign-up code checked whether the email existed and then inserted, but a double click meant both requests passed the check before either insert happened. The only guard was in the application; the table had no unique constraint on email. The fix had three parts. I wrote a script to merge the duplicates, moving their orders to the surviving account. Then I added a unique index on the lower-cased email, so the database itself refuses a second row. Finally the code catches the unique-violation error and shows a friendly message. My lesson: rules that must always hold belong in the database, not only in application checks."
Cleaning up the rows but not fixing the cause, so the same duplicates come back a month later.
Symptom: the errors and when they happened.
Diagnosis: which sessions held locks and which were waiting on them.
Fix: shorter transactions, a consistent lock order, retries on deadlock.
Guard: monitoring so you hear about it before users do.
"We had an order service that started throwing lock-wait timeouts every evening. Looking at the database's views of active sessions and locks, I saw a nightly job holding locks for minutes. It updated the status of every overdue order in one big transaction, so any customer request touching those orders queued behind it. We were also getting occasional deadlocks, because the job and the checkout code updated orders and inventory in opposite orders. I made two changes. The job now works in batches of a few hundred rows, each in its own short transaction, so locks are held for moments instead of minutes. And we agreed that any code touching both tables locks orders first, then inventory. The timeouts stopped and the deadlocks went away. I also added an alert on long lock waits so we'd catch it early next time."
Blaming 'the database being slow' without ever looking at which session held the locks.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.