Skip to content

Scenario-Based SQL Interview Questions

45 questions with answers. Real-work situations — broken dashboards, flaky tests, unclear requirements — and how a strong data analyst (sql) would handle them.

How to approach these questions

Clarify the situation, list likely causes, say how you would check each one, then give the fix and how you would stop it happening again.

Solved — scenario-basedStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
3 questions free Unlock the rest in seconds
01

Your query WHERE manager_id <> 5 is not returning employees who have no manager. Why?

Save to ▾
Show answer
Employees with no manager have manager_id as NULL, and NULL <> 5 evaluates to unknown, not true, so those rows are excluded. Rewrite it as WHERE manager_id <> 5 OR manager_id IS NULL, or use COALESCE(manager_id, 0) <> 5. This three-valued logic with true, false and unknown causes many subtle bugs in reports and NOT IN subqueries.
02

Someone ran an UPDATE without WHERE on production. What would you do?

Save to ▾
Show answer
Stop further changes and inform the lead and DBA immediately. If the session is still in an open transaction, roll back. Otherwise, restore affected rows from the latest backup or point-in-time recovery to a separate database and update the production table from it, or use audit or history tables if available. Afterwards, add safeguards such as safe-update mode, reviews and restricted write access.
03

A report sums amounts stored in a FLOAT column and finance says totals are off by a few paise. Why?

Save to ▾
Show answer
FLOAT stores approximate binary values, so decimals such as 0.1 cannot be represented exactly and rounding errors accumulate over millions of rows. Money should be stored as DECIMAL or NUMERIC with fixed scale, for example DECIMAL(12,2). Fix it by migrating the column type carefully, validating totals before and after, and rounding only at presentation time.
04

A search on name LIKE '%sharma%' is very slow on a 20 million row table. What can you do?

Save to ▾
Show answer
A leading wildcard cannot use a normal B-tree index, so the database scans the table. Options are full-text indexing, trigram indexes in PostgreSQL, a reversed or computed column if the pattern is a suffix, or a search engine for heavy text search. Also limit the result, filter on other indexed columns first, and check whether users really need contains search.
05

After joining orders with payments, revenue doubled. What went wrong?

JoinsMedium
Save to ▾
Show answer
Some orders have multiple payment rows, such as partial payments or retries, so each order row repeats and SUM(order amount) counts it multiple times. This is join fan-out. Fix it by aggregating payments per order in a subquery or CTE before joining, or summing order amounts from the orders table alone. Always check row counts before and after a join.
06

A LEFT JOIN is returning fewer rows than the left table. Why could that happen?

JoinsMedium
Save to ▾
Show answer
A LEFT JOIN never removes left rows by itself, so a WHERE condition on the right table is likely filtering out rows where it is NULL. For example WHERE o.status = 'Paid' drops customers without orders. Move the condition into the ON clause or add OR o.status IS NULL. Another cause is a later INNER JOIN in the chain that removes rows.
07

A join between two large tables takes minutes. How would you speed it up?

JoinsHard
Save to ▾
Show answer
Check the execution plan for scans, join type and row estimates. Ensure join keys have indexes and matching data types, because implicit conversion blocks index use. Filter both sides early, select only needed columns, update statistics, and avoid functions on join columns. For very large data, consider partitioning, pre-aggregated tables, or batch processing outside peak hours.
08

Two systems store customer IDs differently, one as INT and one as VARCHAR with leading zeros. How do you join them?

JoinsHard
Save to ▾
Show answer
Convert consistently, for example CAST(b.cust_code AS INT) = a.customer_id, but understand this prevents index use on b.cust_code and fails on non-numeric values. Better options are a computed or persisted column with the converted value and an index, or a mapping table built during ETL. Also validate unmatched rows to catch formatting issues such as spaces.
09

You need customers who bought both product A and product B. How would you write it?

JoinsMedium
Save to ▾
Show answer
Group by customer and count distinct matching products: SELECT customer_id FROM order_items WHERE product_id IN ('A', 'B') GROUP BY customer_id HAVING COUNT(DISTINCT product_id) = 2. Alternatively use INTERSECT of two queries, or two EXISTS conditions. The HAVING approach is concise, scans the table once and is easy to extend to three or more products.
10

Your GROUP BY query returns an error that a column is not in the group by clause. How do you fix it?

Save to ▾
Show answer
Each selected column must be grouped or aggregated. Decide what you need: add the column to GROUP BY if it defines the group, aggregate it with MIN, MAX or STRING_AGG if any or all values are acceptable, or remove it. If you need details of the top row in each group, use a window function such as ROW_NUMBER instead of GROUP BY.
11

Average order value looks too low in a report. What could be wrong?

