Skip to the questions

SQL & DBMS · Fresher to 3 years

SQL interview questions

The 40 SQL and DBMS questions that come up most in interviews, built on one small company database. Each one has a short answer you can say out loud in about 30 seconds, and the full explanation, with the query and its result, for when you need it.

Read the quick answers

3 DBMS basics · 4 Keys · 5 Core SQL · 5 Joins · 6 Grouping · 4 Subqueries · 3 Window functions · 2 Normalization · 3 Transactions · 2 Indexes · 3 Views & proceduresUpdated 27 Oct 2026

When is your interview?

Before you startThe four tables every query uses

Every query on this page runs against this small company database. The data is deliberately awkward: it has a top boss with no manager (Asha), an employee with no department (Suresh), a department with no employees (Finance), two employees on the same salary (Priya and Arjun), two customers sharing an email (the two Rahuls), customers with no orders (Fatima and Deepak), and orders saved twice or three times (104 and 105; 107, 108 and 111). Those are exactly the cases interview questions probe.

TableColumnsKeys and constraints
departmentsdept_id, dept_name, locationPK dept_id; dept_name NOT NULL UNIQUE
employeesemp_id, emp_name, email, salary, hire_date, dept_id, manager_idPK emp_id; email UNIQUE; salary > 0; FK dept_id → departments (nullable); FK manager_id → employees (nullable, a self-reference)
customerscustomer_id, customer_name, email, city, signup_datePK customer_id; email deliberately not unique
ordersorder_id, customer_id, emp_id, order_date, amount, statusPK order_id; FK customer_id → customers (NOT NULL); FK emp_id → employees (nullable: the sales rep); status in placed, delivered, cancelled

The script (MySQL 8)

SQL
CREATE TABLE departments (
  dept_id   INT AUTO_INCREMENT PRIMARY KEY,
  dept_name VARCHAR(50) NOT NULL UNIQUE,
  location  VARCHAR(50)
);

CREATE TABLE employees (
  emp_id     INT AUTO_INCREMENT PRIMARY KEY,
  emp_name   VARCHAR(100) NOT NULL,
  email      VARCHAR(100) NOT NULL UNIQUE,
  salary     DECIMAL(10,2) NOT NULL CHECK (salary > 0),
  hire_date  DATE NOT NULL,
  dept_id    INT NULL,
  manager_id INT NULL,
  FOREIGN KEY (dept_id) REFERENCES departments (dept_id),
  FOREIGN KEY (manager_id) REFERENCES employees (emp_id)
);

CREATE TABLE customers (
  customer_id   INT AUTO_INCREMENT PRIMARY KEY,
  customer_name VARCHAR(100) NOT NULL,
  email         VARCHAR(100),
  city          VARCHAR(50),
  signup_date   DATE NOT NULL
);

CREATE TABLE orders (
  order_id    INT AUTO_INCREMENT PRIMARY KEY,
  customer_id INT NOT NULL,
  emp_id      INT NULL,
  order_date  DATE NOT NULL,
  amount      DECIMAL(10,2) NOT NULL CHECK (amount > 0),
  status      VARCHAR(20) NOT NULL DEFAULT 'placed'
              CHECK (status IN ('placed', 'delivered', 'cancelled')),
  FOREIGN KEY (customer_id) REFERENCES customers (customer_id),
  FOREIGN KEY (emp_id) REFERENCES employees (emp_id)
);

INSERT INTO departments (dept_id, dept_name, location) VALUES
  (1, 'Engineering', 'Bengaluru'),
  (2, 'Sales', 'Mumbai'),
  (3, 'HR', 'Pune'),
  (4, 'Finance', 'Delhi');                                                   -- no employees

INSERT INTO employees (emp_id, emp_name, email, salary, hire_date, dept_id, manager_id) VALUES
  (1, 'Asha Rao', '[email protected]', 250000, '2015-04-01', 1, NULL),         -- the top boss
  (2, 'Ravi Kumar', '[email protected]', 180000, '2017-06-15', 1, 1),
  (3, 'Priya Shah', '[email protected]', 120000, '2019-01-10', 1, 2),
  (4, 'Arjun Mehta', '[email protected]', 120000, '2020-03-02', 1, 3),        -- same salary as Priya
  (5, 'Sneha Iyer', '[email protected]', 150000, '2018-08-20', 2, 1),
  (6, 'Vikram Singh', '[email protected]', 95000, '2021-11-11', 2, 5),
  (7, 'Sana Khan', '[email protected]', 160000, '2022-02-14', 2, 5),           -- earns more than her manager
  (8, 'Meera Nair', '[email protected]', 85000, '2023-07-01', 3, 1),
  (9, 'Suresh Pillai', '[email protected]', 70000, '2024-01-15', NULL, 1),   -- no department
  (10, 'Sameer Joshi', '[email protected]', 90000, '2024-09-01', 3, 8);      -- earns more than his manager

INSERT INTO customers (customer_id, customer_name, email, city, signup_date) VALUES
  (1, 'Kiran Patel', '[email protected]', 'Ahmedabad', '2023-01-05'),
  (2, 'Lakshmi Menon', '[email protected]', 'Kochi', '2023-02-11'),
  (3, 'Rahul Verma', '[email protected]', 'Delhi', '2023-03-20'),
  (4, 'Rahul V', '[email protected]', 'Delhi', '2024-06-02'),                   -- signed up twice
  (5, 'Fatima Sheikh', '[email protected]', 'Hyderabad', '2024-08-15'),        -- no orders
  (6, 'Deepak Rao', '[email protected]', 'Pune', '2025-01-10');                -- no orders

INSERT INTO orders (order_id, customer_id, emp_id, order_date, amount, status) VALUES
  (101, 1, 5, '2025-01-15', 1200.00, 'delivered'),
  (102, 1, 6, '2025-01-28', 450.00, 'delivered'),
  (103, 2, 7, '2025-02-03', 3200.00, 'cancelled'),
  (104, 3, 5, '2025-02-10', 899.00, 'delivered'),
  (105, 3, 5, '2025-02-10', 899.00, 'delivered'),                             -- a duplicate of 104
  (106, 4, NULL, '2025-03-05', 2500.00, 'placed'),
  (107, 2, 7, '2025-03-18', 1500.00, 'delivered'),
  (108, 2, 7, '2025-03-18', 1500.00, 'delivered'),                            -- a duplicate of 107
  (109, 1, 6, '2025-03-30', 700.00, 'cancelled'),
  (110, 3, 5, '2025-04-02', 5600.00, 'delivered'),
  (111, 2, 7, '2025-03-18', 1500.00, 'delivered');                            -- and another

Who reports to whom: Asha manages Ravi, Sneha, Meera and Suresh; Ravi manages Priya; Priya manages Arjun; Sneha manages Vikram and Sana; Meera manages Sameer.

01DBMS basics

0/3 got it

Q01Not marked yetFresherAsked very oftenConcept

What is a DBMS, and how is an RDBMS different?

Say this first~30 sec to say

A DBMS (database management system) is software that stores, retrieves and manages data for you, handling concurrency, security, backups and crash recovery, instead of every program juggling its own files. An RDBMS is a DBMS that keeps data in tables of rows and columns, links tables through keys, enforces constraints, and is queried with SQL. MySQL, PostgreSQL, Oracle and SQL Server are RDBMSs; MongoDB and Redis are DBMSs that aren't relational.

Could you say it?
Full answer2 min read

Why not just use files?

Problem with plain filesWhat a DBMS does
Two programs writing at once corrupt the dataConcurrency control and locking
A crash halfway through an update leaves half-written dataTransactions and recovery logs
The same data copied into many files drifts apartOne shared, consistent store
Finding records means reading whole filesIndexes and a query language
Anyone who can open the file sees everythingUsers, permissions and auditing
DBMSRDBMS
Data stored asFiles, documents, key-value pairs, graphs or tablesTables with rows and columns
RelationshipsUp to the applicationForeign keys, enforced by the database
Integrity rulesVariesConstraints: primary keys, foreign keys, CHECK, UNIQUE
Query languageVariesSQL
ExamplesMongoDB, Redis, older file-based systemsMySQL, PostgreSQL, Oracle, SQL Server

The usual warm-up, "SQL vs MySQL": SQL is a language, standardised by ANSI and ISO. MySQL is one product, an RDBMS, that implements SQL along with its own extensions, such as LIMIT and AUTO_INCREMENT.

Follow-ups

  • What is a schema? The structure of the database: its tables, columns, types, keys and constraints, as opposed to the data in them.
  • Is MongoDB a DBMS? Yes, a document-oriented DBMS. It isn't relational: it stores JSON-like documents and doesn't enforce joins and foreign keys the same way.
Q02Not marked yetFresherAsked very oftenConcept

What are DDL, DML, DCL and TCL, and which commands belong to each?

Say this first~25 sec to say

SQL commands fall into groups by what they act on. DDL (data definition) changes the structure: CREATE, ALTER, DROP, TRUNCATE, RENAME. DML (data manipulation) changes the rows: INSERT, UPDATE, DELETE. DCL (data control) manages permissions: GRANT, REVOKE. TCL (transaction control) groups changes into transactions: COMMIT, ROLLBACK, SAVEPOINT. Many lists also give SELECT its own group, DQL (data query); others count it as DML.

Could you say it?
Full answer2 min read
GroupStands forCommandsActs on
DDLData Definition LanguageCREATE, ALTER, DROP, TRUNCATE, RENAMETables, indexes, views: the structure
DMLData Manipulation LanguageINSERT, UPDATE, DELETE (some include SELECT)The data in tables
DQLData Query LanguageSELECTReading data
DCLData Control LanguageGRANT, REVOKEUsers' permissions
TCLTransaction Control LanguageCOMMIT, ROLLBACK, SAVEPOINT, START TRANSACTIONTransactions
SQL
-- DDL: change the structure
ALTER TABLE employees ADD COLUMN phone VARCHAR(15);

-- DML: change the data
UPDATE employees SET salary = salary * 1.10 WHERE dept_id = 2;

-- DCL: change who may do what
GRANT SELECT ON company.orders TO 'report_user'@'%';

-- TCL: make changes permanent, or undo them
START TRANSACTION;
DELETE FROM orders WHERE status = 'cancelled';
ROLLBACK;                                          -- the cancelled orders are back

Why the grouping matters: in MySQL, DDL statements commit implicitly, so they end any open transaction and can't be rolled back. DML inside a transaction can be. That's the root of the DELETE vs TRUNCATE difference (question 8).

Follow-ups

  • Is TRUNCATE DDL or DML? DDL: it deallocates and recreates the table's storage rather than deleting rows one by one.
  • What does GRANT ALL do? Gives every privilege on the named objects. In production, grant only what each application user needs.
Q03Not marked yetFresherAsked oftenConcept

What does an ER diagram show, and how do one-to-one, one-to-many and many-to-many relationships become tables?

Say this first~30 sec to say

