Databases & SQL Interview Questions: Database Performance

Reviewed by Mark Dickie · Last updated

17 questions on this page you can answer and see marked on the spot. Go to the first one

Database performance is the study of how fast a database system can read and write data under a given workload, and what slows it down. For interviews, you should know how indexes work (B-tree vs. hash, covering indexes, when an index is ignored), how to read an execution plan, and the common causes of slow queries: full table scans, N+1 queries, missing or redundant indexes, and poorly written joins.

The table below maps the core performance topics you are likely to be tested on:

TopicWhat an interviewer checks
Indexing strategyWhether you can pick the right index type and explain trade-offs
Query execution plansWhether you can interpret EXPLAIN output and spot inefficiencies
Join optimizationWhether you understand join algorithms and join ordering
Connection and lock contentionWhether you can diagnose blocking, deadlocks, and pool exhaustion
Data modeling for speedWhether you can normalize enough without hurting read performance

What does a database performance interview test?

Most interviewers focus on whether you can look at a slow query and form a concrete plan to fix it. That usually means walking through the following steps:

  1. Read the execution plan and identify the expensive operator (scan, sort, hash).
  2. Check whether an index exists on the columns used in the filter, join, and sort.
  3. Decide whether a covering index or a composite index would help, and in what column order.
  4. Rewrite the query if the structure prevents index usage, such as a function wrapping an indexed column.
  5. Consider whether the data model itself forces the problem, and whether denormalization or partitioning is worth the trade-off.

How do you read an EXPLAIN plan in SQL?

An execution plan shows the sequence of operations the query engine uses to return results. Look for the operator type (scan vs. seek), the estimated cost per node, row-count estimates, and whether a sort or hash is happening early. A key thing to watch for is a large gap between estimated and actual rows, which usually means stale statistics. If you see a full table scan on a large table where a filter is applied, that is a strong signal an index is missing or not usable.

Key facts

  • Tarmac has 36 Databases & SQL interview questions on this topic, 25 of them on this page, at difficulty 1–5 of 5.
  • Tarmac tracked 6,625 job postings asking for Databases & SQL in September 2026.
  • Roles asking for Databases & SQL advertise a median base salary of US$162,913, across 1,799 job postings as of September 2026.
  • Tarmac last reviewed these Databases & SQL interview questions on 13 October 2026.

At a glance

Questions25 shown · 36 in the bank
Difficulty1–5 of 5
FormatsMultiple choice, Fill in the blank, True / false, Short answer, Ordering, Multiple answer, Find the bug

What you'll review

  1. db performance
  2. query planning
  3. n plus one

Practice questions

Try one before you open the answer. Pick an option and press Check; it's marked on the spot.

Which of the following is the primary purpose of adding an index to a database table column?#

Options

Show answer

The primary purpose of a database index is to speed up data retrieval queries. An index (typically a B-tree) lets the database jump directly to matching rows instead of scanning the whole table, dramatically reducing query time for large datasets. Enforcing uniqueness or preventing NULLs requires separate constraints, not an index.

Why:

An index is a data structure (commonly a B-tree) that the database maintains alongside a table so that rows matching a WHERE clause or ORDER BY can be located without scanning every row. While a UNIQUE index also enforces uniqueness, that is a secondary effect — the primary purpose of an index is faster reads. Indexes do not compress data or prevent NULLs.

Most SQL databases provide the _____ command (or _____ _____) to display the execution plan chosen by the query planner for a given SQL statement, without actually running the query.#

Show answer

Most SQL databases provide the **EXPLAIN** command (or **EXPLAIN** **EXPLAIN**) to display the execution plan chosen by the query planner for a given SQL statement, without actually running the query.

Why:

The EXPLAIN keyword (used alone or as EXPLAIN ANALYZE in PostgreSQL/MySQL) instructs the database engine to output the query execution plan — including join strategies, index usage, and estimated costs — without executing the query. This is the primary tool developers use to understand and optimize query performance.

Adding an index to every column of a large table is always a good performance optimization because it makes every possible query faster.#

Options

Show answer

False — indexing every column actually hurts overall performance. While indexes speed up SELECT queries, they add overhead to every INSERT, UPDATE, and DELETE because each index must be updated. They also use extra disk space. Good practice is to index only the columns that appear frequently in WHERE, JOIN, or ORDER BY clauses.