Save to ▾
Show answer
Check the grain: if the query averages order items instead of orders, each line counts separately and the average drops. Joins can also duplicate rows. Verify whether cancelled or zero-value orders are included and how NULL amounts are treated. Compute SUM(amount) / COUNT(DISTINCT order_id) at order level and compare with business definitions before changing the report.
12

A daily aggregation over 2 billion rows is too slow for a dashboard. What would you propose?

Save to ▾
Show answer
Avoid recalculating raw data on every request. Build a summary table or materialised view at daily grain refreshed incrementally, partition the fact table by date, and use columnar storage if available. Pre-compute common metrics, index the summary on dimensions used in filters, and let the dashboard query the summary. Validate summary totals against raw data periodically.
13

Management wants the percentage of each category in total sales. How do you write it?

Save to ▾
Show answer
Use a window aggregate over the grouped result: SELECT category, SUM(sales) AS sales, ROUND(100.0 * SUM(sales) / SUM(SUM(sales)) OVER (), 2) AS pct FROM sales GROUP BY category. The inner SUM is per category and the outer SUM over an empty window gives the grand total. Multiplying by 100.0 avoids integer division in some databases.
14

Your NOT IN query returns zero rows even though you expect results. What do you check?

Save to ▾
Show answer
Check whether the subquery returns any NULL values, because NOT IN with a NULL makes every comparison unknown. Run the subquery alone and look for NULLs. Fix it with NOT EXISTS, which handles NULLs correctly, or add WHERE column IS NOT NULL inside the subquery. Also confirm data types match so values compare as expected.
15

A query with a correlated subquery in SELECT runs for every row and is slow. How do you improve it?

Save to ▾
Show answer
Rewrite the correlated scalar subquery as a join to a pre-aggregated derived table or CTE, for example aggregate order counts per customer once and LEFT JOIN it. Alternatively use a window function. Ensure the correlation column is indexed if you keep the subquery. Compare execution plans and timing before and after to confirm the improvement.
16

A long report query has eight nested subqueries and nobody can maintain it. How would you refactor it?

Save to ▾
Show answer
Break it into CTEs with meaningful names, each producing one business step such as active customers, order totals and returns. Remove repeated logic, standardise filters, and add comments on business rules. Test each step's row count against the original output. If parts are reused across reports, move them into views. Finally compare results and execution plans to ensure nothing changed.
17

A recursive CTE on a category tree runs forever. What is the likely cause and fix?

Save to ▾
Show answer
The data probably has a cycle, such as category A as parent of B and B as parent of A, so recursion never ends. Add a depth column and stop at a maximum level, track a path string or array and exclude nodes already visited, and use database limits such as MAXRECURSION in SQL Server. Then fix the cyclic data at the source with constraints or validation.
18

You need to delete duplicate rows but keep one copy of each. How do you do it?

Save to ▾
Show answer
Use a CTE with ROW_NUMBER: WITH d AS (SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users) DELETE FROM users WHERE id IN (SELECT id FROM d WHERE rn > 1). SQL Server allows DELETE FROM d WHERE rn > 1 directly. Back up the table first, run a SELECT to verify, then add a unique constraint to prevent new duplicates.
19

An interviewer asks for the Nth highest salary in a reusable way. How do you answer?

Save to ▾
Show answer
Use DENSE_RANK so ties share a rank: SELECT salary FROM (SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS r FROM employees) t WHERE r = :n. Alternatively use ORDER BY salary DESC with OFFSET n-1 ROWS FETCH NEXT 1 ROW ONLY on distinct salaries. Mention how ties are handled and that per-department versions add PARTITION BY department_id.
20

Your running total jumps by large amounts on some dates instead of row by row. Why?

Save to ▾
Show answer
With ORDER BY and no explicit frame, the default frame is RANGE UNBOUNDED PRECEDING, which treats all rows with the same order value as peers and includes them together. When several rows share the same date, the running total adds them all at once. Specify ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, or add a unique tiebreaker to ORDER BY.
21

You need each customer's latest address from a history table. How do you query it?

Save to ▾
Show answer
Use ROW_NUMBER partitioned by customer and ordered by updated date descending, then keep rn = 1: WITH r AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY updated_at DESC) AS rn FROM address_history) SELECT * FROM r WHERE rn = 1. Add a tiebreaker such as id DESC in case two updates share the same timestamp.
22

An interviewer asks to show each employee's salary next to their department's average. How do you do it?

Save to ▾
Show answer
Use a window aggregate: SELECT name, department_id, salary, AVG(salary) OVER (PARTITION BY department_id) AS dept_avg, salary - AVG(salary) OVER (PARTITION BY department_id) AS diff FROM employees. Unlike GROUP BY, every employee row is kept. Mention that a join with a grouped subquery produces the same result but is longer.
23