An ER (entity–relationship) diagram models data before you build tables: entities (things such as Employee and Department), their attributes (name, salary), and the relationships between them, with cardinality: how many of one relate to how many of the other. In tables: a one-to-many relationship puts a foreign key on the "many" side; a one-to-one puts a unique foreign key on either side; a many-to-many needs a junction table holding both keys.

Could you say it?
Full answer2 min read

Mapping each relationship to tables

RelationshipExampleTables
One-to-manyOne department has many employeesemployees.dept_id → departments.dept_id
One-to-oneOne employee has one company laptoplaptops.emp_id UNIQUE → employees.emp_id
Many-to-manyEmployees work on many projects; projects have many employeesA junction table employee_projects (emp_id, project_id)
Recursive (self)An employee's manager is an employeeemployees.manager_id → employees.emp_id
SQL
-- The many-to-many case: a junction table
CREATE TABLE projects (
  project_id INT AUTO_INCREMENT PRIMARY KEY,
  name       VARCHAR(100) NOT NULL
);

CREATE TABLE employee_projects (
  emp_id     INT NOT NULL,
  project_id INT NOT NULL,
  role       VARCHAR(50),                         -- attributes of the relationship itself live here
  PRIMARY KEY (emp_id, project_id),                -- a composite key: one row per pairing
  FOREIGN KEY (emp_id) REFERENCES employees (emp_id),
  FOREIGN KEY (project_id) REFERENCES projects (project_id)
);

ER notation in brief: rectangles for entities, ovals for attributes (underlined for keys), diamonds for relationships, and 1, N or M on the lines for cardinality. Crow's-foot notation draws the "many" end as a three-pronged foot.

Follow-ups

  • What is a weak entity? One that can't be identified by its own attributes alone, such as an order line identified only together with its order. It takes part of its key from its owner.
  • What are composite, multivalued and derived attributes? Composite: made of parts (an address of street, city, PIN). Multivalued: several values (phone numbers), which become a separate table. Derived: computed from others (age from date of birth), usually not stored.

02Keys and constraints

0/4 got it

Q04Not marked yetFresherAsked very oftenConcept

Primary key vs unique key: how do they differ?

Say this first~30 sec to say

Both reject duplicate values. A primary key is the table's main identifier: there's only one per table, it can never be NULL, and other tables' foreign keys usually point at it; in InnoDB it also decides how the rows are physically stored. A table can have several unique keys, and they accept NULLs, because NULL isn't equal to anything, not even another NULL. In employees, emp_id is the primary key and email a unique key.

Could you say it?
Full answer1 min read
Primary keyUnique key
How many per tableOne (it may span several columns)Any number
NULL allowedNoYes (how many depends on the database)
DuplicatesNoNo
Index createdYes; clustered in InnoDB (question 36)Yes, a secondary index
Typical useThe row's identity, referenced by foreign keysNatural identifiers that must stay unique: email, PAN, phone
SQL
INSERT INTO employees (emp_name, email, salary, hire_date)
VALUES ('Test User', '[email protected]', 50000, '2025-01-01');
-- ERROR 1062 (23000): Duplicate entry '[email protected]' for key 'employees.email'

NULLs in unique columns differ between databases: MySQL and PostgreSQL allow many NULLs in a unique column; SQL Server allows only one, unless you use a filtered index.

Follow-ups

  • Can a primary key span several columns? Yes, a composite primary key, such as (emp_id, project_id) in a junction table (question 6).
  • Natural or surrogate key? A natural key comes from the data (an email); a surrogate key is invented (an auto-increment ID). Surrogates are usually preferred as primary keys, because natural values can change.
Q05Not marked yetFresherAsked very oftenConcept

What does a foreign key enforce, and what happens to employees when their department is deleted under RESTRICT, CASCADE or SET NULL?

Say this first~30 sec to say

A foreign key enforces referential integrity: a value in the child column must exist in the parent's key, or be NULL if the column allows it, so employees.dept_id can't point at a department that doesn't exist. The ON DELETE rule decides what happens when a referenced parent row is deleted. RESTRICT (InnoDB's default) blocks the delete; CASCADE deletes the child rows too; SET NULL keeps the children but clears their reference.

Could you say it?
Full answer2 min read
SQL
CREATE TABLE employees_demo (
  emp_id   INT PRIMARY KEY,
  emp_name VARCHAR(100) NOT NULL,
  dept_id  INT NULL,
  FOREIGN KEY (dept_id) REFERENCES departments (dept_id)
    ON DELETE SET NULL                    -- or RESTRICT (the default), or CASCADE
    ON UPDATE CASCADE
);

Deleting the Sales department (dept_id 2), which has three employees

RuleWhat happens
RESTRICT / NO ACTION (default)The delete fails: ERROR 1451: Cannot delete or update a parent row: a foreign key constraint fails. Move or delete the employees first
CASCADESales and Sneha, Vikram and Sana are deleted
SET NULLSales is deleted; the three employees stay, with dept_id set to NULL

Choosing a rule: CASCADE suits parts that can't exist without their parent, such as an order's line items. SET NULL suits optional links. RESTRICT is the safe default when deleting the parent would be a mistake, as with a department that still has staff.

The usual opening version, "primary key vs foreign key":

Primary keyForeign key
PurposeIdentifies each row in its own tableLinks a row to a row in another (or the same) table
UniqueYesNo: many employees share a dept_id
NULLNeverAllowed if the column is nullable
Per tableOneAny number

Follow-ups

  • Can a foreign key reference its own table? Yes: employees.manager_id references employees.emp_id, a self-referencing key.
  • Does MySQL index foreign key columns? InnoDB requires an index on them and creates one automatically if none exists.
Q07Not marked yetFresherAsked oftenQuery

Write the CREATE TABLE statement for employees with all of its keys and constraints

Say this first~30 sec to say

Declare each column with its type and NOT NULL where required, make emp_id an AUTO_INCREMENT primary key, put UNIQUE on email, a CHECK (salary > 0) constraint, and two foreign keys: dept_id referencing departments, and manager_id referencing employees itself. Naming constraints, such as CONSTRAINT fk_emp_dept, makes error messages and later changes clearer. In MySQL, CHECK is only enforced from version 8.0.16; earlier versions parse it and silently ignore it.

Could you say it?
Full answer2 min read
SQL
CREATE TABLE employees (
  emp_id     INT AUTO_INCREMENT,
  emp_name   VARCHAR(100)  NOT NULL,
  email      VARCHAR(100)  NOT NULL,
  salary     DECIMAL(10,2) NOT NULL,
  hire_date  DATE          NOT NULL,
  dept_id    INT           NULL,                  -- an employee may have no department yet
  manager_id INT           NULL,                  -- the top boss has no manager
  CONSTRAINT pk_employees PRIMARY KEY (emp_id),
  CONSTRAINT uq_employees_email UNIQUE (email),
  CONSTRAINT chk_employees_salary CHECK (salary > 0),
  CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES departments (dept_id)
    ON DELETE SET NULL,
  CONSTRAINT fk_emp_manager FOREIGN KEY (manager_id) REFERENCES employees (emp_id)
);

What each constraint stops

ConstraintRejects
NOT NULLA missing name, email, salary or hire date
PRIMARY KEYDuplicate or NULL IDs
UNIQUE (email)Two employees with one email address
CHECK (salary > 0)A zero or negative salary
FOREIGN KEY (dept_id)A department that doesn't exist
FOREIGN KEY (manager_id)A manager who isn't an employee
DEFAULT (not used here)Supplies a value when an insert leaves the column out

Why DECIMAL for money? DECIMAL(10,2) stores exact values. FLOAT and DOUBLE are binary approximations, so sums of rupees and paise can drift by tiny amounts.

Follow-ups

  • How do you add a constraint later? ALTER TABLE employees ADD CONSTRAINT chk_hire CHECK (hire_date >= '2000-01-01');
  • Can a CHECK constraint refer to another table? No. Cross-table rules need a foreign key, a trigger (question 40) or application code.

03Core SQL

0/5 got it

Q08Not marked yetFresherAsked very oftenConcept

DELETE vs TRUNCATE vs DROP: what does each remove, and which can be rolled back?

Say this first~30 sec to say

DELETE is DML: it removes rows one by one, all of them or only those matching a WHERE, fires DELETE triggers, and can be rolled back inside a transaction. TRUNCATE is DDL: it empties the whole table at once, much faster, with no WHERE, and resets AUTO_INCREMENT. DROP removes the table itself: structure, data, indexes and constraints. In MySQL, TRUNCATE and DROP commit implicitly, so neither can be rolled back.

Could you say it?
Full answer2 min read
DELETETRUNCATEDROP
RemovesSome or all rowsAll rowsThe whole table
WHERE allowedYesNoNo
Command typeDMLDDLDDL
Speed on big tablesSlow: row by row, loggedFastFast
Fires DELETE triggersYesNoNo
Resets AUTO_INCREMENTNoYesThe table is gone
Rollback in MySQLYes, inside a transactionNo: implicit commitNo: implicit commit
Blocked by foreign keys pointing at itOnly for referenced rowsYes, if another table references itYes, if another table references it
SQL
START TRANSACTION;
DELETE FROM orders WHERE status = 'cancelled';   -- removes 103 and 109
ROLLBACK;                                        -- they're back

TRUNCATE TABLE orders;                           -- every order gone, and no undo in MySQL

DROP TABLE IF EXISTS orders_archive;             -- the table itself is removed

Database differences: PostgreSQL and SQL Server let you roll back TRUNCATE, and even DROP, inside a transaction. That's a common follow-up for anyone who says "TRUNCATE can never be rolled back".

Follow-ups

  • Why is TRUNCATE faster? It deallocates the table's storage pages instead of deleting and logging each row.
  • How do you remove all rows but keep the IDs counting up? DELETE FROM table without a WHERE: slower, but AUTO_INCREMENT continues from where it was.
Q09Not marked yetFresherAsked very oftenConcept

WHERE vs HAVING: when does each filter, and why can't WHERE use an aggregate or a SELECT alias?

Say this first~30 sec to say

WHERE filters rows before grouping; HAVING filters groups after GROUP BY has formed them. A query runs in a logical order, FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT, which explains both rules. When WHERE runs, there are no groups yet, so AVG(salary) doesn't exist; and the SELECT list hasn't been evaluated, so its aliases don't exist either. Filter rows in WHERE whenever you can: it shrinks the data before the expensive grouping.

Could you say it?
Full answer2 min read

Departments whose staff hired since 2018 average over ₹1 lakh

SQL
SELECT dept_id, AVG(salary) AS avg_salary
FROM employees
WHERE hire_date >= '2018-01-01'          -- row filter: before grouping
GROUP BY dept_id
HAVING AVG(salary) > 100000;             -- group filter: after grouping
dept_idavg_salary
1120000.000000
2135000.000000

HR (Meera and Sameer) averages 87,500, so HAVING drops it; Suresh's group (no department) averages 70,000. MySQL shows AVG of a DECIMAL(10,2) column with six decimal places.

The logical order a query runs in

