Data Modeling & Transactions
Schema normalization and denormalization, constraints, surrogate keys, document versus relational stores, OLAP versus OLTP, transaction isolation, row and optimistic locking, and database scaling.
18 questions
JuniorTheoryVery commonWhat is connection pooling, and why does Go's database/sql need one?
What is connection pooling, and why does Go's database/sql need one?
Each new PostgreSQL connection forks a backend process — too expensive to open per query. A pool keeps a set of live connections and hands them out, so requests reuse them instead of reconnecting. Go's database/sql pools automatically; size it with SetMaxOpenConns, SetMaxIdleConns, and SetConnMaxLifetime.
Common mistakes
- ✗Thinking a Postgres connection is cheap, so pooling barely matters
- ✗Opening a new
*sql.DB(and thus a new pool) per request instead of once at startup - ✗Confusing a connection pool with a query or result cache
Follow-up questions
- →What goes wrong if every replica of your service opens its own large pool?
- →Why is
SetConnMaxLifetimeuseful behind a load balancer or after a failover?
JuniorTheoryVery commonWhat is a database transaction, and what do COMMIT and ROLLBACK do?
What is a database transaction, and what do COMMIT and ROLLBACK do?
A transaction groups several statements into one atomic unit — either all take effect or none do. COMMIT makes its changes permanent and visible to other transactions; ROLLBACK discards everything done since BEGIN, leaving the database as if the transaction never ran.
Common mistakes
- ✗Thinking each statement auto-commits, so a transaction cannot span multiple statements
- ✗Believing ROLLBACK keeps the statements that already succeeded
- ✗Treating COMMIT as merely flushing to disk rather than making changes durable and visible
Follow-up questions
- →What guarantees does a transaction give beyond all-or-nothing — what are the ACID properties?
- →What happens to a transaction's changes if the connection drops before
COMMIT?
MiddleTheoryVery commonWhat do the four ACID properties guarantee?
What do the four ACID properties guarantee?
Atomicity — all of a transaction's statements apply, or none do. Consistency — it moves the database from one valid state to another, never breaking constraints. Isolation — concurrent transactions don't observe each other's unfinished work. Durability — once COMMIT returns, the change survives a crash, backed by the WAL.
Common mistakes
- ✗Equating Consistency with Isolation — they are distinct guarantees
- ✗Thinking Durability needs a replica rather than the local WAL
- ✗Believing Isolation requires transactions to run strictly one at a time
Follow-up questions
- →How does PostgreSQL provide Isolation without running transactions serially?
- →Which ACID property does the write-ahead log most directly provide?
JuniorTheoryCommonWhat is normalization and what problem does it solve?
What is normalization and what problem does it solve?
Normalization organizes a relational schema into well-formed tables (1NF, 2NF, 3NF) by storing each fact exactly once. It removes redundancy and the update, insert, and delete anomalies that arise when the same value is duplicated across many rows.
Common mistakes
- ✗Confusing normalization with data compression — it is a logical schema design discipline, not a storage technique
- ✗Thinking normalization is about query speed, when it is about removing redundancy and update anomalies
- ✗Believing higher normal forms always merge tables, when they actually split data into more tables
Follow-up questions
- →What is an update anomaly, and give a concrete example of one?
- →What does third normal form require that second normal form does not?
MiddleTheoryCommonWhat are the SQL transaction isolation levels and how do they differ?
What are the SQL transaction isolation levels and how do they differ?
The four levels are Read Uncommitted, Read Committed, Repeatable Read, and Serializable. Each one forbids progressively more anomalies — dirty read, non-repeatable read, then phantom read. Higher isolation costs more concurrency; PostgreSQL defaults to Read Committed.
Common mistakes
- ✗Confusing the direction —
Serializableis the strongest level, not the weakest - ✗Thinking
Repeatable Readalso prevents phantom reads under the standard, when onlySerializableis guaranteed to - ✗Assuming higher isolation is free, when it reduces concurrency and raises serialization-failure retries
Follow-up questions
- →What is the difference between a non-repeatable read and a phantom read?
- →How does PostgreSQL's MVCC implement
Repeatable Readwithout blocking readers?
JuniorTheoryOccasionalWhat is a constraint in a relational database?
What is a constraint in a relational database?
A constraint is a rule the database enforces on every write to keep data valid; a violating statement is rejected. Common ones are NOT NULL, UNIQUE, PRIMARY KEY (unique and not null), FOREIGN KEY (referential integrity), and CHECK (a predicate must hold).
Common mistakes
- ✗Confusing a
UNIQUEconstraint with aPRIMARY KEY— a primary key is also not-null and there is only one per table - ✗Assuming a
FOREIGN KEYchecks only at insert time, when it also blocks deleting a referenced parent row - ✗Treating
CHECKas application-side validation, when the database itself rejects the violating write
Follow-up questions
- →What is the difference between a
PRIMARY KEYand aUNIQUEconstraint? - →What happens when you try to delete a row that a
FOREIGN KEYstill references?
JuniorTheoryOccasionalWhat is a row-level lock in a database, and what is it for?
What is a row-level lock in a database, and what is it for?
A row-level lock guards a single row so only one transaction can modify it at a time; others touching that row wait until the holder commits or rolls back. It is finer-grained than a table lock, so unrelated rows stay concurrent, and both UPDATE and SELECT ... FOR UPDATE acquire one.
Common mistakes
- ✗Thinking a row lock blocks reads — a plain
SELECTof a locked row normally still works - ✗Confusing row-level with table-level locking, assuming one locked row blocks the whole table
- ✗Believing locks are released manually rather than automatically at commit or rollback
Follow-up questions
- →How does
SELECT ... FOR UPDATEdiffer from the implicit lock that a plainUPDATEtakes? - →What happens when two transactions try to lock the same two rows in opposite order?
MiddleTheoryOccasionalWhy and when would you denormalize a relational schema?
Why and when would you denormalize a relational schema?
Denormalization deliberately reintroduces redundancy — duplicated columns or precomputed aggregates — to cut the joins a read path needs. Apply it when read latency matters more than write simplicity, accepting that every duplicated copy must be kept consistent on write.
Common mistakes
- ✗Thinking denormalization removes redundancy, when it deliberately adds redundancy back
- ✗Ignoring the write-side cost — every duplicated copy must be updated together to stay consistent
- ✗Denormalizing prematurely before measuring that joins are actually the read bottleneck
Follow-up questions
- →How do you keep a precomputed aggregate column consistent when the underlying rows change?
- →Why does a denormalized counter column become a hotspot under concurrent writes?
MiddleTheoryOccasionalWhat is MVCC, and why don't readers and writers block each other in PostgreSQL?
What is MVCC, and why don't readers and writers block each other in PostgreSQL?
Multi-Version Concurrency Control keeps several versions of each row, stamped with the transaction that created (xmin) and the one that deleted (xmax) it. A reader sees the version valid for its snapshot, so a writer creating a new version never blocks the reader and vice versa. The cost is dead row versions, which VACUUM later reclaims.
Common mistakes
- ✗Thinking MVCC uses read/write locks instead of versioned rows
- ✗Believing UPDATE overwrites a row in place rather than writing a new version
- ✗Forgetting old versions become dead tuples that VACUUM must reclaim
Follow-up questions
- →What are
xminandxmaxon a row version, and how does a snapshot use them? - →How does MVCC underpin
Repeatable Readisolation in PostgreSQL?
MiddleTheoryOccasionalWhat is optimistic locking, and when is it preferable to pessimistic row locks?
What is optimistic locking, and when is it preferable to pessimistic row locks?
Optimistic locking holds no DB lock: a version column is read, and the update runs ... WHERE id = ? AND version = ? while bumping the version. If RowsAffected is 0, another writer won and you retry. It beats pessimistic SELECT FOR UPDATE when conflicts are rare — no held locks, higher throughput — but suffers repeated retries under high contention.
Common mistakes
- ✗Forgetting to check
RowsAffectedafter the conditional update, so a lost update goes unnoticed - ✗Thinking optimistic locking takes a database lock — it does not; the conflict is detected at write time
- ✗Using it under heavy write contention, where constant retries make it slower than a pessimistic lock
Follow-up questions
- →How do you bound retries so an optimistic update does not livelock under contention?
- →Why must the version bump and the row update happen in the same statement or transaction?
MiddleTheoryOccasionalWhat mechanisms scale a relational database, and what are their trade-offs?
What mechanisms scale a relational database, and what are their trade-offs?
Vertical scaling means a bigger machine — simple, but capped and a single point of failure. Read replicas scale reads but lag asynchronously and do not help writes. Partitioning splits one table by range, list, or hash for pruning. Sharding spreads data across nodes by a shard key, scaling writes but making cross-shard joins hard.
Common mistakes
- ✗Believing read replicas help write throughput — replication scales reads only, and replicas lag behind
- ✗Conflating partitioning (one table split inside one database) with sharding (data spread across separate nodes)
- ✗Forgetting that sharding makes cross-shard joins and multi-shard transactions hard, the cost of write scalability
Follow-up questions
- →Why does asynchronous replication mean a read replica can return stale data?
- →How does the choice of shard key affect cross-shard query cost?
MiddleTheoryOccasionalWhat are the trade-offs of a UUID versus a serial primary key?
What are the trade-offs of a UUID versus a serial primary key?
A serial/identity key is a compact 4/8-byte integer whose sequential inserts append to the right of the B-tree, giving good locality. A random UUID is 16 bytes, unique, and generatable client-side with no round-trip, but scatters inserts — prefer UUIDv7.
Common mistakes
- ✗Thinking random
UUIDkeys are as index-friendly as sequential integers, ignoring the scattered inserts - ✗Ignoring that a wider 16-byte key bloats every secondary index, not just the primary key
- ✗Assuming PostgreSQL physically clusters rows by the primary key, like MySQL InnoDB does
Follow-up questions
- →How does
UUIDv7fix the insert-locality problem of randomUUIDv4? - →Why does a random primary key cause more WAL and page splits than a
serial?
SeniorTheoryOccasionalWhat does SELECT FOR UPDATE do, and how does lock ordering cause deadlocks?
What does SELECT FOR UPDATE do, and how does lock ordering cause deadlocks?
SELECT ... FOR UPDATE takes a row-level write lock on each selected row, so other transactions touching those rows block until the holder commits. If two transactions lock rows in opposite order, each waits on a row the other holds — a deadlock. The engine detects the cycle and aborts a victim; consistent lock ordering avoids it.
Common mistakes
- ✗Thinking
SELECT FOR UPDATEtakes a shared read lock — it takes an exclusive row-level write lock - ✗Believing the application must detect deadlocks, when the engine detects the cycle and aborts a victim
- ✗Ignoring that locking rows in a consistent global order across transactions is what prevents deadlocks
Follow-up questions
- →How does
SELECT FOR UPDATE SKIP LOCKEDchange the behaviour for a job-queue pattern? - →Why does a longer transaction holding
FOR UPDATElocks raise the deadlock and contention risk?
MiddleTheoryRareFor deduplicating a large event stream, how do ClickHouse and PostgreSQL differ?
For deduplicating a large event stream, how do ClickHouse and PostgreSQL differ?
PostgreSQL is a row-store OLTP engine: you dedup with a UNIQUE constraint or INSERT ... ON CONFLICT, enforced transactionally per row. ClickHouse is a columnar OLAP engine built for huge append-only scans and has no unique constraint — you dedup with a ReplacingMergeTree that collapses duplicates lazily at merge time, or argMax/GROUP BY at query time.
Common mistakes
- ✗Expecting a UNIQUE constraint in ClickHouse like in PostgreSQL
- ✗Assuming ReplacingMergeTree deduplicates immediately rather than lazily at merge
- ✗Treating a columnar OLAP store as a drop-in for transactional row-level dedup
Follow-up questions
- →Why does ReplacingMergeTree need
FINALor aggregation to read fully deduplicated results? - →When is per-row transactional dedup in PostgreSQL the right choice over ClickHouse?
MiddleTheoryRareWhat advantages do document databases have over relational ones?
What advantages do document databases have over relational ones?
Document stores allow a flexible, per-record schema and embed related data inside one document, so a read needs no joins. They also shard horizontally more easily. Relational databases instead give joins, multi-row ACID transactions, fine-grained row locking, and enforced referential integrity.
Common mistakes
- ✗Believing document databases have no schema at all, when they have a flexible per-record schema instead
- ✗Assuming document stores always provide the same multi-row ACID guarantees as a relational database
- ✗Thinking embedding related data needs joins, when embedding is precisely what removes the join
Follow-up questions
- →When does embedding related data inside a document become a liability rather than an advantage?
- →Why is horizontal sharding generally easier for a document store than for a joined relational schema?
SeniorDesignRareYou must run a one-off data fix that updates about 10 million rows in a single table that holds roughly 1 billion rows and is under continuous heavy read/write traffic in production. A single UPDATE covering all 10M rows would hold row locks and create bloat for minutes, stall autovacuum, blow up the WAL, and risk replication lag plus a very long rollback if it fails midway. Describe how you would design and run this data fix safely: how you batch the work, how you select which rows to touch, how you avoid long locks and replication lag, how you make the job resumable and idempotent if it is interrupted, and how you pace it against live traffic.
You must run a one-off data fix that updates about 10 million rows in a single table that holds roughly 1 billion rows and is under continuous heavy read/write traffic in production. A single UPDATE covering all 10M rows would hold row locks and create bloat for minutes, stall autovacuum, blow up the WAL, and risk replication lag plus a very long rollback if it fails midway. Describe how you would design and run this data fix safely: how you batch the work, how you select which rows to touch, how you avoid long locks and replication lag, how you make the job resumable and idempotent if it is interrupted, and how you pace it against live traffic.
Never one giant UPDATE. Batch by primary-key ranges (a few thousand rows per transaction), each batch a short transaction so locks are held briefly and autovacuum and replicas keep up. Make it resumable by tracking the last processed key, and idempotent so a re-run skips already-fixed rows (a WHERE that no longer matches a fixed row). Throttle between batches, watch replication lag and back off under load, and run off-peak.
Common mistakes
- ✗Doing it as one large transaction, holding locks and bloating the table for minutes
- ✗Using LIMIT/OFFSET for batching, which rescans skipped rows and drifts under concurrent writes
- ✗Forgetting idempotency, so a retry after a crash double-applies the fix
Follow-up questions
- →Why is keyset pagination (
WHERE id > :last ORDER BY id LIMIT n) better thanLIMIT/OFFSETfor batching? - →How do you monitor and react to replication lag while the job runs?
SeniorTheoryRareWhy add the external pooler PgBouncer if the app already has a database/sql pool?
Why add the external pooler PgBouncer if the app already has a database/sql pool?
An app-side pool is per-instance: run hundreds of replicas and each opens its own pool, so total connections far exceed what Postgres can serve (every connection is a backend process). PgBouncer sits in front and multiplexes many client connections onto a small server pool — in transaction mode a server connection is borrowed only for the length of one transaction.
Common mistakes
- ✗Thinking an app-side pool bounds total connections across all instances
- ✗Believing PgBouncer adds CPU or write capacity rather than just sharing connections
- ✗Confusing
transactionmode withsessionmode, which holds a server connection for the whole session
Follow-up questions
- →What breaks in
transactionmode if code relies on session state like prepared statements? - →How would you split a fixed Postgres connection budget across N service replicas?
SeniorTheoryRareWhat is virtual sharding?
What is virtual sharding?
Virtual sharding maps keys not directly to physical nodes but to a large, fixed number of virtual shards (buckets); each node then owns a set of those shards. Adding or removing a node moves whole virtual shards rather than rehashing every key, so rebalancing is cheap and data stays evenly distributed. It decouples the logical partitioning from the physical topology.
Common mistakes
- ✗Thinking virtual shards are themselves separate servers, when they are logical buckets that physical nodes own in sets
- ✗Believing adding a node rehashes every key — only whole virtual shards move, which is why rebalancing is cheap
- ✗Assuming the shard count equals the node count, when virtual sharding deliberately fixes a much larger shard count
Follow-up questions
- →How does this relate to consistent hashing with virtual nodes?
- →Why does fixing a large shard count keep the data distribution even as nodes are added?