A retention report needs the percentage of users active in each month after signup. How would you build it in SQL?

Save to ▾
Show answer
Compute each user's signup month with MIN over their activity, then derive month number as the difference between activity month and signup month. Group by cohort month and month number, counting distinct users. Divide by the cohort size using FIRST_VALUE or a window SUM over month zero within each cohort. Validate with a few known users and handle users with no activity.
24

A table stores phone numbers as 'ph1, ph2, ph3' in one column. What is wrong and how do you fix it?

Save to ▾
Show answer
The column violates first normal form because it stores multiple values in one field, making search, validation and updates difficult. Create a separate customer_phones table with customer_id, phone and type columns, one row per number, with a foreign key to customers. Migrate data by splitting the strings, then drop the old column after applications are updated.
25

How would you design a database for an online food delivery app at a high level?

Save to ▾
Show answer
Core tables are users, addresses, restaurants, menu_items, orders, order_items, payments, delivery_partners and deliveries. Orders reference users, restaurants and addresses, order_items reference orders and menu_items with price at order time, and payments and deliveries reference orders. Add status history tables for tracking and indexes on user_id, restaurant_id and created_at for common queries.
26

Product prices change often, but old orders must keep the price at the time of purchase. How do you design it?

Save to ▾
Show answer
Store the unit price, discount and tax in order_items when the order is created instead of reading the current product price, so history is not affected by updates. Maintain a product_price_history table with effective_from and effective_to dates for analysis. Reports on past orders use order_items values, while catalogue pages use the current price.
27

Your team debates using UUIDs or auto-increment integers as primary keys. What do you advise?

Save to ▾
Show answer
Auto-increment integers are compact, fast to index and naturally ordered, but expose counts and are hard to generate across distributed systems. UUIDs are globally unique and good for distributed inserts and merging data, but larger and random, causing index fragmentation. Time-ordered UUIDs such as UUIDv7 reduce fragmentation. Choose based on scale, distribution and security needs.
28

A reporting team wants to query the production OLTP database directly. What are the risks and alternatives?

Save to ▾
Show answer
Heavy analytical queries can lock rows, use CPU and slow customer transactions, and normalised schemas make reporting complex. Alternatives are a read replica for reporting, a data warehouse loaded by ETL or CDC with a star schema, or materialised summary tables. Grant read-only access with limits if direct access is unavoidable, and schedule heavy jobs off-peak.
29

A query that ran in 1 second now takes 40 seconds after a data load. What do you check?

Save to ▾
Show answer
Compare the current execution plan with the old one. A big data load often makes statistics stale, causing poor row estimates and a different join or scan choice, so update statistics first. Check for index fragmentation, missing indexes after the load, blocking from other sessions, and whether the data volume crossed a threshold. Test again after each change.
30

You added an index but the query still does a full table scan. Why might that be?

Save to ▾
Show answer
Possible reasons: the predicate is not SARGable because of a function or type conversion, the query returns a large share of rows so a scan is cheaper, the composite index column order does not match the filter, statistics are stale, or OR conditions prevent index use. Check the plan, rewrite the predicate, update statistics, or adjust the index to match the query.
31

Inserts into a heavily used table have become slow. What could be causing it?

Save to ▾
Show answer
Too many indexes increase write cost, so audit and remove unused ones. Random UUID primary keys cause page splits and fragmentation. Triggers, foreign key checks without indexes on child columns, lock contention from long transactions, and log disk bottlenecks also slow inserts. Use batch inserts, sequential keys, proper indexing of foreign keys and monitor waits to find the actual bottleneck.
32

The dashboard queries hit the production database and slow down the app at month end. What do you propose?

Save to ▾
Show answer
Separate workloads: route reporting to a read replica or data warehouse, pre-aggregate month-end metrics into summary tables refreshed after close, and cache results for dashboards. Tune the heaviest queries with indexes and better predicates. Schedule batch reports outside business hours and set resource limits or query timeouts for reporting users so customer transactions are protected.
33

A query uses SELECT * on a table with 80 columns. Why is that a problem?

Save to ▾
Show answer
SELECT * fetches every column, increasing disk reads, memory, network transfer and application processing. It prevents covering indexes from being used and can break applications when columns are added or reordered. Select only required columns, which lets the optimiser use narrower indexes and makes the intent clear. SELECT * is acceptable only for quick exploration, not production code.
34

An interviewer asks how you would find why a query is slow. What steps do you describe?

Save to ▾
Show answer
First reproduce the issue and measure time. Then check the execution plan for scans, expensive sorts, row estimate errors and join types. Look at indexes on filter and join columns, whether predicates are SARGable, and statistics freshness. Check for blocking or locks from other sessions. Change one thing at a time, measure again, and document the improvement.
35

A query with many OR conditions across different columns is slow. How can you optimise it?