Why:

Indexes speed up reads but slow down writes (INSERT, UPDATE, DELETE) because the database must maintain each index whenever data changes. They also consume additional disk space. Over-indexing is a common anti-pattern: only columns that appear frequently in WHERE, JOIN, or ORDER BY clauses benefit enough to justify the write overhead and storage cost.

To examine how a database engine plans to execute a query without actually running it, you prefix the query with the keyword _____. The resulting output is commonly called an execution _____.#

Show answer

To examine how a database engine plans to execute a query without actually running it, you prefix the query with the keyword EXPLAIN. The resulting output is commonly called an execution plan.

Why:

Most relational databases (PostgreSQL, MySQL, SQLite, etc.) support the EXPLAIN keyword, which instructs the query planner to return the execution plan — showing operations like sequential scans, index scans, joins, and their estimated costs — without executing the query. Developers use EXPLAIN (and EXPLAIN ANALYZE in some databases) to diagnose slow queries and decide whether indexes are being used.

What is the primary purpose of a database index?#

Show answer

The primary purpose of a database index is to speed up data retrieval (SELECT query) performance by providing a fast lookup structure so the database does not have to scan every row in a table.

Why:

A database index is a data structure that allows the database engine to find rows matching a query predicate without scanning the entire table. This makes SELECT queries with WHERE clauses much faster. Indexes do add overhead to inserts, updates, and deletes because the index must be maintained alongside the table data.

In most relational databases (e.g., PostgreSQL, MySQL InnoDB), adding an index on a column used only in the SELECT list (but never in WHERE, JOIN, ORDER BY, or GROUP BY clauses) will improve the query planner's ability to speed up that query.#

Options

Show answer

False. An index on a column that only appears in the SELECT list provides no speed benefit to the query planner. Indexes help by narrowing down which rows to read (via WHERE, JOIN) or eliminating sorts (ORDER BY/GROUP BY). If the column is never used for filtering or ordering, the planner won't use that index and it simply adds overhead to write operations.

Why:

Indexes help the query planner speed up row lookups by filtering (WHERE), joining (JOIN), sorting (ORDER BY/GROUP BY), or enabling index-only scans. If a column appears only in the SELECT list and not in any filtering or ordering clause, an index on that column alone provides no benefit to the planner — the database still needs to access the table rows by another means and then simply read the column value. The index would be unused and just add overhead to writes.

The query planner follows a general pipeline when processing a SQL query. Arrange these steps in the correct order from first to last:#

Put these in order

Show answer

The correct order is: Parse → Rewrite → Plan/Cost → Execute. First the SQL text is parsed into a parse tree. Then the tree is rewritten (e.g., views expanded). Next the planner generates candidate plans and estimates costs using table statistics. Finally, the cheapest plan is selected and executed. This pipeline is standard across major relational databases like PostgreSQL and MySQL.

Why:

A relational query planner works in four major stages: (1) Parsing — the SQL string is lexed and parsed into a structured parse tree, catching syntax errors early. (2) Rewriting — the parse tree is transformed by applying rewrite rules such as view expansion, subquery flattening, and constant folding. (3) Planning / Optimization — the planner enumerates candidate execution plans (e.g., which indexes to use, which join algorithm to apply) and estimates the cost of each using table statistics (row counts, histograms). (4) Execution — the chosen lowest-cost plan is executed by the storage engine. This pipeline is consistent across PostgreSQL, MySQL, and most other RDBMSs.

A table orders has 10 million rows. A developer runs the following query frequently:#

Options

Show answer

Creating an index on the customer_id column is the most direct improvement. Without it, the database performs a full table scan over all 10 million rows. An index allows the engine to do an O(log n) lookup instead, drastically reducing I/O. Selecting fewer columns helps with data transfer but does not eliminate the expensive scan.

Why:

Without an index on customer_id, the database must perform a full table scan over all 10 million rows for every query. Creating an index on customer_id allows the database engine to use a B-tree (or similar) lookup and retrieve matching rows in O(log n) time instead of O(n). Reducing the selected columns can reduce data transfer but doesn't eliminate the costly scan, and partitioning on id doesn't help filter by customer_id.