StepClauseWhat exists at this point
1FROM, JOINAll the joined rows
2WHERERows filtered one at a time
3GROUP BYGroups
4HAVINGGroups filtered
5SELECTOutput columns and aliases are computed
6ORDER BYAliases can be used here
7LIMIT / OFFSETThe final cut
SQL
SELECT emp_name, salary * 12 AS annual FROM employees WHERE annual > 1500000;
-- ERROR 1054 (42S22): Unknown column 'annual' in 'where clause'

Database differences: MySQL lets you use a SELECT alias in GROUP BY and HAVING, as an extension. PostgreSQL accepts aliases in GROUP BY but not in HAVING, and SQL Server accepts them in neither, so repeating the expression is the portable habit.

Follow-ups

  • Can HAVING be used without GROUP BY? Yes: the whole result is then one group. SELECT COUNT(*) FROM orders HAVING COUNT(*) > 5 returns one row or none.
  • Is the logical order the order the database executes in? Not necessarily. The optimiser can reorder work, but the result must be as if it ran in this order.
Q10Not marked yetFresherAsked very oftenConcept

UNION vs UNION ALL: what is the difference, and which is faster?

Say this first~25 sec to say

Both stack the results of two SELECTs vertically. UNION removes duplicate rows, which costs extra work, usually a sort or hash over the whole result. UNION ALL keeps every row, so it's faster, and it's the right choice whenever duplicates are impossible or wanted. Both need the same number of columns in each SELECT, with compatible types, and the column names come from the first SELECT.

Could you say it?
Full answer1 min read

Every city the company touches, from customers and from department locations

SQL
SELECT city FROM customers
UNION
SELECT location FROM departments
ORDER BY city;
city
Ahmedabad
Bengaluru
Delhi
Hyderabad
Kochi
Mumbai
Pune

UNION returns 7 distinct cities; UNION ALL returns all 10 values, with Delhi three times and Pune twice. The ORDER BY applies to the combined result, and uses the first query's column name.

UNIONUNION ALL
DuplicatesRemovedKept
SpeedSlower: must find duplicatesFaster
Use whenYou need a distinct listRows can't overlap, or counts matter

Related set operators: INTERSECT (rows in both) and EXCEPT (rows in the first but not the second). MySQL only supports them from 8.0.31; before that, use JOIN or NOT EXISTS instead.

Follow-ups

  • UNION vs JOIN? UNION stacks rows from similar queries; JOIN combines columns from related tables side by side.
  • Does UNION ALL guarantee the order of rows? No. Without ORDER BY, no SQL result has a guaranteed order.
Q11Not marked yetFresherAsked oftenQuery

List the five most recently hired employees whose names start with 'S'

Say this first~25 sec to say

Filter with LIKE 'S%', where % matches any run of characters, sort by hire_date descending so the newest come first, and cut with LIMIT 5. Under MySQL's default collation LIKE is case-insensitive, so 's%' matches too; in PostgreSQL it's case-sensitive and ILIKE is the case-insensitive version. SQL Server has no LIMIT: it uses TOP 5 or OFFSET … FETCH.

Could you say it?
Full answer2 min read
SQL
SELECT emp_name, hire_date
FROM employees
WHERE emp_name LIKE 'S%'
ORDER BY hire_date DESC
LIMIT 5;
emp_namehire_date
Sameer Joshi2024-09-01
Suresh Pillai2024-01-15
Sana Khan2022-02-14
Sneha Iyer2018-08-20

Only four employees match, so LIMIT 5 returns four: it's a maximum, not a guarantee.

LIKE patterns

PatternMatches
'S%'Starts with S
'%a'Ends with a
'%kumar%'Contains "kumar" anywhere
'_a%'Second letter is a (_ is exactly one character)
'S____'S followed by exactly four characters

The same in other databases

SQL
-- SQL Server
SELECT TOP 5 emp_name, hire_date FROM employees WHERE emp_name LIKE 'S%' ORDER BY hire_date DESC;

-- Standard SQL (PostgreSQL, Oracle 12c+, SQL Server)
SELECT emp_name, hire_date FROM employees WHERE emp_name LIKE 'S%'
ORDER BY hire_date DESC FETCH FIRST 5 ROWS ONLY;

Follow-ups

  • Can LIKE 'S%' use an index? Yes: a fixed prefix lets the database range-scan an index on emp_name. A leading wildcard, '%S', can't (question 37).
  • What if two people were hired the same day? The order between them is undefined; add a tie-breaker such as ORDER BY hire_date DESC, emp_id.
Q12Not marked yetFresherAsked oftenPredict the output

On employees, what do WHERE manager_id = NULL, WHERE manager_id != 3 and WHERE emp_id NOT IN (SELECT manager_id FROM employees) each return, and why?

Say this first~30 sec to say

= NULL returns no rows: any comparison with NULL gives UNKNOWN, which WHERE treats as false; use IS NULL. != 3 returns 8 rows, not 9: it skips Arjun, whose manager is 3, and also Asha, whose manager_id is NULL, because NULL != 3 is UNKNOWN too. The NOT IN query returns no rows at all: the subquery's list contains a NULL, and x NOT IN (…, NULL) can never be true. NOT EXISTS avoids that trap.

Could you say it?
Full answer2 min read
SQL
SELECT emp_name FROM employees WHERE manager_id = NULL;
-- (no rows)

SELECT emp_name FROM employees WHERE manager_id IS NULL;
-- Asha Rao

SELECT COUNT(*) FROM employees WHERE manager_id != 3;
-- 8: Arjun (manager 3) and Asha (manager NULL) are both excluded

SELECT emp_name FROM employees WHERE emp_id NOT IN (SELECT manager_id FROM employees);
-- (no rows): the list includes Asha's NULL

SELECT e.emp_name
FROM employees e
WHERE NOT EXISTS (SELECT 1 FROM employees r WHERE r.manager_id = e.emp_id);
-- Arjun Mehta, Vikram Singh, Sana Khan, Suresh Pillai, Sameer Joshi: the people who manage nobody

Three-valued logic in one table

ExpressionResult
NULL = NULLUNKNOWN
NULL != 3UNKNOWN
5 NOT IN (1, 2, NULL)UNKNOWN, because 5 != NULL is unknown
NULL IS NULLTRUE
UNKNOWN AND FALSEFALSE
UNKNOWN OR TRUETRUE

WHERE keeps only rows where the condition is TRUE, so UNKNOWN rows silently disappear.

Including the NULLs on purpose

SQL
SELECT COUNT(*) FROM employees WHERE manager_id != 3 OR manager_id IS NULL;   -- 9

NULL vs 0 vs an empty string: NULL means "unknown or not applicable"; 0 and '' are real values. Replace NULLs for display with COALESCE(manager_id, 0) (standard) or MySQL's IFNULL. Beware: ISNULL(x) tests for NULL in MySQL (returning 1 or 0) but replaces NULL in SQL Server.

Follow-ups

  • Is NOT IN ever safe? Yes, when the subquery's column can't contain NULL, for example a NOT NULL column, or with WHERE manager_id IS NOT NULL added inside the subquery.
  • How do aggregates treat NULL? They skip it: COUNT(manager_id) is 9, not 10 (question 19).

04Joins

0/5 got it

Q13Not marked yetFresherAsked very oftenConcept

What does each join type return: INNER, LEFT, RIGHT, FULL OUTER and CROSS?

Say this first~30 sec to say

INNER JOIN returns only rows that match in both tables. LEFT JOIN returns every row from the left table, with NULLs on the right where nothing matches; RIGHT JOIN is the mirror image. FULL OUTER JOIN returns every row from both sides, matched where possible and NULL-padded where not. CROSS JOIN pairs every row with every row: m × n rows. MySQL has no FULL OUTER JOIN; you build it from a LEFT JOIN and a RIGHT JOIN.

Could you say it?
Full answer2 min read

Employees and departments, joined on dept_id

JoinRowsWhat's included
INNER JOIN9Employees with a department; Suresh and Finance drop out
employees LEFT JOIN departments10Every employee; Suresh gets NULL for the department
employees RIGHT JOIN departments10Every department; Finance appears with NULL for the employee
FULL OUTER JOIN11Both: Suresh with no department, and Finance with no employee
CROSS JOIN4010 employees × 4 departments
SQL
SELECT e.emp_name, d.dept_name
FROM employees e
LEFT JOIN departments d ON d.dept_id = e.dept_id;     -- every employee, Suresh with dept_name NULL

FULL OUTER JOIN in MySQL

SQL
SELECT e.emp_name, d.dept_name
FROM employees e LEFT JOIN departments d ON d.dept_id = e.dept_id
UNION ALL
SELECT e.emp_name, d.dept_name
FROM employees e RIGHT JOIN departments d ON d.dept_id = e.dept_id
WHERE e.emp_id IS NULL;                                 -- only the right-side rows with no match

That returns the same 11 rows a real FULL OUTER JOIN would. Many tutorials use UNION instead of UNION ALL without the WHERE, which works only when the rows contain no duplicates (question 16 shows how it breaks).

INNER:  [ A ∩ B ]        LEFT:  [ A ] + matches        RIGHT: matches + [ B ]
FULL:   [ A ∪ B ]        CROSS: every A paired with every B

Follow-ups

  • What is a self join? A table joined to itself, with two aliases, such as employees and their managers (question 14).
  • What is a NATURAL JOIN? A join on every column with the same name in both tables. Avoid it: adding a column later can silently change the join.
  • Does the order of tables matter? For INNER and CROSS joins, no; for LEFT and RIGHT, yes: it decides which side keeps all its rows.
Q14Not marked yetFresherAsked very oftenQuery

List employees who are paid more than their own manager, showing both names and salaries

Say this first~25 sec to say

Use a self join: join employees to itself, one copy as the employee (e) and one as the manager (m), on e.manager_id = m.emp_id, then keep rows where e.salary > m.salary. An INNER JOIN is right here, because the top boss has no manager to compare with. The usual warm-up, listing everyone with their manager's name, needs a LEFT self join so the top boss isn't dropped.

Could you say it?
Full answer2 min read
SQL
SELECT e.emp_name, e.salary,
       m.emp_name AS manager_name, m.salary AS manager_salary
FROM employees e
JOIN employees m ON e.manager_id = m.emp_id     -- m is the manager's row
WHERE e.salary > m.salary;
emp_namesalarymanager_namemanager_salary
Sana Khan160000.00Sneha Iyer150000.00
Sameer Joshi90000.00Meera Nair85000.00

Arjun earns exactly what his manager Priya earns, so the strict > leaves him out.

The warm-up: everyone with their manager

SQL
SELECT e.emp_name, m.emp_name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id
ORDER BY e.emp_id;
emp_namemanager
Asha RaoNULL
Ravi KumarAsha Rao
Priya ShahRavi Kumar
Arjun MehtaPriya Shah
Sneha IyerAsha Rao
……

With an INNER JOIN, Asha would vanish, because she has no manager row to match.

What is a self join? Not a special keyword: an ordinary join where both sides are the same table, distinguished by aliases. It's how you query any hierarchy stored in one table: employees and managers, categories and parent categories, replies and the comments they reply to.