Save to ▾
Show answer
OR across different columns often prevents a single index seek. Rewrite it as UNION ALL of separate queries, each using its own index, removing duplicates carefully if needed, or use IN for multiple values of the same column. Some optimisers can combine indexes with bitmap or index merge plans. Compare plans and timings to choose the best approach.
36

How do you prevent SQL injection in applications?

Save to ▾
Show answer
Never build SQL by concatenating user input. Use parameterised queries or prepared statements, where input is sent separately from the SQL text, or ORMs that do this automatically. Validate and whitelist inputs such as sort column names, give application users least privilege, and avoid dynamic SQL in procedures unless parameters are bound. Input like ' OR 1=1 -- then cannot change the query logic.
37

Two users book the last seat at the same time and both succeed. How do you fix it?

Save to ▾
Show answer
This is a race condition. Wrap the check and booking in one transaction and lock the seat row with SELECT ... FOR UPDATE so the second transaction waits and then sees it is taken. Alternatively use an atomic update such as UPDATE seats SET booked_by = :user WHERE seat_id = :id AND booked_by IS NULL and check affected rows, plus a unique constraint as a final safety net.
38

Your application logs frequent deadlock errors at peak hours. How do you investigate and reduce them?

Save to ▾
Show answer
Capture deadlock graphs or logs to see which statements and resources are involved. Common fixes are accessing tables and rows in the same order in all code paths, keeping transactions short, adding indexes so updates lock fewer rows, avoiding user interaction inside transactions, and using lower isolation where safe. Add retry logic for deadlock victims since some deadlocks cannot be fully eliminated.
39

A long-running report transaction is causing table bloat and blocking. What do you do?

Save to ▾
Show answer
Long transactions hold snapshots and locks, preventing cleanup of old row versions and blocking DDL or writers. Move the report to a read replica, break it into smaller batches, use read committed snapshot where supported, and avoid leaving transactions open in client tools. Set statement or idle-in-transaction timeouts, and monitor sessions with long transaction durations.
40

You need to update 50 million rows in production. How do you do it safely?

Save to ▾
Show answer
Avoid one huge transaction that locks the table and fills the log. Test on a copy first, take a backup, and update in batches of a few thousand rows using a key range, committing each batch and pausing briefly to reduce load. Run during low traffic, monitor replication lag and locks, keep a rollback script, and verify counts after completion.
41

A business user needs a dashboard on a complex 6-table join that runs slowly. Would you use a view or a materialised view?

Save to ▾
Show answer
A normal view only simplifies the query; it still runs the full join every time, so performance does not improve. If the data can be a few minutes or hours old, a materialised view or summary table refreshed on schedule gives fast reads. If real-time data is required, tune the underlying query with indexes or build an incrementally maintained table.
42

How would you design data archiving for a table growing by 10 million rows a month?

Save to ▾
Show answer
Partition the table by month so old partitions can be detached or moved to an archive table or cheaper storage without heavy deletes. Define a retention policy with business and compliance teams, for example 24 months online. Keep archived data queryable through a separate schema or warehouse, update indexes and statistics, and automate the process with monitoring and alerts.
43

Finance asks for the count of customers whose total spend crossed 1 lakh in each city. How do you write it?

Save to ▾
Show answer
Aggregate in two levels. First compute total spend per customer and city in a CTE: SELECT city, customer_id, SUM(amount) AS spend FROM orders GROUP BY city, customer_id. Then in the outer query: SELECT city, COUNT(*) FROM cte WHERE spend > 100000 GROUP BY city. Trying to do it in one GROUP BY by city fails because the threshold applies to each customer, not each city.
44

A COUNT(DISTINCT) query on a huge events table is very slow. What options do you have?

Save to ▾
Show answer
Exact distinct counts need sorting or hashing all values, which is expensive. Options are filtering the date range tightly, pre-aggregating daily distinct users into a summary table, indexing the columns involved, or using approximate functions such as APPROX_COUNT_DISTINCT or HyperLogLog where small error is acceptable. Explain the accuracy trade-off to stakeholders before switching to approximations.
45

An e-commerce app needs products with very different attributes, such as phones and shirts. How do you model them?

Save to ▾
Show answer
Options are a common products table plus category-specific tables (table per type), an entity-attribute-value table, or a JSON or JSONB column for flexible attributes. A practical design keeps shared fields like name, price and category as columns, stores variable attributes in JSONB with indexes on frequently filtered keys, and validates required attributes in the application or with check constraints.

Frequently asked questions

Why are scenario-based questions important?

They show how you think on real problems. Interviewers use them to separate people who memorised definitions from people who have done the work.

How should I answer them?

Clarify the situation, list likely causes, say how you would check each one, then give the fix and how you would stop it happening again.