A composite index defined as CREATE INDEX idx ON orders (status, created_at) can be used to efficiently satisfy the filter WHERE created_at > '2024-01-01' even when status is not specified in the query's WHERE clause.#

Options

Show answer

False. Composite indexes follow the left-prefix rule: the database can only use the index efficiently when the query's filter begins with the leftmost column(s) of the index. Because status is the leading column of (status, created_at), a filter on created_at alone cannot leverage this index and will result in a full scan. A separate index on created_at would be required.

Why:

Composite (multi-column) indexes follow the left-prefix rule: the index can only be used efficiently when the query filters start from the leftmost column(s) of the index. Here the index is (status, created_at), so status is the leading column. A query that filters only on created_at — skipping status — cannot use the index for that lookup and will fall back to a full table scan (or index scan). To support the created_at-only filter efficiently, a separate index (created_at) would be needed.

A slow SQL query has been reported in production. Arrange the following diagnostic and remediation steps in the most logical order, from first to last.#

Put these in order

Show answer

The correct order is: (1) Identify the slow query using logs or monitoring tools, (2) run EXPLAIN/EXPLAIN ANALYZE to inspect its execution plan, (3) create or adjust an index based on what the plan reveals, and (4) re-run EXPLAIN and benchmark to confirm the improvement. This order ensures you diagnose the root cause before applying a fix, and verify results afterward.

Why:

The correct workflow is: (1) Identify which query is slow using logs or monitoring, (2) Inspect its execution plan with EXPLAIN to understand why it's slow (e.g., full table scan, missing index), (3) Apply the appropriate fix such as adding an index, and (4) Verify with EXPLAIN and benchmarking that the change actually improved performance. Skipping the EXPLAIN step before making changes is a common mistake that can lead to ineffective or even harmful modifications.

A table employees has a composite B-tree index defined as:#

Options

Show answer

A query filtering only on first_name (the second column of the index) cannot use the composite index (last_name, first_name, dob) because it skips the leading column last_name. Database query planners require the leftmost prefix of a composite index to be present in the WHERE clause to perform an efficient index range scan; without it, the engine resorts to a full sequential table scan.

Why:

When a query planner encounters a predicate on a column that has a composite (multi-column) index, the leading-column rule applies: the index can only be used efficiently if the WHERE clause includes the leftmost prefix of the index columns. For an index on (last_name, first_name, dob), a filter solely on first_name skips the leading column last_name, so the planner cannot perform an index range scan and must fall back to a full sequential scan of the table. The clauses that include last_name = 'Smith' both include the leading column so the index is usable; the clause that uses the full key is also fine.

You run EXPLAIN ANALYZE on a slow query in PostgreSQL. Which of the following are valid and actionable insights you can directly obtain from its output? Select all that apply.#

Options

Pick every one that applies.

Show answer

From EXPLAIN ANALYZE output you can directly learn: the access method chosen for each table (Sequential Scan vs. Index Scan), estimated vs. actual row counts at every plan node — crucial for spotting stale statistics — and the total wall-clock execution time. Network latency between client and server and specific OS page-cache blocks read are not reported in EXPLAIN ANALYZE output.

Why:

EXPLAIN ANALYZE executes the query and annotates each plan node with estimated cost/rows, actual rows returned, and actual timing (wall-clock time per node and total). This lets you identify bad row-count estimates (estimated vs. actual rows) and expensive nodes (total wall-clock execution time). Network latency is outside the scope of the query planner output entirely. Buffer/block-level cache statistics are only visible with EXPLAIN (ANALYZE, BUFFERS) and even then show buffer hit counts, not specific block addresses.

After loading millions of new rows into a table, the query planner may produce poor plans because its internal _____ are stale. Running the _____ command updates them. Separately, when a WHERE clause matches a very large percentage of rows, the planner considers the predicate to have low _____ and may prefer a sequential scan over an index scan.#

Show answer

After loading millions of new rows into a table, the query planner may produce poor plans because its internal statistics are stale. Running the **ANALYZE** command updates them. Separately, when a WHERE clause matches a very large percentage of rows, the planner considers the predicate to have low selectivity and may prefer a sequential scan over an index scan.