Follow-ups

  • How would you count each manager's direct reports? SELECT m.emp_name, COUNT(*) FROM employees e JOIN employees m ON e.manager_id = m.emp_id GROUP BY m.emp_id, m.emp_name;
  • How do you get everyone below a manager, at every level? A recursive CTE (question 26).
Q15Not marked yetFresherAsked very oftenQuery

List the customers who signed up but have never ordered

Say this first~25 sec to say

This is an anti-join: rows in one table with no match in another. Either LEFT JOIN orders and keep the rows where the order side IS NULL, or use NOT EXISTS with a correlated subquery. Both return Fatima and Deepak. Avoid NOT IN with a subquery on a nullable column: if that column contains a single NULL, NOT IN returns nothing at all (question 12).

Could you say it?
Full answer2 min read
SQL
-- LEFT JOIN ... IS NULL
SELECT c.customer_id, c.customer_name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;

-- NOT EXISTS: often the clearest, and NULL-safe
SELECT c.customer_id, c.customer_name
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
customer_idcustomer_name
5Fatima Sheikh
6Deepak Rao

Why check o.order_id, a primary key, for NULL? A primary key is never NULL in a real row, so a NULL there can only mean "no matching row". Checking a nullable column such as o.emp_id would also catch real orders that happen to have no sales rep.

ApproachNULL-safeNotes
LEFT JOIN … WHERE right.pk IS NULLYesFamiliar; shows the anti-join clearly
NOT EXISTS (correlated subquery)YesStops at the first match; optimisers handle it well
NOT IN (subquery)NoWrong as soon as the subquery returns a NULL

Follow-ups

  • Customers whose last order was over 90 days ago? Group orders by customer, take MAX(order_date), and keep those before CURDATE() - INTERVAL 90 DAY, plus the never-ordered ones from this query.
  • Which is faster, NOT EXISTS or LEFT JOIN … IS NULL? Usually the same plan in modern optimisers; check with EXPLAIN rather than guessing.
Q16Not marked yetFresherAsked oftenPredict the output

Two one-column tables hold (1, 1, 2, NULL) and (1, 1, 3, NULL). How many rows do INNER, LEFT, RIGHT, FULL OUTER and CROSS JOIN each return when joined on that column?

Say this first~30 sec to say

INNER: 4. Each of the two 1s on the left matches both 1s on the right, 2 × 2 = 4, and NULL never equals NULL, so it doesn't match. LEFT: 6: the 4 matches plus the unmatched 2 and NULL from the left. RIGHT: 6: the 4 matches plus 3 and NULL from the right. FULL OUTER: 8: the 4 matches plus both sides' unmatched rows. CROSS: 16: 4 × 4, every pairing. Duplicates multiply matches; NULLs never join.

Could you say it?
Full answer2 min read
SQL
CREATE TABLE a (v INT);
CREATE TABLE b (v INT);
INSERT INTO a VALUES (1), (1), (2), (NULL);
INSERT INTO b VALUES (1), (1), (3), (NULL);

SELECT COUNT(*) FROM a JOIN b ON a.v = b.v;          -- 4
SELECT COUNT(*) FROM a LEFT JOIN b ON a.v = b.v;     -- 6
SELECT COUNT(*) FROM a RIGHT JOIN b ON a.v = b.v;    -- 6
SELECT COUNT(*) FROM a CROSS JOIN b;                 -- 16
JoinRowsMade of
INNER4(1,1) × 4
LEFT64 matches + (2, NULL) + (NULL, NULL)
RIGHT64 matches + (NULL, 3) + (NULL, NULL)
FULL OUTER84 matches + 2 left-only + 2 right-only
CROSS164 × 4

The MySQL FULL JOIN trap. The common workaround, LEFT JOIN … UNION … RIGHT JOIN, returns only 4 rows here, not 8. UNION removes duplicates, so the four identical (1, 1) rows collapse into one, and the two (NULL, NULL) rows into one. The safe version uses UNION ALL and keeps only the unmatched rows from the second half (question 13).

Follow-ups

  • How would you join NULLs to NULLs on purpose? MySQL's null-safe equality operator <=> treats two NULLs as equal; the standard spelling is IS NOT DISTINCT FROM.
  • Why does this matter in practice? Joining on a non-unique column multiplies rows, which silently inflates sums and counts. Check row counts before and after a join.
Q17Not marked yet1–3 yrsAsked sometimesPredict the output

In customers LEFT JOIN orders, what changes when the condition status = 'delivered' moves from the ON clause to the WHERE clause?

Say this first~30 sec to say

In the ON clause, the condition only decides which orders match: every customer still appears, and customers with no delivered orders get NULLs. In the WHERE clause, it filters after the join: the NULL-extended rows have status NULL, which fails the test, so they're dropped, and the LEFT JOIN behaves like an inner join. Here, ON gives 11 rows covering all 6 customers; WHERE gives 8 rows covering only the 3 with delivered orders.

Could you say it?
Full answer2 min read
SQL
-- Condition in ON: still a real LEFT JOIN
SELECT c.customer_name, o.order_id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id AND o.status = 'delivered';
-- 11 rows: Rahul V, Fatima and Deepak appear with order_id NULL

-- Condition in WHERE: the LEFT JOIN collapses into an inner join
SELECT c.customer_name, o.order_id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.status = 'delivered';
-- 8 rows: only Kiran, Lakshmi and Rahul Verma
CustomerCondition in ONCondition in WHERE
Kiran Patel101, 102101, 102
Lakshmi Menon107, 108, 111107, 108, 111
Rahul Verma104, 105, 110104, 105, 110
Rahul V (only a placed order)NULL(gone)
Fatima Sheikh (no orders)NULL(gone)
Deepak Rao (no orders)NULL(gone)

The rule: for an outer join, conditions on the optional (right-hand) table belong in ON if you want to keep the unmatched rows. Conditions on the preserved (left) table, such as c.city = 'Delhi', belong in WHERE.

Follow-ups

  • Does it matter for an INNER JOIN? No: for inner joins, ON and WHERE conditions give the same result.
  • How would you count delivered orders per customer, zeros included? Put the status test in ON and count o.order_id: COUNT(o.order_id) gives 0 for customers with no match.

05Aggregation and grouping

0/6 got it

Q18Not marked yetFresherAsked very oftenConcept

What does GROUP BY do, and why does selecting a column that is neither grouped nor aggregated fail?

Say this first~30 sec to say

GROUP BY collapses rows that share the same values in the grouping columns into one output row per group, and aggregate functions such as COUNT, SUM, AVG, MIN and MAX summarise each group. Every column in the SELECT list must therefore be either grouped or aggregated: a plain column like emp_name has many values in a group, so the database can't know which one to show. MySQL 8 rejects it under ONLY_FULL_GROUP_BY, which is on by default.

Could you say it?
Full answer2 min read
SQL
SELECT dept_id, COUNT(*) AS staff, MAX(salary) AS top_salary
FROM employees
GROUP BY dept_id;
dept_idstafftop_salary
NULL170000.00
14250000.00
23160000.00
3290000.00

NULL forms its own group: Suresh, who has no department.

The error

SQL
SELECT dept_id, emp_name, MAX(salary)
FROM employees
GROUP BY dept_id;
-- ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains
-- nonaggregated column 'company.employees.emp_name' which is not functionally dependent on
-- columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

Older MySQL versions (before 5.7) accepted this and returned an arbitrary emp_name from each group, often not the person with the top salary, which is why some old tutorials show it "working".

The fix depends on what you meant: to show the top earner's name, join back to the maximum or use a window function (questions 21 and 29); to show any one name, say so explicitly with ANY_VALUE(emp_name).

Follow-ups

  • Can you group by several columns? Yes: GROUP BY dept_id, YEAR(hire_date) gives one row per department per hiring year.
  • What does "functionally dependent" mean in that error? If you group by a primary key, every other column of that row has one value per group, so MySQL allows selecting them.
Q19Not marked yetFresherAsked oftenPredict the output

On employees, what do COUNT(*), COUNT(manager_id) and COUNT(DISTINCT dept_id) return, and why do they differ?

Say this first~20 sec to say

COUNT(*) returns 10: it counts rows, whatever they contain. COUNT(manager_id) returns 9: counting a column skips NULLs, and Asha has no manager. COUNT(DISTINCT dept_id) returns 3: it skips the NULL and counts each department only once. The same NULL rule applies to SUM, AVG, MIN and MAX: they ignore NULLs instead of treating them as 0.

Could you say it?
Full answer1 min read
SQL
SELECT COUNT(*), COUNT(manager_id), COUNT(DISTINCT dept_id)
FROM employees;
COUNT(*)COUNT(manager_id)COUNT(DISTINCT dept_id)
1093
ExpressionCountsHere
COUNT(*)Rows10
COUNT(1)Rows: identical to COUNT(*), not faster10
COUNT(column)Non-NULL values9 for manager_id
COUNT(DISTINCT column)Distinct non-NULL values3 for dept_id

AVG skips NULLs too, which changes the answer

If a bonus column held 100, 200 and NULL, AVG(bonus) is 150 (300 ÷ 2), not 100 (300 ÷ 3). If NULL should count as zero, say so: AVG(COALESCE(bonus, 0)).

Follow-ups

  • Why does COUNT(*) give 1 for an empty department in a LEFT JOIN? Because the NULL-extended row is still a row; count a column from the right-hand table instead (question 22).
  • What does SUM return for a group with only NULLs, or no rows? NULL, not 0; wrap it in COALESCE(SUM(x), 0) when you need a number.
Q20Not marked yetFresherAsked very oftenQuery

Find email addresses used by more than one customer, with how many times each appears

Say this first~25 sec to say

Group by email and keep only the groups with more than one row: GROUP BY email HAVING COUNT(*) > 1. The HAVING is essential, because the condition is on a count, which only exists after grouping. Here it returns [email protected], used twice. To see the full duplicate rows, join back to this result, or use a window function.

Could you say it?
Full answer1 min read
SQL
SELECT email, COUNT(*) AS times
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;

Showing the duplicate customers themselves

SQL
SELECT c.customer_id, c.customer_name, c.email, c.signup_date
FROM customers c
JOIN (
  SELECT email FROM customers GROUP BY email HAVING COUNT(*) > 1
) dup ON dup.email = c.email
ORDER BY c.email, c.signup_date;
customer_idcustomer_nameemailsignup_date
3Rahul Verma[email protected]2023-03-20
4Rahul V[email protected]2024-06-02

