Week 02
SQL Interview Theory: The Questions They Actually Ask
Explain the reason behind the query
Writing the right query is one part of an interview. You should also be able to explain why a key is needed, what an index helps with, and how concurrent updates can go wrong.
Start with a short answer and one example. For a composite key: "It uses more than one column to identify a row. In an enrolments table, student_id and course_code together identify one enrolment." Then discuss the trade-off or follow-up if asked. There is no need to recite every related definition at once.
There is no practice set today. Read this once, say the answers out loud, and open it again on the morning of your interview.
One schema to work through
This enrolment table repeats student, branch and course details. If a course fee changes, several rows need updating. If one is missed, the data disagrees with itself. We will use it to discuss keys, normalisation and query performance.
enrolments_raw, the college keeping its whole record in a single sheet:
| student_id | name | branch | hod | course_code | course_name | fee | marks |
|---|---|---|---|---|---|---|---|
| 1 | Asha | CSE | Prof. Rao | CS301 | DBMS | 5000 | 78 |
| 1 | Asha | CSE | Prof. Rao | CS302 | OS | 4000 | 81 |
| 2 | Ravi | IT | Prof. Iyer | CS301 | DBMS | 5000 | 65 |
| 3 | Neha | CSE | Prof. Rao | CS302 | OS | 4000 | 74 |
| 4 | Karthik | IT | Prof. Iyer | CS301 | DBMS | 5000 | 88 |
| 5 | Meera | ECE | Prof. Nair | CS302 | OS | 4000 | NULL |
Six rows, one per student and course pair, and Asha owns two because she takes two subjects. four different things are wrong here.
student_id alone cannot identify a row, because Asha appears twice. name and branch are copied on every row of the same student. fee has nothing to do with the student, it belongs to the course. hod belongs to the branch, not to the student.
Meera's NULL marks mean no mark is recorded. Keep that separate from zero; it will matter when we calculate averages and check conditions.
The Pattern
Six layers, from the table definition upward. When the interviewer jumps around, you are only moving up and down this stack.
- Keys
- Normalization
- Indexes
- Transactions
- Isolation
- Execution order
1. Keys, how a row is identified
A key is a column, or set of columns, whose value tells you exactly which row you mean. Without one, "delete Asha's DBMS entry" is not a well defined instruction.
A super key is any column set that identifies a row, minimal or not. A candidate key is a super key with nothing spare in it. In enrolments_raw the only candidate key is (student_id, course_code).
You pick one candidate key and call it the primary key: unique, never NULL, exactly one per table. The ones you did not pick are alternate keys, enforced with UNIQUE, which still allows NULL. A key built from two or more columns is a composite key.
A foreign key requires a non-NULL reference to match a key in the referenced table. It can reference a primary or suitable unique key, including one in the same table. The chosen delete action decides whether deleting a parent is blocked, cascades to children, or changes the reference.
TipTip: A surrogate key is an id with no business meaning. A natural key comes from the data itself. A surrogate id is useful when business identifiers may change, but you may still need a unique constraint on the natural identifier.
2. Normalization, store every fact exactly once
Before the sheet above, the clerk kept one row per student with course_code holding "CS301, CS302" and marks holding "78, 81". That is unnormalised. Split those cells and you reach 1NF: atomic values, no repeating groups, one row per student and course. The table above is that 1NF table.
2NF removes partial dependencies, meaning a non-key column depending on only part of the composite key. Here name, branch and hod depend on student_id alone, course_name and fee on course_code alone. Pull both groups out.
Students
| student_id | name | branch | hod |
|---|---|---|---|
| 1 | Asha | CSE | Prof. Rao |
| 2 | Ravi | IT | Prof. Iyer |
| 3 | Neha | CSE | Prof. Rao |
| 4 | Karthik | IT | Prof. Iyer |
| 5 | Meera | ECE | Prof. Nair |
Courses
| course_code | course_name | fee |
|---|---|---|
| CS301 | DBMS | 5000 |
| CS302 | OS | 4000 |
Enrolments
| student_id | course_code | marks |
|---|---|---|
| 1 | CS301 | 78 |
| 1 | CS302 | 81 |
| 2 | CS301 | 65 |
| 3 | CS302 | 74 |
| 4 | CS301 | 88 |
| 5 | CS302 | NULL |
The fee now exists in exactly one place, and Enrolments keeps only what is truly about the pair, the marks.
3NF removes transitive dependencies, a non-key column depending on another non-key column. Inside Students, hod depends on branch, and branch is not a key. One more split.
Students
| student_id | name | branch |
|---|---|---|
| 1 | Asha | CSE |
| 2 | Ravi | IT |
| 3 | Neha | CSE |
| 4 | Karthik | IT |
| 5 | Meera | ECE |
Branches
| branch | hod |
|---|---|
| CSE | Prof. Rao |
| IT | Prof. Iyer |
| ECE | Prof. Nair |
Three student rows in CSE and IT now point at one HOD row each, instead of carrying a copy of the name.
Now nothing is stored twice and the three anomalies interviewers name are gone. Change the DBMS fee in one place, no row left behind, that is the update anomaly. Add a course before anybody enrols, the insert anomaly. Remove the last ECE student without losing Prof. Nair, the delete anomaly.
BCNF strengthens 3NF: for every non-trivial functional dependency, the determining columns must form a superkey. In other words, a set of columns should not determine other facts unless it can identify the row.
3. Indexes, how rows are found fast
An index provides another way to find rows without scanning the whole table. A B-tree lookup takes roughly logarithmic work to find a starting point, then additional work to read the matching entries and rows.
A common index structure is a B-tree. Its ordered entries support equality checks, ranges and some ordering requirements. Hash indexes mainly support equality lookups. The useful choice depends on the query and database.
| An index may help | Check before assuming it helps |
|---|---|
WHERE or JOIN on that column | the query returns most of the table |
ORDER BY or GROUP BY in that column order | the column is wrapped in a function |
MIN and MAX | a leading wildcard, like LIKE '%kar' |
| covering reads, when every column needed is in the index | the column has few distinct values |
Two useful points to explain:
Leading columns. An index on (course_code, marks) is ordered by course first and marks within that course. It is a natural fit for a course filter, possibly followed by a marks range. A marks-only filter is less direct; the planner may scan the index or use another optimisation. Check the plan rather than treating this as an absolute rule.
Cost. Every insert, update and delete maintains every index on the table, so an index is never free.
A clustered index stores the table data in its index structure in engines that support it. Secondary indexes provide another route to those rows. The details differ: MySQL InnoDB organises data around the primary key, while PostgreSQL does not maintain that same clustered layout automatically.
4. Transactions and ACID
A transaction is a group of statements that must happen as one unit: BEGIN, do the work, then COMMIT to keep it or ROLLBACK to undo all of it. SAVEPOINT marks a spot inside it that you can roll back to partially.
| Letter | Property | One line |
|---|---|---|
| A | Atomicity | all statements or none, so a debit can never happen without its credit |
| C | Consistency | valid state to valid state, every constraint and key still holds at the end |
| I | Isolation | concurrent transactions do not see each other's half done work |
| D | Durability | once committed, a power cut cannot undo it |
The four letters are one bank transfer told from four angles, which is how you should say it out loud.
5. Isolation levels
Stronger isolation restricts which concurrent outcomes are allowed. It can add waiting or retries. This table shows the anomalies permitted by the SQL standard; actual database behaviour can be stronger.
| Level | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
| READ UNCOMMITTED | possible | possible | possible |
| READ COMMITTED | no | possible | possible |
| REPEATABLE READ | no | no | possible |
| SERIALIZABLE | no | no | no |
A dirty read sees another transaction's uncommitted change. A non-repeatable read is the same SELECT twice giving a different value. A phantom read is the same SELECT twice giving a different row count, because somebody inserted a row that matches your condition.
PostgreSQL defaults to READ COMMITTED and MySQL InnoDB to REPEATABLE READ. PostgreSQL's REPEATABLE READ also prevents phantom reads, although it still allows serialization anomalies. SQLite normally serialises writes and provides snapshot isolation in WAL mode. These details matter when discussing a real application. The PostgreSQL isolation documentation explains its guarantees.
6. Execution order, the fence
The clause you write first is not the clause that runs first. Build the query up in three steps and you can see it happen.
- FROM + JOIN
- WHERE
- GROUP BY
- HAVING
- SELECT
- ORDER BY
- LIMIT
Step 1, just read the column. SELECT branch FROM Students; gives five rows, one per student: CSE, IT, CSE, IT, ECE. Nothing is folded yet.
Step 2, group them. GROUP BY takes rows sharing a value and folds them into one row per value, so you can count each pile.
SELECT branch, COUNT(*) AS students
FROM Students
GROUP BY branch;| branch | students |
|---|---|
| CSE | 2 |
| ECE | 1 |
| IT | 2 |
Five rows have become three, and COUNT(*) is how many original rows went into each pile.
Step 3, filter the piles and sort them. HAVING filters groups, the way WHERE filters rows.
SELECT branch, COUNT(*) AS students
FROM Students
WHERE branch IS NOT NULL
GROUP BY branch
HAVING COUNT(*) >= 2
ORDER BY students DESC, branch ASC;| branch | students |
|---|---|
| CSE | 2 |
| IT | 2 |
Read the query line by line:
FROM Studentsreads the five student rows.WHERE branch IS NOT NULLdrops rows with no branch, one row at a time. Here nothing is dropped.GROUP BY branchfolds those rows into one pile per branch, so three piles.HAVING COUNT(*) >= 2drops whole piles, so ECE with its single student is gone.SELECT branch, COUNT(*) AS studentsbuilds the output columns, and the namestudentsis born right here.ORDER BY students DESC, branch ASCsorts what survived, and the alias is finally visible.
Dry Run, the same query counted step by step:
| Step | What happens | Rows alive |
|---|---|---|
| FROM Students | read the table | 5 |
| WHERE branch IS NOT NULL | per row filter | 5 |
| GROUP BY branch | fold into groups | 3, CSE and IT and ECE |
| HAVING COUNT(*) >= 2 | per group filter | 2, ECE is dropped here |
| SELECT branch, COUNT(*) AS students | build the output columns | 2 |
| ORDER BY students DESC, branch ASC | sort the result | 2 |
CSE and IT tie on 2, so the second sort key branch ASC is what fixes the order between them. Drop it and the rows can come out either way on a rerun, which is how a query passes on your laptop and fails on the judge.
What an OA judge sees. Judges never read your query. They capture the printed rows and compare them as text against a saved expected output:
CSE|2
IT|2Pipe separated, no header, no padding, in exactly the order your ORDER BY produced. The practice judge here works the same way, on SQLite. Get the order or the decimals wrong and the case fails, even with correct logic.
Three consequences of the fence, asked constantly. WHERE runs before GROUP BY, so it can never hold an aggregate. A SELECT alias is invisible to WHERE, GROUP BY and HAVING, but visible to ORDER BY. LIMIT is last, so it cuts the sorted result, not the scan.
In the query above, you replace HAVING COUNT(*) >= 2 with WHERE students >= 2. What happens?
Query Templates
SQL
-- The 3NF schema in one go: primary key, composite key, foreign keys, unique, index.
CREATE TABLE Branches (
branch TEXT PRIMARY KEY,
hod TEXT NOT NULL
);
CREATE TABLE Students (
student_id INTEGER PRIMARY KEY, -- surrogate primary key
name TEXT NOT NULL,
email TEXT UNIQUE, -- alternate key: unique, but NULL allowed
branch TEXT REFERENCES Branches(branch) -- foreign key
);
CREATE TABLE Courses (course_code TEXT PRIMARY KEY, course_name TEXT NOT NULL, fee INTEGER NOT NULL);
CREATE TABLE Enrolments (
student_id INTEGER NOT NULL REFERENCES Students(student_id) ON DELETE CASCADE,
course_code TEXT NOT NULL REFERENCES Courses(course_code),
marks INTEGER, -- NULL means the student was absent
PRIMARY KEY (student_id, course_code) -- composite key
);
CREATE INDEX idx_enrol_course ON Enrolments(course_code, marks);
-- A transaction: all of it, or none of it.
BEGIN TRANSACTION;
UPDATE Accounts SET balance = balance - 500 WHERE id = 1;
UPDATE Accounts SET balance = balance + 500 WHERE id = 2;
COMMIT; -- ROLLBACK; undoes both
-- A view is a saved query, not stored data.
CREATE VIEW v_branch_strength AS
SELECT b.branch, b.hod, COUNT(s.student_id) AS students
FROM Branches b LEFT JOIN Students s ON s.branch = b.branch
GROUP BY b.branch, b.hod;That index is not decoration, and you can prove it in ten seconds. EXPLAIN QUERY PLAN prints the plan the engine chose instead of running the query.
EXPLAIN QUERY PLAN
SELECT course_code, marks FROM Enrolments WHERE course_code = 'CS301';SEARCH Enrolments USING COVERING INDEX idx_enrol_course (course_code=?)SEARCH means it jumped straight to the matching entries. COVERING means both columns were read out of the index itself and the table was never touched. Drop the index and the same line becomes SCAN Enrolments, which is the engine reading every row.
With WHERE marks = 78, this example can use SCAN Enrolments USING COVERING INDEX idx_enrol_course. It reads the index rather than searching a specific course range. Plans depend on data and statistics; SQLite also has optimisations such as skip-scan for some non-leading-column filters. See the SQLite optimiser notes.
Variations
Variation 1 - DELETE vs TRUNCATE vs DROP
Start with what each command removes, then mention the database when discussing rollback and triggers.
| DELETE | TRUNCATE | DROP | |
|---|---|---|---|
| Type | DML | DDL | DDL |
| Removes | selected rows | all rows | rows plus the table itself |
WHERE | yes | no | no |
| Work involved | deletes matching rows; optimisations vary | generally avoids individual row deletion | removes the table and its data |
| Rollback | depends on transaction support | engine-specific | engine-specific |
| Triggers | row delete triggers can run | no row delete triggers; dedicated triggers vary | engine-specific DDL behaviour |
| Identity counter | generally retained | engine and options determine reset behaviour | removed with the table |
SQLite has no TRUNCATE command. It can optimise a DELETE without WHERE when the required conditions hold. In other engines, do not infer rollback or trigger behaviour from the DDL/DML label alone.
Variation 2 - UNION vs UNION ALL
UNION stacks the output of two queries into one list. Toppers and backlog cases from Enrolments, together:
SELECT course_code FROM Enrolments WHERE marks >= 80
UNION ALL
SELECT course_code FROM Enrolments WHERE marks < 70;| course_code |
|---|
| CS301 |
| CS302 |
| CS301 |
Karthik's 88 in CS301 and Asha's 81 in CS302 from the top half, Ravi's 65 in CS301 from the bottom half. Meera's NULL fails both >= 80 and < 70, so she appears in neither. That is three valued logic doing its job, not a bug.
Swap UNION ALL for UNION and you get two distinct values, CS301 and CS302. That does not guarantee their output order; add ORDER BY when order matters. Use UNION ALL when duplicates should remain or are already ruled out.
Both forms need the same column count and compatible types, names come from the first SELECT, and one ORDER BY at the very end applies to the combined result.
For JOIN vs subquery, first check whether both forms return the same rows. The optimiser may transform either form, so compare plans before claiming one is faster. For EXISTS vs IN, be especially careful with negative checks: a NULL returned by a NOT IN subquery can change the result, while NOT EXISTS tests whether a matching row is present.
Variation 3 - NULL, and the rest of the syllabus
NULL is not a value, it is "unknown", so SQL is three valued: TRUE, FALSE, UNKNOWN. NULL = NULL is UNKNOWN, which is why you write IS NULL. WHERE keeps only TRUE rows, so UNKNOWN rows vanish silently with no error.
SELECT COUNT(*) AS rows_seen, COUNT(marks) AS graded, AVG(marks) AS avg_marks
FROM Enrolments;| rows_seen | graded | avg_marks |
|---|---|---|
| 6 | 5 | 77.2 |
COUNT(*) counts rows, COUNT(marks) counts non NULL values, and AVG follows the second one, dividing by 5. With these integer marks, SUM(marks) / COUNT(*) divides by 6 and truncates to 64 in SQLite. Even with decimal division, the denominator would still be wrong. The judge prints 77.2, not 77.20, so use ROUND(...) only when the statement asks for it.
GROUP BY and DISTINCT are the two places where NULLs are treated as equal, and all NULLs fall into one group.
CHAR vs VARCHAR. CHAR(10) is fixed width and space padded, so 'IT' is compared as 'IT ', while VARCHAR(10) stores only what you gave it. SQLite ignores the length and treats every text column as TEXT.
Views vs materialized views. A view is a stored query with no data of its own, so it re-runs every time and is always current. A materialized view stores the result on disk, so a dashboard may read it more cheaply, but it is only as fresh as the last refresh. Postgres and Oracle have them, MySQL and SQLite do not.
Stored procedures and triggers. A procedure is a named block of SQL living inside the database, called with CALL; a function is the same idea but must return a value. A trigger is a block the database fires by itself BEFORE or AFTER an INSERT, UPDATE or DELETE, used for audit logs and rules a constraint cannot express. Triggers are invisible, so a bad one makes a plain insert mysteriously slow or fail.
Relational vs NoSQL databases. Relational databases organise data in tables with keys, constraints and joins. NoSQL covers several models, including documents, key-value pairs and graphs. Transactions, consistency guarantees and scaling options vary by product. Choose based on the data relationships, access patterns and guarantees the application needs.
Nobody scored 90 in either course. What does SELECT course_name FROM Courses c WHERE 90 NOT IN (SELECT marks FROM Enrolments e WHERE e.course_code = c.course_code); return?
Common Mistakes
- Giving only a definition. Add a small example: a composite key on
(student_id, course_code)is easier to discuss than a memorised sentence alone. - Saying an index always helps. Explain the access pattern, write cost and storage cost. Check the plan before claiming an improvement.
- Ignoring index column order. An index on
(course_code, marks)is a natural fit for a course filter. A marks-only filter may need another index or a different plan. - Using
HAVINGfor a simple row filter. Put ordinary row conditions inWHERE. UseHAVINGfor conditions on grouped results. - Overlooking NULL in
NOT IN. A NULL in the subquery can make the condition UNKNOWN.NOT EXISTSis often clearer when checking for missing matches. - Giving one rollback rule for every database. Name the engine. For example, MySQL
TRUNCATEcommits implicitly; PostgreSQL can roll it back in a transaction. - Assuming the result is already sorted. Only a final
ORDER BYguarantees output order. - Quoting an isolation table as universal behaviour. The SQL standard describes permitted anomalies. A database may provide stronger guarantees at a named level.
Interview Notes
Primary key vs unique constraint? A table has at most one primary key, which identifies its rows. It can have several unique constraints. For a nullable unique column, check the engine's NULL rules rather than assuming they are identical everywhere. In SQLite, declare NOT NULL explicitly on non-integer primary-key columns in ordinary tables because of its legacy NULL behaviour.
Can a foreign key be NULL? Yes, if the column allows NULL. A non-NULL reference must match the referenced key. Deleting the parent follows the configured action, such as blocking the delete, cascading it or setting the reference to NULL.
Why normalise, and when would you stop? Normalisation reduces redundant facts and the update anomalies they cause. Start with the dependencies in the data. If a reporting workload needs a summary table or deliberate duplication, explain how it will stay in sync.
Why not index everything? Indexes need storage and maintenance when data changes. Choose them for actual filters, joins and ordering requirements, then inspect the plan and measure the workload.
Explain ACID with an example. For a transfer of 500 rupees, atomicity keeps the debit and credit together. Consistency means the transaction preserves the required constraints and business rules. Isolation controls concurrent interactions. Durability preserves a committed result under the database's configured guarantees.
Which isolation level would you choose? First identify what concurrent operations must not do: overspend a balance, reserve the last seat twice, or read an inconsistent report. Then choose constraints, locking and an isolation level for that requirement. Stronger isolation may require retries, so discuss those too.
What would you check in a slow query? Start with the plan: row estimates, scans, join sizes, sorts and useful indexes. Check whether a join multiplies more rows than expected. Compare alternative queries only after confirming they mean the same thing. EXPLAIN ANALYZE, where supported, executes the query and reports actual work.
40 rapid-fire questions with one-line answers
- DBMS vs RDBMS? An RDBMS keeps data in related tables with keys and constraints.
- DDL, DML, DCL, TCL? CREATE and DROP; SELECT and UPDATE; GRANT and REVOKE; COMMIT and ROLLBACK.
- Candidate key? The minimal column set that identifies a row uniquely.
- Super key? Any column set that identifies a row, minimal or not.
- Composite key? A key of two or more columns, like
(student_id, course_code). - Alternate key? A candidate key you did not make the primary key.
- Surrogate key? A generated id with no business meaning, preferred because business data changes.
- Referential integrity? A foreign key value must exist in the parent, or be NULL.
- ON DELETE CASCADE? Delete the parent and its child rows go too.
- 1NF? Atomic cells, no repeating groups.
- 2NF? 1NF plus nothing depending on part of a composite key.
- 3NF? 2NF plus no non-key column depending on another non-key column.
- BCNF? For each non-trivial dependency, the determinant must be a superkey.
- De-normalization? Duplicating data on purpose so reads skip the joins.
- Update anomaly? One fact in many rows, so a missed row breaks consistency.
- Index? An additional access structure that can make lookups cheaper.
- Clustered vs non-clustered? Clustered stores table data in the index structure; secondary indexes provide other access paths. Details depend on the engine.
- B-tree vs hash? B-tree does ranges and sorting, hash does equality only.
- When does an index hurt? Write heavy tables, low cardinality columns, wide result sets.
- Leftmost prefix? Leading columns determine the most direct lookup paths; check the plan for other filters.
- ACID? Atomicity, Consistency, Isolation, Durability.
- Transaction? Statements that commit or roll back together.
- COMMIT, ROLLBACK, SAVEPOINT? Keep it, undo it, mark a point to undo back to.
- Dirty read? Reading an uncommitted change.
- Non-repeatable read? Same row twice, different value.
- Phantom read? Same query twice, different row count.
- Which level stops all three? SERIALIZABLE.
- Default isolation level? READ COMMITTED in Postgres, REPEATABLE READ in MySQL InnoDB.
- DELETE vs TRUNCATE? DELETE can select rows; TRUNCATE empties the table. Rollback, triggers and identity handling depend on the engine.
- TRUNCATE vs DROP? TRUNCATE empties the table, DROP removes it.
- WHERE vs HAVING? Rows before grouping, groups after.
- Execution order? FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT.
- UNION vs UNION ALL? UNION removes duplicate result rows; UNION ALL keeps them. Neither guarantees output order.
- INNER vs LEFT JOIN? Matches only, versus every left row padded with NULL.
- CROSS JOIN? Every row with every row, the Cartesian product.
- Self join? A table joined to itself with two aliases, like employee to manager.
- CHAR vs VARCHAR? Fixed width and space padded, versus only what you gave it.
- COUNT(*) vs COUNT(col)? Rows, versus non-NULL values.
- View vs materialized view? A view reads through a saved query; a materialized view stores a result that needs refreshing.
- SQL vs NoSQL? Relational tables versus models such as documents, key-value pairs and graphs; compare the actual guarantees and workload.
TipTip: Use these as revision prompts. Give a short answer, then explain one example or limitation. If you are unsure, say which part needs checking.
Quick Test
Check what you learned
In enrolments_raw, which column set is the only candidate key?