Why:

The FILL_BLANK tests understanding of two fundamental query-planning concepts: (1) after bulk data changes the query planner relies on table statistics, which must be refreshed via ANALYZE (or VACUUM ANALYZE) so that cardinality estimates remain accurate; and (2) an index is not used when the planner estimates the query will touch a large fraction of the table — this threshold is called the 'selectivity' of the predicate. Low selectivity (many matching rows) typically leads the planner to prefer a sequential scan over an index scan because random I/O for most rows is more expensive.

Complete the two blanks in this PostgreSQL EXPLAIN terminology sentence:#

Show answer

Complete the two blanks in this PostgreSQL EXPLAIN terminology sentence:

"When PostgreSQL reads every row in a table without using an index, the plan node shown is called a **Seq Scan**. When it uses an index to look up each matching row directly and then fetches non-indexed columns from the heap one row at a time (without building a bitmap of pages first), the plan node is called an **Index Scan**.

Why:

In a PostgreSQL query plan, a Seq Scan (Sequential Scan) reads every page of the table from start to finish — no index is involved. An Index Scan uses the B-tree (or other) index to find matching row pointers and then follows each pointer back to the heap one row at a time to retrieve columns not stored in the index. This row-by-row heap access distinguishes it from the Bitmap Index Scan + Bitmap Heap Scan pair, which instead collects all matching tuple IDs into a bitmap first and then visits heap pages in physical order — a separate plan shape PostgreSQL typically chooses when many rows match. A third variant, Index Only Scan, avoids the heap entirely when all needed columns are covered by the index and the visibility map shows those pages are all-visible.

A developer adds the following index and query to power a leaderboard page that should show the top 10 players (highest scores) for a given game. The query returns incorrect results. Identify the line that contains the bug.#

CREATE INDEX idx_leaderboard
    ON player_scores (game_id, score);

SELECT player_id, score
FROM   player_scores
WHERE  game_id = 7
ORDER BY score ASC
LIMIT  10;

Options

Show answer

Line 7 — ORDER BY score ASC returns the 10 lowest scores; it should be ORDER BY score DESC to retrieve the top (highest) 10 scores

Why:

The intent is to fetch the top 10 players by highest score, making this a leaderboard. ORDER BY score ASC LIMIT 10 is a purely logical bug: it returns the 10 lowest scores, not the highest. Changing it to ORDER BY score DESC LIMIT 10 fixes the correctness issue. Importantly, B-tree indexes on (game_id, score) support both forward (ASC) and backward (DESC) scans with equal efficiency — modern optimizers (PostgreSQL, MySQL, SQL Server) simply traverse the index leaf pages in the opposite direction for DESC, incurring no extra cost. So the bug is entirely about correctness, not performance. The claim that the index should be on (score, game_id) instead of (game_id, score) is wrong: (game_id, score) is the correct column order — placing the equality-filtered column (game_id) first and the sort column (score) second is the standard pattern for this access pattern. The claim that LIMIT is incompatible with indexed lookups and should be removed is wrong: LIMIT actually enables an early-stop optimization, reducing work. The claim that WHERE game_id = 7 should use a range predicate (e.g., game_id >= 7) to allow the index to be used is wrong: WHERE game_id = 7 is an exact equality predicate, which is ideal for the leading index column and allows the index to be used efficiently.

This pseudocode renders a list of posts with their author names but is slow on large lists. What is the performance bug?#

posts = db.query("SELECT id, author_id, title FROM posts")
for post in posts:
    author = db.query(
        "SELECT name FROM authors WHERE id = ?", post.author_id)
    render(post.title, author.name)

Options

Show answer

It is an N+1 query: 1 query for the posts plus one extra query per post to fetch its author

Why:

This is the N+1 query problem: one query loads N posts, then the loop fires one additional query per post to fetch its author — N+1 round trips total. Replace it with a single JOIN (or a batched WHERE id IN (...) over the collected author ids) so the database returns posts and author names in one round trip.

You have a PostgreSQL table orders(id, customer_id, status, created_at, total) with a composite B-tree index idx_cs on (customer_id, status, created_at). Write a query (or describe the predicate structure) that lets the PostgreSQL planner use all three columns of idx_cs as Index Cond (index seek conditions), not merely as post-scan filters. Explain the principle.#