Watch out for: case and spaces. [email protected] and [email protected] are different strings; group by LOWER(TRIM(email)) to catch them. (MySQL 8's default collation already ignores case, but it treats trailing spaces as significant, so normalising is the portable habit.)

Follow-ups

  • How would you delete the duplicates? Keep one row per email and delete the rest, as question 27 does for orders. For customers, merge their orders first.
  • Duplicates across several columns? Group by all of them: GROUP BY customer_id, order_date, amount HAVING COUNT(*) > 1.
Q21Not marked yetFresherAsked very oftenQuery

Show each department's name next to the highest salary paid in it

Say this first~25 sec to say

Join employees to departments to get the names, GROUP BY the department, and take MAX(salary) in each group. Group by the department's ID as well as its name, so two departments that happen to share a name can't be merged. An inner join leaves out departments with no employees and employees with no department; switch to a LEFT JOIN from departments if empty departments should appear.

Could you say it?
Full answer2 min read
SQL
SELECT d.dept_name, MAX(e.salary) AS highest_salary
FROM employees e
JOIN departments d ON d.dept_id = e.dept_id
GROUP BY d.dept_id, d.dept_name
ORDER BY highest_salary DESC;
dept_namehighest_salary
Engineering250000.00
Sales160000.00
HR90000.00

The follow-up: who earns it?

Adding e.emp_name to the query above fails (question 18). Join back to the per-department maximum instead:

SQL
SELECT d.dept_name, e.emp_name, e.salary
FROM employees e
JOIN departments d ON d.dept_id = e.dept_id
JOIN (
  SELECT dept_id, MAX(salary) AS max_salary
  FROM employees
  GROUP BY dept_id
) top ON top.dept_id = e.dept_id AND top.max_salary = e.salary;
dept_nameemp_namesalary
EngineeringAsha Rao250000.00
SalesSana Khan160000.00
HRSameer Joshi90000.00

If two people tie for the top salary, both appear, which is usually what's wanted. The window-function version is in question 29.

Follow-ups

  • Highest salary per department, but only departments with at least three people? Add HAVING COUNT(*) >= 3.
  • And the lowest, average and total, too? Add MIN(e.salary), ROUND(AVG(e.salary), 2) and SUM(e.salary) to the same SELECT.
Q22Not marked yetFresherAsked oftenQuery

Show every department with its number of employees, including departments that have none

Say this first~20 sec to say

Start from departments and LEFT JOIN employees, so departments without staff still appear, then GROUP BY the department and count e.emp_id, not *. COUNT(e.emp_id) skips the NULL produced for an empty department and returns 0, whereas COUNT(*) counts that NULL-padded row and wrongly reports 1. Here Finance has no employees and must show 0.

Could you say it?
Full answer1 min read
SQL
SELECT d.dept_name, COUNT(e.emp_id) AS employees
FROM departments d
LEFT JOIN employees e ON e.dept_id = d.dept_id
GROUP BY d.dept_id, d.dept_name
ORDER BY employees DESC;
dept_nameemployees
Engineering4
Sales3
HR2
Finance0

With COUNT(*), Finance would show 1: the LEFT JOIN creates one row for it, with every employee column NULL, and COUNT(*) counts rows.

The common variant: departments with more than three people

SQL
SELECT d.dept_name, COUNT(e.emp_id) AS employees
FROM departments d
LEFT JOIN employees e ON e.dept_id = d.dept_id
GROUP BY d.dept_id, d.dept_name
HAVING COUNT(e.emp_id) > 3;          -- Engineering, with 4

Follow-ups

  • What about the employee with no department? This query starts from departments, so Suresh doesn't appear. Counting "unassigned" staff needs a separate query, or a FULL OUTER JOIN.
  • How is this the same as the pandas question? It's the SQL form of a left merge followed by counting non-null values (ML kit, question 28).
Q23Not marked yet1–3 yrsAsked sometimesQuery

For each customer, show the number of delivered and cancelled orders as two columns in one row

Say this first~25 sec to say

Use conditional aggregation: group by customer, and inside the aggregate, count only the rows that match each status. SUM(CASE WHEN o.status = 'delivered' THEN 1 ELSE 0 END) counts delivered orders, and the same with 'cancelled' gives the second column. LEFT JOIN from customers so those without orders show zeros. This pivots row values into columns in a single pass over the data.

Could you say it?
Full answer2 min read
SQL
SELECT c.customer_name,
       SUM(CASE WHEN o.status = 'delivered' THEN 1 ELSE 0 END) AS delivered,
       SUM(CASE WHEN o.status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.customer_name
ORDER BY c.customer_id;
customer_namedeliveredcancelled
Kiran Patel21
Lakshmi Menon31
Rahul Verma30
Rahul V00
Fatima Sheikh00
Deepak Rao00

For customers without orders, o.status is NULL, so every CASE falls through to ELSE 0.

What CASE does, the usual fresher warm-up: it returns a value based on conditions, like if-else inside a query:

SQL
SELECT emp_name, salary,
       CASE
         WHEN salary >= 150000 THEN 'High'
         WHEN salary >= 90000 THEN 'Medium'
         ELSE 'Low'
       END AS band
FROM employees;

The conditions are checked top to bottom and the first match wins, so order them from the most specific.

A MySQL shortcut that doesn't travel: in MySQL, SUM(o.status = 'delivered') works, because a comparison evaluates to 1 or 0. PostgreSQL and SQL Server reject it. COUNT(CASE WHEN o.status = 'delivered' THEN 1 END) is another portable form, since COUNT skips the NULLs from unmatched CASEs.

Follow-ups

  • How would you add each customer's delivered revenue? SUM(CASE WHEN o.status = 'delivered' THEN o.amount ELSE 0 END).
  • What if the statuses aren't known in advance? Static SQL needs fixed columns. Dynamic pivots need prepared statements, or are better done in the reporting tool.

06Subqueries and CTEs

0/4 got it

Q24Not marked yetFresherAsked very oftenQuery

Find the second-highest distinct salary in the company

Say this first~30 sec to say

Two classic answers. Take the maximum of the salaries below the maximum: SELECT MAX(salary) FROM employees WHERE salary < (SELECT MAX(salary) FROM employees). Or list the distinct salaries in descending order and skip the first: ORDER BY salary DESC LIMIT 1 OFFSET 1. Both return 180,000 here. Say how you handle ties and edge cases: with only one distinct salary, the first returns NULL and the second returns no rows. For the Nth highest, DENSE_RANK() generalises best.

Could you say it?
Full answer2 min read
SQL
-- 1. The max below the max
SELECT MAX(salary) AS second_highest
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);

-- 2. Distinct salaries, skip the first
SELECT DISTINCT salary AS second_highest
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 1;
second_highest
180000.00

Why DISTINCT matters: without it, a tie for the top salary would make the "second" row repeat the highest value. That's also why the first method works: salary < MAX naturally skips every copy of the top salary.

The Nth highest: DENSE_RANK

SQL
SELECT DISTINCT salary
FROM (
  SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS salary_rank
  FROM employees
) ranked
WHERE salary_rank = 5;          -- 120000.00: shared by Priya and Arjun

DENSE_RANK gives tied salaries the same rank and doesn't skip numbers afterwards, so "the 5th highest salary" means the 5th distinct value (question 28).

MethodEdge case: only one salaryGeneralises to N?
MAX(...) WHERE salary < MAXReturns NULLAwkwardly: nested N levels
DISTINCT … LIMIT 1 OFFSET 1No rowsYes: OFFSET N-1
DENSE_RANK()No rowsYes, and per group with PARTITION BY

Follow-ups

  • The second-highest salary in each department? DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC), then filter rank 2 (question 29).
  • Why not LIMIT 1, 1? It means the same as LIMIT 1 OFFSET 1 in MySQL, but the explicit OFFSET form is clearer and more portable.
Q25Not marked yetFresherAsked oftenQuery

List employees who earn more than the average salary of their own department

Say this first~30 sec to say

Use a correlated subquery: for each employee row, a subquery computes the average salary of that employee's department, WHERE e2.dept_id = e.dept_id, and the outer query keeps the row if the salary is higher. It's "correlated" because the inner query refers to the outer row, so it conceptually runs once per row. The equivalent join computes all department averages once, in a derived table, then joins: often faster on big tables.

Could you say it?
Full answer2 min read
SQL
SELECT e.emp_name, e.dept_id, e.salary
FROM employees e
WHERE e.salary > (
  SELECT AVG(e2.salary)
  FROM employees e2
  WHERE e2.dept_id = e.dept_id          -- refers to the outer row: correlated
)
ORDER BY e.dept_id, e.salary DESC;
emp_namedept_idsalary
Asha Rao1250000.00
Ravi Kumar1180000.00
Sana Khan2160000.00
Sneha Iyer2150000.00
Sameer Joshi390000.00

The department averages are 167,500 (Engineering), 135,000 (Sales) and 87,500 (HR). Suresh has no department: e2.dept_id = NULL matches nothing, the average is NULL, and the comparison drops him.

The same with a join to the averages

SQL
SELECT e.emp_name, e.dept_id, e.salary, ROUND(a.avg_salary) AS dept_avg
FROM employees e
JOIN (
  SELECT dept_id, AVG(salary) AS avg_salary
  FROM employees
  GROUP BY dept_id
) a ON a.dept_id = e.dept_id
WHERE e.salary > a.avg_salary;

The warm-up: above the company average, a plain (non-correlated) subquery that runs once:

SQL
SELECT emp_name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);   -- average 132000.00: Asha, Ravi, Sana, Sneha

Follow-ups

  • What's a correlated subquery, in one line? A subquery that depends on the current row of the outer query, so its result changes row by row.
  • And the window-function version? AVG(salary) OVER (PARTITION BY dept_id) puts each department's average on every row without grouping, then an outer query filters.
Q26Not marked yet1–3 yrsAsked oftenConcept

What is a CTE, and when would you use one instead of a subquery or a view?

Say this first~30 sec to say

A CTE (common table expression) is a named, temporary result defined with WITH name AS (…) at the start of a statement and usable only inside it. It splits a multi-step query into readable, named steps, and can be referenced more than once. Use one where nested subqueries get hard to read; use a view when a query should be saved and reused. CTEs need MySQL 8.0+, and WITH RECURSIVE walks hierarchies.

Could you say it?
Full answer3 min read
SQL
WITH dept_stats AS (
  SELECT dept_id, AVG(salary) AS avg_salary, COUNT(*) AS staff
  FROM employees
  WHERE dept_id IS NOT NULL
  GROUP BY dept_id
),
big_depts AS (
  SELECT dept_id, avg_salary FROM dept_stats WHERE staff >= 3
)
SELECT d.dept_name, ROUND(b.avg_salary) AS avg_salary
FROM big_depts b
JOIN departments d ON d.dept_id = b.dept_id;
dept_nameavg_salary
Engineering167500
Sales135000

Recursive CTEs: everyone in the org chart, with their level

SQL
WITH RECURSIVE org AS (
  SELECT emp_id, emp_name, manager_id, 1 AS level
  FROM employees
  WHERE manager_id IS NULL                 -- the anchor: the top boss
  UNION ALL
  SELECT e.emp_id, e.emp_name, e.manager_id, o.level + 1
  FROM employees e
  JOIN org o ON e.manager_id = o.emp_id    -- the recursive step: reports of the previous level
)
SELECT emp_name, level FROM org ORDER BY level, emp_id;
emp_namelevel
Asha Rao1
Ravi Kumar2
Sneha Iyer2
Meera Nair2
Suresh Pillai2
Priya Shah3
Vikram Singh3
Sana Khan3
Sameer Joshi3
Arjun Mehta4

