Database Selection and Trade-offs Questions
Choosing the right database and data platform for a workload: relational versus NoSQL versus specialized stores, managed versus self-hosted, and matching technology to consistency, scale, cost, and query or access-pattern needs. Covers OLTP versus OLAP and transactional-versus-analytical workload splits, polyglot persistence across multiple data stores, structuring an ambiguous selection prompt, naming trade-offs, and defending a recommendation to stakeholders.
Explain the differences between ACID and BASE properties and between strong, causal, and eventual consistency models. Describe how these models affect availability, latency, developer mental models, and operational complexity in distributed databases. Provide one concrete example where BASE is acceptable and one where ACID is required.
Sample Answer
Direct answer
Atomicity, consistency, isolation, durability (ACID) describes the guarantee a traditional relational transaction gives you: it either fully happens or not at all, it leaves the database in a valid state, it's isolated from other concurrent transactions, and once committed it survives a crash. Basically available, soft state, eventually consistent (BASE) describes the opposite design philosophy many distributed NoSQL systems adopt: stay available under load or partition by relaxing the promise that every read reflects the very latest write, accepting instead that replicas will converge soon rather than agree immediately.
Structured elaboration
The consistency spectrum between these two poles:
- Strong consistency: every read sees the latest committed write, as if there were a single copy of the data. Requires real-time coordination (for example a synchronous quorum acknowledgment) and so costs latency, and availability during a partition.
- Causal consistency: operations that are causally related (a reply is seen after the comment it replies to) are seen in the same order by everyone, but unrelated operations may appear in different orders on different replicas. Weaker coordination cost than strong consistency, while still preserving the orderings that usually matter to users.
- Eventual consistency: no ordering guarantee during propagation at all, only a promise that, absent new writes, all replicas will eventually agree. Cheapest in coordination cost, but pushes the most burden onto the application to tolerate or reconcile temporarily stale or conflicting reads.
Effects on the system and the team. Strong consistency costs latency and can cost availability during a partition, because the system has to wait for coordination before responding. Eventual consistency buys low latency and continuous availability, at the cost of a genuinely harder developer mental model: for example a user posts a comment, refreshes the page, and briefly doesn't see it because their read landed on a replica that hasn't caught up yet (a read-your-own-writes anomaly). Operationally, eventually-consistent BASE systems need an explicit conflict-resolution strategy for when two replicas accept concurrent writes to the same key: last-writer-wins by timestamp (simplest, but it silently discards whichever write loses the timestamp comparison), vector clocks (each replica tags its writes with a counter per replica, so the system can tell a truly concurrent write from one that clearly happened after another, instead of guessing), or conflict-free replicated data types (CRDTs: data structures like counters or sets built so that merging two conflicting versions always lands on the same result, no matter what order the merge happens in). ACID systems push that complexity into lock and isolation-level tuning on a single logical system instead.
Worked example
Where ACID is required: transferring funds between two accounts in one database. Debiting account A and crediting account B must both happen or neither happens (atomicity), and no concurrent read may observe a state where money has left A but hasn't yet arrived in B (isolation). This is the textbook case for a transaction wrapped in BEGIN/COMMIT on a relational engine like Postgres, because the cost of a partial or visibly-inconsistent transfer is unacceptable.
Where BASE is acceptable: a "like" or view counter on a social media post. If two regions briefly disagree on the exact count for a few seconds after a write, no one is harmed, and a system that stays available everywhere and converges shortly after (an eventually-consistent counter in a system like Cassandra, or a DynamoDB table read with eventually consistent reads) is a better engineering trade than paying synchronous cross-region coordination latency on every single increment.
Trade-offs & pitfalls
Real production systems are rarely purely one or the other. Postgres's own default isolation level (read committed) is weaker than full serializability, so "we use a relational database" doesn't automatically mean every transaction gets the strongest possible ACID guarantee available; the specific isolation level chosen matters, from read committed up through serializable (the strictest level, where concurrent transactions are guaranteed to behave as if they ran one after another in some order, ruling out the anomalies a weaker level would allow), and each level permits a different set of anomalies. Conversely, "NoSQL means BASE" is also an oversimplification: many document databases offer strong, majority-acknowledged writes or single-document transactional guarantees for a scoped operation. The consistency guarantee is a property of the specific system and its configuration, not a label that comes free with a product category.
Explain the difference between columnar and row-oriented storage engines. For which query patterns and workloads would you choose a columnar engine (e.g., ClickHouse, Redshift, BigQuery) versus a row store (e.g., Postgres, MySQL)? Include examples of how storage format affects IO and compression.
Sample Answer
Direct answer
Row-oriented storage keeps every column of one record contiguous on disk, so it's efficient when a query needs most or all fields of a small number of rows, the online transaction processing (OLTP) pattern: fetch this one order, update this one profile. Columnar storage keeps each column contiguous across all rows instead, so it's efficient when a query touches only a few columns but scans millions or billions of rows, the online analytical processing (OLAP) pattern: sum this one numeric column, grouped by another, across the whole table, because the engine only has to read and decompress the columns the query actually references.
Structured elaboration
IO impact. Imagine a wide table with 50 columns and a query that computes the average of one numeric column over 10 million rows. A row store has to read each row's full width (or maintain a separate covering index, an index that stores extra columns alongside the key so a query can be answered from the index alone, without touching the full row) even though 49 of those 50 columns are irrelevant to the query; a columnar store reads only that one column's contiguous block, a small fraction of the bytes, before any decompression happens.
Compression. Columns hold values of a single type and are often low-entropy in sequence (a status column with three distinct values, a sorted timestamp column, slowly changing sensor readings), so column-wise run-length, dictionary, and delta encoding compress far better than a row that mixes types and columns together.
Vectorized execution. Columnar engines (ClickHouse, Redshift, BigQuery) typically pair columnar storage with vectorized execution: the engine processes a batch of values from one column together using processor-friendly, batched instructions, instead of interpreting the query's logic one row at a time. This is a major, separate reason columnar engines outperform row stores on aggregate scans beyond what compression alone explains.
Named engines. ClickHouse's MergeTree family (its main table-engine design) physically sorts and compresses data by column on disk and merges/compacts files in the background; Redshift is columnar and uses block-level min/max metadata to skip blocks that can't match a filter; BigQuery is serverless with storage and compute decoupled, so scan cost scales with bytes actually read rather than a fixed cluster size. Postgres and MySQL, by contrast, store rows contiguously and optimize for point lookups and small-row updates.
Worked example
A machine-learning feature-extraction pipeline reads from a wide user-behavior table with 200 sparse behavioral columns across 50 million users (a high-cardinality user_id), and a training job needs only 15 of those 200 columns across the full 50 million rows. Analytically: a row store must read bytes for all 200 columns per row to reach the 15 it needs (or maintain an expensive covering index across exactly those 15), while a columnar store reads only the 15 relevant columns, which is 15/200 = 0.075, about 7.5% of the row-store's bytes, before either applies compression. That ratio is why wide, sparse feature tables used for training are one of the clearest cases for columnar storage: the fraction of columns actually read per query is small and the row count is large, which is precisely the shape columnar compression and vectorized scanning are built for.
For dashboards that need sub-minute freshness, note the trade-off in the other direction: columnar engines that batch-compress large blocks (classically Redshift and BigQuery-style architectures) often ingest via micro-batches rather than single-row commits, so if a dashboard must reflect a write within seconds, either the ingestion pattern needs to support smaller, more frequent batches (ClickHouse's MergeTree generally tolerates more frequent inserts than a fully batch-oriented warehouse), or the dashboard reads from a periodically refreshed rollup/materialized view rather than the raw table directly, trading a small, known freshness lag for scan speed.
Trade-offs & pitfalls
Columnar stores are a poor fit for high-frequency single-row updates or deletes: changing one row means rewriting or marking stale a compressed column block rather than a simple in-place row update, and some columnar engines (ClickHouse's mutations, for example) handle updates as an asynchronous, comparatively expensive background operation rather than an immediate one. Don't put an OLTP-shaped write pattern on a columnar engine. Conversely, don't assume columnar always wins: for narrow tables or workloads that fetch entire rows by primary key, a row store remains equal or better.
Compare relational databases and non-relational databases for common data scientist workloads (user profiles, event logs, feature stores). Explain when you'd choose a document store vs a key-value store vs a relational DB and list three concrete signals (query patterns, latency requirements, transaction needs) that would push you to one family over another.
Sample Answer
Direct answer
Three concrete signals decide the family: how the data is queried, by a single key versus by arbitrary filters and joins; how strict the consistency and transactional need is at write time; and how uniform the schema is across records. Applied to common data-workload storage, relational fits structured, joinable, transactional data such as model metadata; document stores fit semi-structured, per-record data fetched whole by identifier, such as a training example; and key-value stores fit the highest-throughput, simplest-shape lookups, such as a precomputed feature vector read at inference time.
Structured elaboration
Four families, each mapped to a concrete storage need around training data, features, and model metadata, with the limitation that keeps it from being a universal answer:
Relational, for model metadata: which dataset version and hyperparameters trained a given model, foreign-keyed to its training run and its deployed version. Joins across runs, datasets, and models are exactly what is needed to answer "which model in production was trained on the dataset version we later found had a labeling bug." Limitation: a wide feature vector with hundreds of loosely related, sparsely populated columns is an awkward fit for a rigid schema, producing many empty columns and frequent schema changes as features get added.
Document, for individual training examples: each example is a self-contained, semi-structured record, raw text or nested metadata with a variable-length label set, fetched by identifier during data loading. Limitation: aggregating or joining across many documents, for example the label distribution across all examples from one source collected after a given date, is comparatively awkward and often needs a separate aggregation pipeline.
Key-value, for the online feature-serving layer: given an entity identifier, return its precomputed feature vector with single-digit-millisecond latency at very high request volume during real-time inference. Limitation: no query flexibility beyond the key. "Give me all entities where a feature exceeds a threshold" is not a key-value operation, so it is a serving cache in front of a system of record, never the system of record itself.
Wide-column or columnar, for bulk training data at scale: large volumes of historical events or features read in column-oriented batches for a training job, where reading a handful of feature columns across billions of rows is exactly what columnar storage is built for. Limitation: point lookups and small transactional updates are comparatively expensive or unsupported, and secondary indexing is limited compared to relational.
Worked example
A concrete pipeline: raw events land in a columnar object-store table for offline training reads. A batch job computes feature vectors and writes them into a key-value store keyed by entity identifier for millisecond-latency serving at inference time. Model metadata, which feature-set version, which training run, which deployed model, lives in a relational table with foreign keys tying all three together, because that is the part where a wrong join or a lost update, for example deploying a model without recording which feature version it expects, is a correctness incident, not just a performance question.
Trade-offs and pitfalls
A pitfall is putting the online feature-serving path directly on the same store used for offline training reads. Millisecond point lookups and bulk columnar scans are different enough access patterns that one store rarely serves both well at real scale.
A second pitfall is letting notebook convenience drive the storage decision. What is easiest to query interactively is a client-side concern; the workload's actual production shape, serving latency, write pattern, and join needs, should decide the storage choice instead.
What would flip the choice: a small team with a modest data volume, where metadata, on-demand features, and training data all fit comfortably in one well-indexed relational database with a JSONB column for the variable feature shapes. There is no reason to stand up four separate systems before scale actually demands it.
Describe wide-column stores (e.g., Cassandra, HBase) and how they differ from relational databases and document stores. What workloads and access patterns are best suited to wide-column stores? Give a simple example schema for storing time-series metric data in a wide-column store.
Sample Answer
Direct answer
A wide-column store organizes data as rows identified by a partition key, with each row holding a flexible, sparse set of columns grouped into column families and physically sorted by a clustering key. That makes it fundamentally different from a relational table (fixed columns, strong joins and transactions) and from a document store (one nested object per key, with rich indexing inside it but no native cross-record ordering guarantee). This shape is purpose-built for write-heavy, mostly-append, time-ordered data accessed by a known key plus a range, exactly the profile of time-series metrics.
Structured elaboration
Wide-column vs relational vs document. A relational database is schema-first with strong multi-table joins and transactions and indexes on any column, but a poor fit for extremely high, mostly-append write rates, because B-tree indexes (the sorted, tree-shaped on-disk index structure most relational databases use to keep rows quickly findable by key) get expensive to maintain under sustained inserts at scale: each insert does a random disk seek to the page holding its key range, and once that page is full the insert forces a page split that rewrites and rebalances part of the tree, the opposite of the sequential, no-seek writes the LSM-tree design below uses. A rigid schema also resists the sparse "some rows have these columns, others don't" shape common in telemetry (not every metric carries every tag). A document store shares wide-column's flexible per-record schema, but organizes around one whole nested object per key with rich intra-document indexing; wide-column instead organizes around many ROWS sharing one partition, each row a flat (though sparse) set of columns sorted by a clustering key, optimized for "give me a sorted range of rows for this partition" rather than "give me documents matching this nested filter." Cassandra, HBase, and Bigtable-family engines use a log-structured merge-tree (LSM-tree) storage engine: writes go to an in-memory buffer plus an append-only commit log, later flushed to immutable files on disk and merged by a background process called compaction. This makes sustained high write throughput cheap, since there is no in-place random-write disk seeking, at the cost of read amplification (a read may need to check the in-memory buffer plus several on-disk files), managed by choosing a compaction strategy.
Best-suited workloads. Time-series metrics, event and activity logs, IoT sensor readings, and messaging or chat history all share "high write rate, data naturally ordered by time, queries are almost always this key plus a time range," rarely arbitrary cross-entity joins.
Example schema: time-series metrics (Cassandra CQL).
CREATE TABLE metrics (
metric_name text,
host text,
bucket_hour timestamp,
ts timestamp,
value double,
PRIMARY KEY ((metric_name, host, bucket_hour), ts)
) WITH CLUSTERING ORDER BY (ts DESC)
AND compaction = {'class': 'TimeWindowCompactionStrategy',
'compaction_window_unit': 'HOURS', 'compaction_window_size': 1}
AND default_time_to_live = 2592000;
The partition key is (metric_name, host, bucket_hour), bucketed by hour so no single partition grows unbounded as points keep arriving; an all-time partition per host would eventually become an oversized, hot partition, the same hotspot failure mode DynamoDB has under a different name. The clustering key ts DESC gives sorted, most-recent-first range scans for free.
SRE operational behavior: compaction and garbage collection (GC) for telemetry retention. TimeWindowCompactionStrategy (TWCS) groups on-disk files by the time window they cover and compacts within a window, so an entire window can be dropped WHOLESALE once every row in it has passed its TTL, rather than being rewritten row by row the way SizeTieredCompactionStrategy (Cassandra's older default) would; this is specifically why TWCS is the documented, recommended strategy for TTL'd time-series data. The retention gotcha to know: gc_grace_seconds (a per-table setting, commonly defaulting to 10 days) controls how long TOMBSTONES, the markers left behind for expired or deleted rows, are retained before being permanently purged during compaction, so disk usage will NOT drop immediately at the TTL boundary, only after this grace window has also passed. If node repairs (the anti-entropy process that reconciles replicas) are not run within that grace window, deleted data can even reappear on an under-repaired replica, a well-known Cassandra operational hazard. Telemetry tables with short retention commonly tune gc_grace_seconds down, since a high-volume, low-per-row-value metrics table is a lower risk to under-repair than user data, specifically to reclaim disk sooner.
Trade-offs & pitfalls
Choosing SizeTieredCompactionStrategy for a TTL'd time-series table wastes disk and CPU rewriting data that is mostly about to expire anyway; TWCS is the correct default for this shape. An unbounded partition key (no time-bucketing) is the single most common wide-column schema mistake for time-series data, and it reproduces the same hot-partition failure seen across every store in this domain.
You must choose between a managed cloud OLAP service (e.g., BigQuery) and self-managed columnar cluster (e.g., Presto+Parquet on Kubernetes) for an analytics team. Compare them across performance predictability, TCO, administrative overhead, query concurrency, and vendor lock-in. For a team with 20 analysts and occasional heavy ad-hoc queries, which would you recommend and why?
Sample Answer
Direct answer
For a team of 20 analysts running occasional heavy ad-hoc queries, recommend the managed cloud OLAP (online analytical processing: query patterns dominated by large scans and aggregations, as opposed to OLTP, online transaction processing, which is dominated by many small transactional writes) service, BigQuery, over a self-managed Presto+Parquet cluster on Kubernetes. The deciding factor is not raw compute cost, where the two are close, but that a self-managed cluster is provisioned capacity billed whether it's busy or idle, and it needs a platform team to keep it healthy; at 20 analysts' actual query volume, that fixed cost overwhelms any per-query savings self-hosting offers.
Comparing the two on the five stated axes
| Axis | BigQuery (managed) | Presto+Parquet on Kubernetes (self-managed) |
|---|---|---|
| Performance predictability | Google auto-scales query execution; a single expensive ad-hoc query doesn't need capacity planning from your team, but concurrent heavy queries share a per-project fair-scheduling pool that can slow each other down unpredictably under load | Fully predictable only if you've provisioned for peak concurrency; if 20 analysts all fire heavy queries at once and you provisioned for average load, everyone queues |
| TCO (total cost of ownership) | Pay per TB scanned plus storage; no infrastructure or platform-team cost | Compute and storage costs plus a platform team's fully loaded salary to operate Kubernetes, Presto, and the Parquet layout; see the worked example below for how much that staffing line matters |
| Administrative overhead | Near zero: no cluster to patch, scale, or keep alive | Real, ongoing: Presto version upgrades, Kubernetes node management, Parquet file compaction (periodically rewriting many small files into fewer, larger ones so scans stay fast) and small-file cleanup, query engine tuning |
| Query concurrency | Google manages resource allocation across concurrent queries within your project's slot allotment | You own the scheduler configuration and capacity headroom; under-provisioning shows up as analysts waiting on each other's queries |
| Vendor lock-in | Real: BigQuery's SQL dialect and storage format are Google-specific, and moving off it later means rewriting queries and re-exporting data | Minimal: Parquet is an open, portable file format and Presto/Trino run on any infrastructure, so this stack is the more portable choice if lock-in is the team's top concern |
Cloud pricing components to model, explicitly
A cost comparison that skips any of these components understates one side or the other:
- Compute: BigQuery's per-TB-scanned on-demand rate, or a flat-rate/editions commitment; self-managed compute-hours for the Kubernetes node pool.
- Storage: BigQuery's active logical storage rate; object storage (S3/GCS-class: services like Amazon S3 or Google Cloud Storage that store flat, key-addressed files rather than a traditional filesystem) for the Parquet files in the self-managed case.
- Network egress: cost of moving data out of the cloud region or between the storage layer and compute nodes, often ignored until a cross-region setup makes it material.
- Committed-use / reserved discounts: BigQuery editions/flat-rate slots, or reserved-instance pricing for the self-managed nodes; both can shift the comparison significantly at steady, predictable load.
- Support tier cost: enterprise support contracts, if the team needs guaranteed response times, are a real line item on either side that's easy to forget in a first-pass estimate.
Worked example
Verified current BigQuery rates (cloud.google.com/bigquery/pricing, checked 2026-09-24): on-demand analysis at $6.25 per TiB scanned (US multi-region), with the first 1 TiB/month free, and active logical storage at $0.02 per GiB-month. Model 20 analysts each running 15 queries/day, 22 working days/month, averaging 50 GiB scanned per query, against a 20 TiB resident dataset.
analysts, queries_per_day, days = 20, 15, 22
scan_per_query_GiB, dataset_GiB = 50, 20_000
queries_per_month = analysts * queries_per_day * days
scanned_TiB = queries_per_month * scan_per_query_GiB / 1024
print(f"queries/month: {queries_per_month:,}, scanned: {scanned_TiB:.1f} TiB")
bq_compute = max(0, scanned_TiB - 1) * 6.25 # VERIFIED rate, first 1 TiB free
bq_storage = dataset_GiB * 0.02 # VERIFIED rate
print(f"BigQuery: ${bq_compute + bq_storage:,.2f}/mo (compute ${bq_compute:,.2f} + storage ${bq_storage:,.2f})")
# Self-managed: ASSUMPTION rates, pinned explicitly (no live quote for a specific node type)
nodes, vcpu_per_node, compute_rate = 10, 16, 0.045
storage_rate, fte, salary = 0.023, 1.5, 190_000
sm_compute = nodes * vcpu_per_node * compute_rate * 730
sm_storage = dataset_GiB * storage_rate
sm_staffing = fte * salary / 12
sm_total = sm_compute + sm_storage + sm_staffing
print(f"Self-managed: ${sm_total:,.2f}/mo (compute ${sm_compute:,.2f} + storage ${sm_storage:,.2f} + staffing ${sm_staffing:,.2f})")
queries/month: 6,600, scanned: 322.3 TiB
BigQuery: $2,407.91/mo (compute $2,007.91 + storage $400.00)
Self-managed: $29,466.00/mo (compute $5,256.00 + storage $460.00 + staffing $23,750.00)
At this team's actual query volume, BigQuery costs about 12x less than the self-managed cluster, almost entirely because the self-managed side pays for 1.5 platform engineers (an assumed, but realistic, staffing level to keep Presto and Kubernetes healthy) whether or not the cluster is busy. The break-even point, where BigQuery's scanned-data cost alone equals the self-managed cluster's fixed compute-plus-storage-plus-staffing cost, works out to roughly 4,700 TiB scanned per month in this model: about 15x this team's actual usage. Self-hosting would only become competitive if the team's query volume grew far beyond what 20 analysts running occasional heavy queries would plausibly generate, or if the platform team's time were already a sunk cost shared across other self-hosted systems.
Trade-offs and pitfalls
- The recommendation flips on scale, not on principle. Once sustained scanned-data volume is high enough (roughly 15x this team's usage, per the break-even above), the self-managed cluster's fixed cost starts to look cheap relative to BigQuery's linear per-scan billing.
- Vendor lock-in is real and worth naming, even when recommending BigQuery. If portability is a hard organizational requirement (regulatory, multi-cloud strategy), that can outweigh the cost argument; that's a legitimate reason to choose the more expensive, more portable option deliberately.
- BigQuery's on-demand billing rewards query discipline. An unindexed
SELECT *over a huge table is expensive precisely because you pay per byte scanned; partitioning and clustering the underlying tables materially changes the cost side of this comparison and should be table stakes before concluding BigQuery is "too expensive." - A self-managed estimate that omits Parquet file compaction and small-file cleanup will understate the real operational burden. Small-file proliferation from frequent writes is one of the most common sources of unplanned Presto-on-Parquet maintenance work.
Unlock Full Question Bank
Get access to all 32 Database Selection and Trade-offs interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.