Show answer

A query with equality conditions on the two leading columns and a range (or equality) condition on the third column lets PostgreSQL use all three key fields as Index Cond, e.g. SELECT * FROM orders WHERE customer_id = 42 AND status = 'shipped' AND created_at >= '2024-01-01';. A B-tree composite index is traversed left-to-right: an equality constraint on the first key (customer_id) pins a sub-range; an equality on the second key (status) narrows within that sub-range; only then can the third key (created_at) be used as a seek condition because both preceding columns are constrained with equality, so the index entries are already sorted by created_at within that (customer_id, status) group. If any leading column lacks an equality predicate (or is skipped), the B-tree cannot narrow the scan far enough for later columns to serve as Index Cond — they would only appear as Filter after the index scan.

Why:

For a B-tree index on (a, b, c), PostgreSQL can use a column as an Index Cond only when every column to its left has an equality predicate. Equality on customer_id lets the index seek into a contiguous range; equality on status further narrows that range; a range predicate on created_at can then also be applied as a seek condition because within the fixed (customer_id, status) pair the entries are sorted by created_at. The query WHERE customer_id = 42 AND status = 'shipped' AND created_at >= '2024-01-01' satisfies all three, so the EXPLAIN plan shows all three columns listed in Index Cond. By contrast, a query that omits an equality on status or only filters status through a non-equality expression would force created_at (and possibly status) into Filter rather than Index Cond, because the B-tree cannot skip levels and still maintain the ordering needed for a range seek on later keys.

A PostgreSQL table orders has 10 million rows. A B-tree index exists on the status column. The following query ignores the index and performs a sequential scan:#

Options

Show answer

The planner correctly chooses a sequential scan because fetching ~9.4 million rows via random index lookups would be far more expensive than one sequential pass over the heap. PostgreSQL's cost model weights random I/O (random_page_cost) higher than sequential I/O (seq_page_cost), so when a predicate matches a large fraction of rows the index offers no benefit.

Why:

PostgreSQL's query planner chooses between a sequential scan and an index scan based on cost estimates derived from statistics (pg_statistic). When a predicate matches a large fraction of rows (e.g., a low-selectivity condition like status = 'active' on a table where 95% of rows are active), the planner correctly decides that reading the entire heap sequentially is cheaper than random index lookups plus heap fetches. The planner uses the page-level cost model: random I/O has a higher cost constant (random_page_cost) than sequential I/O (seq_page_cost). An index is only beneficial when selectivity is high (few rows returned). Options about outdated statistics (ANALYZE not run) and fillfactor are distractors — the described behavior is correct planner behavior, not a bug.

Place the following PostgreSQL query-processing stages in the correct order, from first to last, for a standard SELECT query.#

Put these in order

Show answer

The correct order is: (1) Parser – converts raw SQL into a parse tree; (2) Analyzer & Rewriter – resolves names and expands views; (3) Planner/Optimizer – costs candidate plans and picks the cheapest; (4) Executor – walks the plan tree to produce result rows. Each stage transforms the query representation before handing it to the next.

Why:

The correct sequence for how PostgreSQL processes a query through the planner/optimizer is: (1) Parser – turns SQL text into a parse tree; (2) Analyzer/Rewriter – resolves names, applies rules/views to produce a query tree; (3) Planner/Optimizer – generates and costs candidate plans using statistics, then selects the cheapest; (4) Executor – executes the chosen plan. Understanding this pipeline is essential for diagnosing why plans are chosen and how statistics feed into cost estimation.

You have the following query running slowly on a PostgreSQL table orders with ~50 million rows:#

Options

Show answer

The best index is CREATE INDEX ON orders (status, created_at). A composite index with the equality-filter column (status) first and the sort column (created_at) second lets the database perform an index range scan on the narrow pending bucket and read rows already in created_at order — eliminating both a table-heap scatter-read and a filesort. Reversing the order (created_at, status) or indexing either column alone cannot satisfy both predicates optimally.

Why:

A composite index on (status, created_at) supports the WHERE clause filtering on status AND the ORDER BY on created_at within each status bucket, allowing the query planner to do an index range scan and avoid a filesort. A single-column index on created_at alone cannot efficiently satisfy the equality predicate on status first, and a single-column index on status has poor cardinality and still requires a separate sort. Reversing the order to (created_at, status) forces a full scan of the date range before filtering by status, which is far less selective. The covering index on (status, created_at, user_id) would also work but is not among the options — the best minimal choice here is (status, created_at).

In PostgreSQL's MVCC implementation, table and index bloat can degrade read/write performance over time. Which of the following statements about bloat and its remediation are correct? (Select all that apply.)#

Options

Pick every one that applies.

Show answer

Statements A, B, D, and E are correct. Dead tuples pile up because MVCC preserves old row versions for active snapshots. VACUUM FULL rewrites the table but demands an exclusive lock. pg_repack performs the same compaction online without a prolonged lock. A long-running SELECT holds an old xmin horizon, preventing autovacuum from pruning dead tuples. Statement C is wrong: HOT updates only bypass index writes when the updated columns are not part of any index.

Why:

MVCC (Multi-Version Concurrency Control) in PostgreSQL keeps old row versions (dead tuples) in the heap to satisfy ongoing transactions that started before the row was updated or deleted. VACUUM reclaims that space, but if autovacuum can't keep up — or a long-running transaction holds an old xmin — table bloat accumulates. Index bloat is a separate but related phenomenon: B-tree index pages also accumulate dead entries that VACUUM marks as reusable but cannot always compact. HOT (Heap Only Tuple) updates avoid index churn for non-indexed columns but don't apply when indexed columns change. Statement-level locking and MVCC snapshots are distinct mechanisms. All three of the listed bloat mitigations (VACUUM FULL, autovacuum tuning, pg_repack) are real strategies.

A PostgreSQL developer runs the following query, which returns exactly one row but takes ~900 ms:#

Show answer

The root cause is stale table statistics: the planner's estimate of 50 000 matching rows is wildly inaccurate (actual: 1 row), so it believes a seq scan is cheaper than an index scan. The fix is to run ANALYZE transactions; (or VACUUM ANALYZE transactions;) to collect fresh column-level statistics so the planner can accurately estimate selectivity and choose the index.

Why:

The EXPLAIN ANALYZE output shows Seq Scan with an estimated 50 000 rows but only 1 actual row returned, and the estimate of cost ~12 000 is far higher than a targeted index seek would be. The planner chose a sequential scan because it believed the predicate would match many rows (poor statistics). After running ANALYZE (or VACUUM ANALYZE), the planner would have an accurate histogram for the account_id column and would likely choose an index scan instead — bringing execution from ~900 ms down to single-digit milliseconds. The other options are either irrelevant or wrong: increasing work_mem helps sorts/hash joins, not seq-scan vs index-scan decisions; creating a GIN index is for full-text/JSONB; and increasing seq_page_cost would make the planner more likely to avoid seq scans but is a global config hack, not a targeted fix.

A table events has ~500 million rows. A DBA creates the following composite B-tree index:#

Show answer

A B-tree index is traversed by walking down the tree using the leading column first. With (status, created_at), the planner uses the high-selectivity equality predicate status = 'PENDING' to jump directly to the subtree for that status value, then applies the range scan on created_at within that narrow band. Because status may have only a handful of distinct values (e.g., PENDING, PROCESSED, FAILED), the equality predicate is very selective — it eliminates most rows immediately. The index then delivers the matching rows in created_at order, so the ORDER BY created_at DESC LIMIT 100 can be satisfied with a single backward index scan without a sort step. With (created_at, status), the leading column is a timestamp with extremely high cardinality; the planner scans the 7-day range (potentially tens of millions of rows) and applies status = 'PENDING' as a residual filter on each fetched row. EXPLAIN ANALYZE would show a large 'rows removed by filter' count, high actual rows, and likely a sort node for the ORDER BY. With (status, created_at), EXPLAIN ANALYZE would show a very small 'rows removed by index recheck', low actual rows fetched, and no extra sort node because the index already provides the required order.

Why:

This question tests deep knowledge of multi-column index column ordering and query planner selectivity. The composite index on (status, created_at) is defined with status as the leading column. Because the query uses an equality predicate on status AND a range predicate on created_at, the planner can use both columns of the index efficiently (equality on the prefix, then range on the second column). Reversing the order to (created_at, status) would allow only the range prefix to be used — the status equality filter would have to be applied as a residual filter on the rows fetched by the range scan, which is less selective. The index on (status, created_at) therefore narrows the result set far more tightly using the B-tree structure, leading to fewer index pages read. EXPLAIN ANALYZE would show a much lower 'rows removed by index recheck' count for the correctly ordered index.

A DBA runs EXPLAIN (ANALYZE, BUFFERS) on two variants of the same query against PostgreSQL 15. Variant A (no LIMIT) uses a Hash Join with cost 1200..95000. Variant B (with LIMIT 1) uses a Nested Loop with cost 0.00..8.50.#

Options

Pick every one that applies.

Show answer

The correct explanations are that the planner scales a Hash Join's cost down proportionally for LIMIT but its startup cost still dominates, that a Nested Loop's near-zero startup cost is favored by LIMIT 1, and that the planner's cost model for LIMIT N approximates expected cost as startup + (N / total_rows) × (total_cost − startup). PostgreSQL's planner estimates the cost of a LIMIT N node as startup + (N / total_rows) × (total_cost − startup). A Hash Join has a high startup cost (it must build the entire hash table before emitting row one), so even though its total cost beats a Nested Loop for full scans, the scaled cost for LIMIT 1 favours the Nested Loop, which can return its first row almost immediately. There is no hard threshold of 10, and Hash Joins are not categorically disabled by LIMIT.

Why:

PostgreSQL's EXPLAIN output uses 'cost' units that are dimensionless and relative. The startup cost (first number) is the estimated cost to return the first row, while the total cost (second number) is the cost to return all rows. For a LIMIT node, the planner scales the total cost of the child node proportionally — it does NOT simply run the child to completion. A Hash Join always has a non-zero startup cost (it must build the hash table before returning any row), whereas a Nested Loop can start emitting rows immediately. When a LIMIT 1 is applied, the planner may switch from a Hash Join to a Nested Loop because the scaled total cost of the Nested Loop (almost equal to just its startup cost) beats the Hash Join's startup cost. This is a classic plan-flip scenario caused by the LIMIT hint to the planner, and it explains why adding LIMIT to a query can paradoxically make it slower if the plan flip is incorrect (e.g., when the nested loop's inner side has no index).

In PostgreSQL, a long-running OLAP query runs for 4 hours on a table that receives heavy OLTP writes. After the query completes, the DBA notices severe table and index bloat even though autovacuum is enabled. Which of the following accurately explain(s) why this happened? (Select all that apply.)#

Options

Pick every one that applies.

Show answer
  • The long-running transaction held an old snapshot, preventing autovacuum from removing dead tuples whose xmax was still needed by that snapshot.
  • Index entries pointing to dead heap tuples could not be reclaimed because the heap tuples themselves were not yet considered dead by VACUUM.
Why:

PostgreSQL's MVCC keeps old row versions (dead tuples) until VACUUM reclaims them. When a long-running transaction holds an old snapshot, autovacuum cannot remove dead tuples whose xmax is still needed by that snapshot, causing table bloat. The 'transaction ID wraparound' problem is a separate but related concern (vacuum must also freeze old XIDs), but the immediate cause of bloat from long-running transactions is the inability to reclaim dead tuples. Index bloat follows table bloat because the index entries for dead tuples also cannot be reclaimed. pg_stat_activity and pg_locks help diagnose, but the root cause is snapshot retention preventing VACUUM from advancing the 'oldest visible XID' horizon.

Sources

The official documentation these questions are checked against:

Related interview questions

Job market

See databases-sql salaries and hiring demand from live job postings.

The other 11 questions

This page shows 25 and marks what you pick. That's as far as a page can go. A free account opens the other 11 and keeps every answer. What you miss comes back until it's right: after a day, then at longer gaps.

Start with this topic

Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan

Interview booked? Practise the job ad instead

What moved, monthly

One email a month when the bulletin comes out: what moved in the markets we track, and the new question topics we published. Confirm your address to join. Unsubscribe any time.