Change the anchor to WHERE emp_id = 5 and the query returns Sneha and everyone under her: Vikram and Sana.

CTESubqueryView
LivesFor one statementInline, for one statementIn the database, until dropped
NamedYesOnly if a derived table gets an aliasYes
Reusable within the queryYes, by nameNo: repeat itYes
RecursiveYes, with WITH RECURSIVENoNo
Best forReadable multi-step queries, hierarchiesShort, one-off filtersReused logic, simplifying access, security

Follow-ups

  • Is a CTE faster than a subquery? Usually the same: MySQL can merge simple CTEs into the main query, much like a derived table. Choose it for readability.
  • What stops a recursive CTE looping forever? The recursion ends when a step returns no rows. MySQL also stops at cte_max_recursion_depth (1,000 by default), which catches cycles in bad data.
Q27Not marked yet1–3 yrsAsked oftenQuery

Some orders were saved twice (same customer, date and amount). Delete the extra copies, keeping the lowest order_id

Say this first~30 sec to say

Two good answers. A self-join DELETE: join orders to itself on customer, date and amount where the other copy's order_id is lower, and delete those rows, since each has an earlier twin. Or number each duplicate group with ROW_NUMBER() partitioned by the duplicate columns, ordered by order_id, and delete every row numbered above 1. In MySQL, a DELETE whose subquery reads the same table directly fails with error 1093; the self-join form avoids it.

Could you say it?
Full answer2 min read

Before: orders 104 and 105 are one duplicate pair; 107, 108 and 111 are a triple.

SQL
-- 1. Self-join DELETE (MySQL)
DELETE later
FROM orders later
JOIN orders earlier
  ON  earlier.customer_id = later.customer_id
  AND earlier.order_date  = later.order_date
  AND earlier.amount      = later.amount
  AND earlier.order_id    < later.order_id;     -- "later" has an earlier twin, so it goes

SELECT order_id FROM orders ORDER BY order_id;
-- 101, 102, 103, 104, 106, 107, 109, 110: 105, 108 and 111 are gone

With ROW_NUMBER

SQL
DELETE FROM orders
WHERE order_id IN (
  SELECT order_id FROM (
    SELECT order_id,
           ROW_NUMBER() OVER (PARTITION BY customer_id, order_date, amount ORDER BY order_id) AS copy_no
    FROM orders
  ) numbered                              -- the extra derived table is what keeps MySQL happy
  WHERE copy_no > 1
);

Without the outer SELECT … FROM (…) numbered wrapper, MySQL refuses with ERROR 1093 (HY000): You can't specify target table 'orders' for update in FROM clause. The wrapper makes MySQL materialise the subquery's result first.

Before deleting anything in a real database

  1. Run the SELECT version first and check exactly which rows would go.
  2. Do it inside a transaction, so a mistake can be rolled back.
  3. Check for child rows: delivery records or payments pointing at an order you're about to delete.
  4. Prevent it recurring: a unique constraint, or an idempotency key on the API that creates orders (Backend kit, question 8).

Follow-ups

  • What if "duplicate" should ignore the amount? Change the columns in the join or the PARTITION BY; that list is the definition of a duplicate, so agree it first.
  • How would you keep the latest copy instead? Flip the comparison to earlier.order_id > later.order_id, or order the ROW_NUMBER by order_id DESC.

07Window functions

0/3 got it

Q28Not marked yetFresherAsked oftenConcept

ROW_NUMBER vs RANK vs DENSE_RANK: how does each number tied salaries?

Say this first~30 sec to say

All three number rows in a given order, and unlike GROUP BY, window functions keep every row. ROW_NUMBER gives every row a unique number, so ties are split arbitrarily: 5, 6. RANK gives ties the same rank and then skips: 5, 5, 7. DENSE_RANK gives ties the same rank without a gap: 5, 5, 6. Use ROW_NUMBER to pick exactly one row per group, DENSE_RANK for "Nth highest value" questions, and RANK for competition-style standings. They need MySQL 8.0 or later.

Could you say it?
Full answer2 min read
SQL
SELECT emp_name, salary,
       ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
       RANK()       OVER (ORDER BY salary DESC) AS rnk,
       DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk
FROM employees;
emp_namesalaryrow_numrnkdense_rnk
Asha Rao250000.00111
Ravi Kumar180000.00222
Sana Khan160000.00333
Sneha Iyer150000.00444
Priya Shah120000.00555
Arjun Mehta120000.00655
Vikram Singh95000.00776
Sameer Joshi90000.00887
Meera Nair85000.00998
Suresh Pillai70000.0010109

Priya and Arjun tie at 120,000. ROW_NUMBER splits them 5 and 6, in an order you shouldn't rely on unless you add a tie-breaker such as ORDER BY salary DESC, emp_id. RANK gives both 5 and then jumps to 7; DENSE_RANK gives both 5 and continues at 6.

Window functions vs GROUP BY

GROUP BYWindow function (OVER)
Output rowsOne per groupOne per input row
Can show detail columns tooNoYes
ExampleTotal salary per departmentEach employee next to their department's total
SQL
SELECT emp_name, dept_id, salary,
       SUM(salary) OVER (PARTITION BY dept_id) AS dept_total    -- every row keeps its detail
FROM employees;

Follow-ups

  • What does PARTITION BY do? It restarts the numbering, or the calculation, for each group (question 29).
  • Other window functions worth naming? LAG and LEAD (previous and next row), SUM and AVG over windows (running totals, question 30), NTILE (split rows into buckets), FIRST_VALUE.
Q29Not marked yet1–3 yrsAsked oftenQuery

List the three best-paid employees in each department, letting tied salaries share a place

Say this first~25 sec to say

Rank employees within each department with DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC), so ties share a place, then keep ranks 1 to 3. The filter must go in an outer query or CTE: window functions are computed after WHERE, so the rank doesn't exist yet when WHERE runs. In Engineering, Priya and Arjun tie for third, so that department returns four people.

Could you say it?
Full answer2 min read
SQL
WITH ranked AS (
  SELECT d.dept_name, e.emp_name, e.salary,
         DENSE_RANK() OVER (PARTITION BY e.dept_id ORDER BY e.salary DESC) AS pay_rank
  FROM employees e
  JOIN departments d ON d.dept_id = e.dept_id
)
SELECT dept_name, emp_name, salary, pay_rank
FROM ranked
WHERE pay_rank <= 3                      -- allowed here: the rank exists in the CTE's output
ORDER BY dept_name, pay_rank, emp_name;
dept_nameemp_namesalarypay_rank
EngineeringAsha Rao250000.001
EngineeringRavi Kumar180000.002
EngineeringArjun Mehta120000.003
EngineeringPriya Shah120000.003
HRSameer Joshi90000.001
HRMeera Nair85000.002
SalesSana Khan160000.001
SalesSneha Iyer150000.002
SalesVikram Singh95000.003

HR has only two people, so it returns two. The inner join leaves out Suresh; without it, PARTITION BY dept_id would put him in a NULL "department" of his own, ranked 1.

Choosing the ranking function changes the answer

FunctionEngineering returnsWhy
DENSE_RANK4 peopleTies share 3rd place
RANK4 peopleSame here; a tie at 2nd would skip 3rd
ROW_NUMBERExactly 3 peopleOne of Priya and Arjun is cut, arbitrarily

The common variant, "the top earner in each department": the same query with WHERE pay_rank = 1.

Follow-ups

  • Why not WHERE DENSE_RANK() OVER (...) <= 3 directly? WHERE runs before window functions are computed, so MySQL rejects window functions in WHERE. Some databases add QUALIFY for exactly this; MySQL doesn't have it.
  • What if the interviewer wants exactly three rows per department? Use ROW_NUMBER with a deterministic tie-breaker, such as ORDER BY salary DESC, hire_date.
Q30Not marked yet1–3 yrsAsked oftenQuery

Show each month's total order value with a running total alongside it

Say this first~25 sec to say

First group by month to get each month's total, then add a window function over those grouped rows: SUM(SUM(amount)) OVER (ORDER BY month). The inner SUM is the month's total; the outer SUM … OVER accumulates the totals in month order. In MySQL, DATE_FORMAT(order_date, '%Y-%m') makes the month label; PostgreSQL uses DATE_TRUNC or TO_CHAR. Decide with the interviewer whether cancelled orders count; here they're excluded.

Could you say it?
Full answer2 min read
SQL
SELECT DATE_FORMAT(order_date, '%Y-%m') AS month,
       SUM(amount) AS month_total,
       SUM(SUM(amount)) OVER (ORDER BY DATE_FORMAT(order_date, '%Y-%m')) AS running_total
FROM orders
WHERE status <> 'cancelled'
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month;
monthmonth_totalrunning_total
2025-011650.001650.00
2025-021798.003448.00
2025-037000.0010448.00
2025-045600.0016048.00

(These totals still include the duplicate orders from question 27; delete those first for true revenue.)

The same in two clear steps, with a CTE

SQL
WITH monthly AS (
  SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, SUM(amount) AS month_total
  FROM orders
  WHERE status <> 'cancelled'
  GROUP BY month
)
SELECT month, month_total,
       SUM(month_total) OVER (ORDER BY month) AS running_total,
       month_total - LAG(month_total) OVER (ORDER BY month) AS change_vs_last_month
FROM monthly
ORDER BY month;

LAG returns the previous row's value, so change_vs_last_month is NULL for January, then 148.00, 5202.00 and -1400.00.

Frames, briefly: with ORDER BY and no frame, a running SUM uses RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. A moving three-month average spells the frame out: AVG(month_total) OVER (ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW).

Follow-ups

  • What if a month has no orders at all? It won't appear. Generate the full list of months, with a calendar table or a recursive CTE, and LEFT JOIN the totals to it.
  • A running total per customer? Add PARTITION BY customer_id to the window.

08Normalization

0/2 got it

Q31Not marked yetFresherAsked very oftenConcept

What is normalization, and which insert, update and delete anomalies does it prevent?

Say this first~25 sec to say

Normalization organises tables so each fact is stored once, splitting data into related tables linked by keys. Copying a fact into many rows causes three anomalies. Update: change one copy but not the others, and the data contradicts itself. Insert: you can't record a fact without an unrelated one. Delete: removing one fact destroys another. Normal forms are the rules for removing that redundancy step by step.

Could you say it?
Full answer2 min read

One wide table, three anomalies

emp_idemp_namedept_namedept_location
1Asha RaoEngineeringBengaluru
2Ravi KumarEngineeringBengaluru
5Sneha IyerSalesMumbai
AnomalyWhat goes wrong here
UpdateEngineering moves to Hyderabad: miss one row, and Engineering is in two cities at once
InsertA new Finance department can't be recorded until it has an employee, since every row needs one
DeleteSneha leaves: deleting her row also deletes the only record that Sales is in Mumbai

The normalized version stores the department's facts once, in departments, and each employee just points at it with dept_id. That's exactly the shared schema on this page: Finance can exist with no employees, and moving Engineering is a one-row update.

Denormalization, the usual follow-up: deliberately adding redundancy back for faster reads, such as storing order_total on an order instead of summing its lines every time, or copying the customer's name into a reporting table. You trade write complexity and the risk of drift for fewer joins; it's common in analytics and read-heavy systems.

Follow-ups

  • Does normalization make queries slower? Reads that need joins can be, slightly; writes are simpler and safer. Normalize first, then denormalize specific spots where measurements show it's needed.
  • What is redundancy, precisely? The same fact stored in more than one place, which is what lets copies disagree.
Q32Not marked yetFresherAsked very oftenConcept

What do 1NF, 2NF, 3NF and BCNF each require? Show it by normalizing one wide table step by step

Say this first~30 sec to say

1NF: every column holds a single, atomic value, with no lists or repeating groups. 2NF: 1NF, and no non-key column depends on only part of a composite key. 3NF: 2NF, and no non-key column depends on another non-key column (no transitive dependency). BCNF: for every functional dependency X → Y, X must be a candidate key. A functional dependency, X → Y, means knowing X fixes Y, as emp_id determines emp_name.

Could you say it?
Full answer3 min read

Start: one unnormalized table of employee project assignments

emp_idemp_namedept_iddept_nameprojects
1Asha Rao1EngineeringP1 (40 hrs), P2 (10 hrs)
2Ravi Kumar1EngineeringP1 (25 hrs)

1NF: make values atomic. projects holds a list, so split it into one row per employee per project:

emp_idproject_idemp_namedept_iddept_namehours
1P1Asha Rao1Engineering40
1P2Asha Rao1Engineering10
2P1Ravi Kumar1Engineering25

The key is now (emp_id, project_id).

2NF: remove partial dependencies. emp_name, dept_id and dept_name depend on emp_id alone, which is only part of the key. Only hours depends on the whole key. Split:

  • employees (emp_id, emp_name, dept_id, dept_name)
  • assignments (emp_id, project_id, hours)

3NF: remove transitive dependencies. In employees, emp_id → dept_id and dept_id → dept_name, so dept_name depends on the key only through dept_id. Split again:

  • employees (emp_id, emp_name, dept_id)
  • departments (dept_id, dept_name)
  • assignments (emp_id, project_id, hours)

Now each fact lives once: a name in employees, a department name in departments, hours in assignments.

BCNF: every determinant is a candidate key. 3NF still allows a dependency X → Y where Y is part of a key and X isn't a candidate key. Example: each topic in a training programme is taught by exactly one trainer, and each trainer teaches one topic; students attend topics. In (student, topic, trainer), the key is (student, topic), but trainer → topic and trainer isn't a candidate key. That breaks BCNF; splitting into (trainer, topic) and (student, trainer) fixes it.

Normal formRule, in one line
1NFAtomic values; no repeating groups
2NF1NF + no partial dependency on a composite key
3NF2NF + no transitive dependency through a non-key column
BCNFEvery determinant is a candidate key

A memory aid often quoted for 3NF: every non-key column depends on "the key, the whole key, and nothing but the key".

Follow-ups

  • Can a table with a single-column key violate 2NF? No: partial dependency needs a composite key. It can still violate 3NF.
  • Do 4NF and 5NF matter? They deal with multi-valued and join dependencies, and rarely come up at this level.

09Transactions and concurrency

0/3 got it

Q33Not marked yetFresherAsked very oftenConcept

What is a transaction, and what does each ACID property guarantee?

Say this first~30 sec to say

A transaction is a group of statements that succeed or fail as one unit, like a bank transfer's debit and credit. ACID describes its guarantees. Atomicity: all of it happens or none of it does. Consistency: it takes the database from one valid state to another, respecting every constraint. Isolation: concurrent transactions don't see each other's half-finished work. Durability: once committed, it survives a crash. You control it with START TRANSACTION, COMMIT, ROLLBACK and SAVEPOINT.

Could you say it?
Full answer2 min read
SQL
CREATE TABLE accounts (
  account_id INT PRIMARY KEY,
  holder     VARCHAR(50),
  balance    DECIMAL(10,2) NOT NULL CHECK (balance >= 0)
);
INSERT INTO accounts VALUES (1, 'Asha', 5000.00), (2, 'Ravi', 1000.00);

START TRANSACTION;
UPDATE accounts SET balance = balance - 2000 WHERE account_id = 1;   -- debit Asha
UPDATE accounts SET balance = balance + 2000 WHERE account_id = 2;   -- credit Ravi
COMMIT;                                       -- both changes become permanent together
-- Asha 3000.00, Ravi 3000.00

If the server crashed between the two updates, or the second one failed, ROLLBACK (or crash recovery) undoes the first, so money is never debited without being credited.

SAVEPOINT: undo part of a transaction

SQL
START TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
SAVEPOINT after_fee;
UPDATE accounts SET balance = balance - 9999 WHERE account_id = 1;   -- fails the CHECK, or a mistake
ROLLBACK TO SAVEPOINT after_fee;                                     -- undo only the second update
COMMIT;                                                              -- the 500 fee stays
PropertyGuaranteesHow databases provide it
AtomicityAll or nothingUndo logs
ConsistencyConstraints hold before and afterConstraints, triggers, and correct application logic
IsolationConcurrent transactions don't interfereLocks and MVCC (question 34)
DurabilityCommitted data survives crashesWrite-ahead (redo) logs flushed to disk

A MySQL-specific note: ACID applies to the InnoDB engine, MySQL 8's default. The older MyISAM engine has no transactions at all, which is the usual follow-up: "InnoDB vs MyISAM?" InnoDB adds transactions, row-level locking, foreign keys and crash recovery.

Follow-ups

  • What is autocommit? MySQL's default mode: each statement outside an explicit transaction is committed on its own.
  • Does every statement in MySQL obey transactions? No: DDL such as CREATE, ALTER, DROP and TRUNCATE commits implicitly (question 8).
Q34Not marked yet1–3 yrsAsked oftenConcept

What are dirty reads, non-repeatable reads and phantom reads, and which isolation level prevents each?

Say this first~30 sec to say

A dirty read sees another transaction's uncommitted changes, which may be rolled back. A non-repeatable read reads the same row twice and gets different values, because another transaction committed an update in between. A phantom read runs the same query twice and gets new or missing rows. The four standard isolation levels block progressively more: READ UNCOMMITTED allows all three, READ COMMITTED blocks dirty reads, REPEATABLE READ also blocks non-repeatable reads, and SERIALIZABLE blocks all three.

Could you say it?
Full answer2 min read
Isolation levelDirty readNon-repeatable readPhantom read
READ UNCOMMITTEDPossiblePossiblePossible
READ COMMITTEDPreventedPossiblePossible
REPEATABLE READPreventedPreventedPossible in the standard; InnoDB prevents most cases
SERIALIZABLEPreventedPreventedPrevented

Each problem as a timeline

Transaction ATransaction B
Dirty readUPDATE accounts SET balance = 0 WHERE account_id = 1; (not committed)
SELECT balance … ; reads 0ROLLBACK;: the 0 never really existed
Non-repeatable readSELECT salary FROM employees WHERE emp_id = 3; → 120000
UPDATE employees SET salary = 130000 WHERE emp_id = 3; COMMIT;
The same SELECT → 130000
Phantom readSELECT COUNT(*) FROM orders WHERE status = 'placed'; → 1
INSERT INTO orders … 'placed' …; COMMIT;
The same SELECT → 2
SQL
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@transaction_isolation;          -- READ-COMMITTED

Defaults differ: InnoDB defaults to REPEATABLE READ, using MVCC snapshots plus next-key locks for locking reads. PostgreSQL, SQL Server and Oracle default to READ COMMITTED.

The trade-off: stronger isolation means more locking or more aborted transactions, so less concurrency. Most applications run at the default and lock explicitly where it matters, for example with SELECT … FOR UPDATE when booking the last seat.

Follow-ups

  • What is MVCC? Multi-version concurrency control: readers see a consistent snapshot of committed versions, so reads don't block writes and writes don't block reads.
  • What is a lost update? Two transactions read a value and both write back changes based on it, so one overwrites the other. SELECT … FOR UPDATE, or an atomic UPDATE … SET stock = stock - 1, prevents it.
Q35Not marked yet1–3 yrsAsked sometimesConcept

What is a deadlock between two transactions, and how does MySQL detect and resolve it?

Say this first~30 sec to say

A deadlock is two transactions each holding a lock the other needs, so both would wait forever: A locks row 1 and wants row 2, while B locks row 2 and wants row 1. InnoDB detects the cycle in its wait-for graph automatically, rolls back one transaction, usually the one that's cheaper to undo, and returns error 1213 to it; the other proceeds. The application should retry the failed transaction. Taking locks in a consistent order prevents most deadlocks.

Could you say it?
Full answer2 min read
StepTransaction ATransaction B
1UPDATE accounts … WHERE account_id = 1; locks row 1
2UPDATE accounts … WHERE account_id = 2; locks row 2
3UPDATE accounts … WHERE account_id = 2; waits for B
4UPDATE accounts … WHERE account_id = 1; waits for A: a cycle
5ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
6A's update goes throughB was rolled back; the app retries it

Shared vs exclusive locks, the usual lead-in: a shared (S) lock lets many transactions read a row at once; an exclusive (X) lock, taken for writes and SELECT … FOR UPDATE, blocks everyone else from locking that row.

Preventing deadlocks

  • Access rows and tables in the same order everywhere: for example, always lock the lower account ID first in a transfer.
  • Keep transactions short, and don't wait for user input inside one.
  • Use indexes on the columns in WHERE, so updates lock few rows instead of scanning and locking many.
  • Accept that some will still happen, and retry on error 1213.

Follow-ups

  • How do you investigate one? SHOW ENGINE INNODB STATUS shows the latest deadlock: both transactions, the locks held and the locks wanted.
  • Deadlock vs lock wait timeout? A deadlock is detected immediately; a plain wait gives up after innodb_lock_wait_timeout (50 seconds by default) with error 1205.
  • Do deadlocks happen in application code too? Yes, with threads and locks; the Java kit's question 38 covers them.

10Indexes

0/2 got it

Q36Not marked yetFresherAsked very oftenConcept

What is an index, and how do clustered and non-clustered indexes differ?

Say this first~25 sec to say

An index is a separate structure, usually a B+ tree, keeping column values sorted with pointers to their rows, so lookups and ranges skip the full scan. A clustered index stores the rows themselves in index order, so a table has only one. A non-clustered index stores the values plus a pointer back to the row. In InnoDB, the primary key is always the clustered index.

Could you say it?
Full answer2 min read
ClusteredNon-clustered (secondary)
Leaf level holdsThe full rowsThe indexed columns + a row pointer
How many per tableOneMany
In InnoDBAlways the primary keyEvery other index; the "pointer" is the primary key value
Lookup costFind the key: the row is right thereFind the key, then fetch the row (unless the index covers the query)
Good forRange scans on the key, ordered readsLookups and filters on other columns
SQL
CREATE INDEX idx_employees_dept_salary ON employees (dept_id, salary);

EXPLAIN SELECT emp_name FROM employees WHERE dept_id = 2 AND salary > 100000;
-- on a table of realistic size: type: range, key: idx_employees_dept_salary
-- (on 10 rows the optimiser may simply scan the table: it's cheaper)

A covering index contains every column a query needs, so the database never visits the table rows: SELECT dept_id, salary FROM employees WHERE dept_id = 2 can be answered from idx_employees_dept_salary alone, shown as "Using index" in EXPLAIN.

How the two kinds fit together in InnoDB: a lookup by email searches the email index, finds the emp_id stored there, then searches the clustered primary-key index for the row. That's why a long primary key makes every secondary index bigger.

SQL Server lets you choose which index is clustered (it defaults to the primary key); InnoDB always clusters on the primary key, or a hidden row ID if a table has none. Why an index speeds up lookups at all, and what it costs on writes, is covered in the Backend kit's question 30.

Follow-ups

  • Why are B+ trees used rather than binary trees? Each node holds hundreds of keys, so the tree is only three or four levels deep even for millions of rows, which means few disk reads per lookup. Leaves are linked, which makes range scans fast.
  • What is a hash index? A structure for exact-match lookups only: no ranges, no sorting. InnoDB uses an adaptive hash index internally; MEMORY tables support them explicitly.
Q37Not marked yet1–3 yrsAsked oftenConcept

When can't a query use an index on a column, and why does column order matter in a composite index?

Say this first~30 sec to say

An index is sorted by the raw column values, so it can't help when the query hides the column: wrapping it in a function or calculation (YEAR(hire_date) = 2024, salary * 12 > …), a leading wildcard (LIKE '%@gmail.com'), or a type mismatch that forces conversion. In a composite index (dept_id, salary), rows are sorted by dept_id first, then salary, so it serves filters on dept_id, or on both, but not on salary alone: the leftmost-prefix rule.

Could you say it?
Full answer2 min read
Query on employeesUses an index on hire_date?Rewrite
WHERE YEAR(hire_date) = 2024No: the function hides the columnWHERE hire_date >= '2024-01-01' AND hire_date < '2025-01-01'
WHERE salary * 12 > 1800000 (on a salary index)NoWHERE salary > 150000
WHERE email LIKE '%@company.in' (on email)No: leading wildcardStore the domain in its own column, or use full-text search
WHERE email LIKE 'asha%'Yes: a fixed prefix is a range
WHERE phone = 9876543210 on a VARCHAR columnOften not: the number forces conversionWHERE phone = '9876543210'

The leftmost-prefix rule, with INDEX (dept_id, salary)

FilterIndex used?
WHERE dept_id = 2Yes
WHERE dept_id = 2 AND salary > 100000Yes: both columns
WHERE salary > 100000No: salary is only sorted within each dept_id
WHERE dept_id = 2 ORDER BY salaryYes, and the sort comes free

Think of a phone book sorted by surname, then first name: it finds "Sharma, Priya" instantly, and every Sharma, but it's no help for "everyone called Priya".

Choosing the column order: put columns tested with equality first and range columns last, and favour the column that appears in the most queries.

Checking with EXPLAIN

SQL
EXPLAIN SELECT emp_name FROM employees WHERE YEAR(hire_date) = 2024;
-- type: ALL (a full scan), even with an index on hire_date

In EXPLAIN, type shows the access method: ALL is a full scan; range, ref and const use an index, from broad to precise. key names the index chosen, and rows estimates how many rows will be examined.

Follow-ups

  • Can MySQL index an expression? Yes, since 8.0.13 a functional index works: CREATE INDEX idx_hire_year ON employees ((YEAR(hire_date)));.
  • Does a low-selectivity column deserve an index? Rarely. On a column like status with three values, the optimiser often prefers a scan anyway.

11Views, stored procedures and triggers

0/3 got it

Q38Not marked yetFresherAsked oftenConcept

What is a view, and can you insert or update data through one?

Say this first~30 sec to say

A view is a saved SELECT with a name, queried like a table; it stores no data of its own, so it always reflects the current rows. Views simplify complex joins, give a stable interface while tables change, and limit access, for example showing employees without their salaries. Simple single-table views are updatable: inserts and updates pass through to the base table. Views with GROUP BY, DISTINCT, aggregates, UNION or most joins are read-only.

Could you say it?
Full answer2 min read
SQL
CREATE VIEW employee_directory AS
SELECT e.emp_id, e.emp_name, e.email, d.dept_name
FROM employees e
LEFT JOIN departments d ON d.dept_id = e.dept_id;       -- no salaries in here

SELECT * FROM employee_directory WHERE dept_name = 'Sales';

GRANT SELECT ON company.employee_directory TO 'intranet'@'%';   -- access to the view, not the table

Updatable or not

SQL
CREATE VIEW sales_team AS
SELECT emp_id, emp_name, salary, dept_id FROM employees WHERE dept_id = 2
WITH CHECK OPTION;                    -- rejects changes that would move a row out of the view

UPDATE sales_team SET salary = 165000 WHERE emp_id = 7;   -- allowed: updates employees
UPDATE sales_team SET dept_id = 3 WHERE emp_id = 7;       -- rejected by WITH CHECK OPTION

CREATE VIEW dept_payroll AS
SELECT dept_id, SUM(salary) AS total FROM employees GROUP BY dept_id;
-- read-only: an UPDATE fails, because a total maps to no single row
View containsUpdatable?
One table, plain columns, optional WHEREYes
GROUP BY, aggregates, DISTINCT, HAVINGNo
UNIONNo
JoinsSometimes, for columns of one base table, with restrictions

Materialized views store the query's result and refresh it on demand, which suits expensive reports. MySQL has no materialized views; you emulate them with a summary table refreshed by a scheduled event. PostgreSQL has CREATE MATERIALIZED VIEW, and SQL Server uses indexed views.

Follow-ups

  • Does a view make a query faster? Not by itself: it's expanded into the underlying query each time. A materialized view can.
  • What happens if the base table drops a column the view uses? The view becomes invalid and errors when queried until it's recreated.
Q39Not marked yet1–3 yrsAsked oftenConcept

Stored procedure vs function: how do they differ, and when would you keep logic in the database rather than in application code?

Say this first~30 sec to say

Both are named routines stored in the database. A stored procedure runs with CALL, takes IN, OUT and INOUT parameters, can return result sets and can manage transactions. A function returns one value and can be used inside a SELECT, but can't commit or roll back. Keep logic in the database when it's data-heavy or shared by several apps; keep business rules in application code, where they're easier to test and change.

Could you say it?
Full answer3 min read
SQL
DELIMITER //

CREATE FUNCTION annual_ctc(monthly DECIMAL(10,2))
RETURNS DECIMAL(12,2)
DETERMINISTIC
BEGIN
  RETURN monthly * 12;
END //

CREATE PROCEDURE give_department_raise(IN p_dept_id INT, IN p_percent DECIMAL(5,2), OUT p_updated INT)
BEGIN
  START TRANSACTION;
  UPDATE employees
  SET salary = ROUND(salary * (1 + p_percent / 100), 2)
  WHERE dept_id = p_dept_id;
  SET p_updated = ROW_COUNT();
  COMMIT;
END //

DELIMITER ;

SELECT emp_name, annual_ctc(salary) AS yearly FROM employees WHERE dept_id = 3;   -- a function, inside SELECT
CALL give_department_raise(3, 10, @updated);                                      -- a procedure, with CALL
SELECT @updated;                                                                  -- 2 rows updated

DELIMITER // changes the statement terminator in the MySQL client, so the semicolons inside the body don't end the CREATE statement early.

Stored procedureFunction
Called withCALL proc(...)Inside a query: SELECT f(col)
ReturnsNothing, OUT parameters, or result setsExactly one value
TransactionsCan COMMIT and ROLLBACKCan't
Can modify dataYesAvoid it; MySQL restricts it
Typical useBatch jobs, multi-step operationsReusable calculations

Database or application?

Keep it in the database whenKeep it in the application when
It processes lots of rows that shouldn't cross the networkIt's business logic that changes often
Several applications must apply it identicallyIt needs unit tests and code review like other code
It enforces data integrityIt calls external services
You may change databases later

Follow-ups

  • Are stored procedures faster? Sometimes, by saving round trips for multi-step work, but most performance comes from good queries and indexes, wherever they're sent from.
  • Do procedures prevent SQL injection? Only if they use parameters internally; building dynamic SQL from strings inside a procedure is just as injectable.
Q40Not marked yetFresherAsked oftenConcept

What is a trigger, and when would you use one or avoid one?

Say this first~30 sec to say

A trigger is code the database runs automatically BEFORE or AFTER an INSERT, UPDATE or DELETE on a table, with access to the row's old and new values. Good uses: writing an audit trail, maintaining derived values, or enforcing a rule no constraint can express. Use them sparingly: they're hidden side effects that run on every matching change, making behaviour hard to trace, debug and test, and slowing bulk writes. In MySQL, triggers fire once per row.

Could you say it?
Full answer2 min read
SQL
CREATE TABLE salary_audit (
  audit_id   INT AUTO_INCREMENT PRIMARY KEY,
  emp_id     INT,
  old_salary DECIMAL(10,2),
  new_salary DECIMAL(10,2),
  changed_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

DELIMITER //
CREATE TRIGGER trg_salary_audit
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
  IF OLD.salary <> NEW.salary THEN           -- only log real salary changes
    INSERT INTO salary_audit (emp_id, old_salary, new_salary)
    VALUES (OLD.emp_id, OLD.salary, NEW.salary);
  END IF;
END //
DELIMITER ;

UPDATE employees SET salary = 130000 WHERE emp_id = 3;
SELECT emp_id, old_salary, new_salary FROM salary_audit;
-- 3 | 120000.00 | 130000.00

OLD and NEW: in an UPDATE trigger, OLD holds the row before the change and NEW after it. INSERT triggers have only NEW; DELETE triggers only OLD. A BEFORE trigger can modify NEW to adjust the values being written.

Use a trigger forAvoid a trigger for
Audit logs that must be written whatever app makes the changeBusiness workflows (sending emails, calling APIs)
Keeping a denormalized column in syncLogic other developers need to see and test
Rules spanning tables that constraints can't expressHeavy work on high-volume tables

Database differences: MySQL triggers are row-level only (FOR EACH ROW). SQL Server triggers fire once per statement, with all affected rows in the inserted and deleted tables, so code written for one row breaks on multi-row updates. PostgreSQL supports both.

Follow-ups

  • Can a trigger stop the change? Yes: a BEFORE trigger can SIGNAL SQLSTATE '45000' with a message, which aborts the statement.
  • How many triggers can a table have? MySQL allows several per event and timing, ordered with FOLLOWS and PRECEDES.

Last step

Write these from memory

Before an SQL round, make sure you can write each of these on a blank page without help:

  • Q7
  • Q14
  • Q15
  • Q20
  • Q22
  • Q23
  • Q24
  • Q25
  • Q26
  • Q27
  • Q28
  • Q29
  • Q30

0 of 13 done