All Blog Posts

Every article published on Tanveer Qureshie Tech, newest first.

Loading all posts...

MySQL Interview Preparation — Top 50 Questions By Tanveer Qureshie

Compiled & Designed by Tanveer Qureshie

MySQL Queries & Subqueries Guide

50 in-depth MySQL interview questions focused on queries and subqueries, with easy-to-understand explanations, comparison tables, visual diagrams and real SQL, covering data types, filtering, joins, subqueries, CTEs, aggregates, views, indexing and optimization. Tick each question off as you master it.

50 QuestionsBasic to AdvancedComparison TablesVisual DiagramsTick to Track Progress
YOUR PROGRESS0 / 50 questions completed (0%)
SECTION 01

MySQL Basics, Data Types & Constraints

0 / 10 done
VISUAL MODEL

Logical Order MySQL Actually Evaluates a Query In

FROM / JOINWHEREGROUP BYHAVINGSELECT → ORDER BY → LIMIT

SQL is written SELECT-first, but MySQL evaluates it in roughly this order internally. This is exactly why you can't reference a SELECT alias in WHERE, but you often can in ORDER BY or HAVING — those clauses run after SELECT has already computed its output columns.

001What is MySQL, and how does a query actually get executed inside it?Beginner+

Easy explanation

MySQL is an open-source relational database management system that stores data in tables and lets you query it using SQL (Structured Query Language). When you send a query, MySQL doesn't execute it in the order you typed it — it parses it, the optimizer picks an execution plan, and only then does the storage engine actually fetch and process the rows.

Understanding the LOGICAL order of evaluation (FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT) rather than the WRITTEN order (SELECT ... FROM ... WHERE ...) explains a lot of behavior that otherwise seems confusing — like why a column alias defined in SELECT can't be used in WHERE, but can often be used in ORDER BY.

Interview tip: None
SELECT name, salary * 12 AS annual_salary
FROM employees
WHERE department = 'Sales'   -- can't use 'annual_salary' here, it doesn't exist yet
ORDER BY annual_salary DESC; -- CAN use it here, SELECT has already run
002What are the main MySQL data types, and how do you choose between them?Beginner+

Easy explanation

Numeric types include INT (whole numbers), DECIMAL/NUMERIC (exact fixed-point numbers — essential for money, since floating point can introduce rounding errors), and FLOAT/DOUBLE (approximate, faster, but not appropriate for currency). String types include VARCHAR (variable-length text with a max length), CHAR (fixed-length, padded), and TEXT (for large, unbounded text).

Date/time types include DATE, DATETIME, and TIMESTAMP (TIMESTAMP is timezone-aware and stored in UTC internally, converting on read/write, while DATETIME stores exactly what you give it with no timezone conversion). Choosing the smallest type that safely fits your data matters for both storage size and index performance — using BIGINT for a column that will never exceed a few thousand rows wastes space unnecessarily.

Interview tip: None
TypeUse for
INT / BIGINTWhole numbers, IDs, counts
DECIMAL(10,2)Money — exact, no rounding errors
VARCHAR(n)Variable-length text with a known max
TEXTLarge, unbounded text (like a blog post)
DATETIMEA specific date and time, no timezone conversion
TIMESTAMPA moment in time, timezone-aware, auto-converts
003What is the difference between CHAR and VARCHAR?Beginner+

Easy explanation

CHAR is a FIXED-length string type — if you define `CHAR(10)` and store a 3-character value, MySQL pads it with spaces to fill all 10 characters, and it always uses the same amount of storage regardless of the actual content's length.

VARCHAR is a VARIABLE-length string type — it only uses as much storage as the actual string needs (plus a small length-tracking overhead), making it more space-efficient for values that vary a lot in length. The rule of thumb: use CHAR for fixed-length codes that are always the same length (like a 2-letter country code), and VARCHAR for everything else, like names or descriptions.

Interview tip: None
CREATE TABLE codes (
  country_code CHAR(2),      -- always exactly 2 chars, e.g. 'US'
  description VARCHAR(255)   -- varies in length
);
004What is a PRIMARY KEY constraint, and how does it differ from a UNIQUE constraint?Beginner+

Easy explanation

A PRIMARY KEY uniquely identifies each row in a table — it can never be NULL, and a table can only have ONE primary key (though that key can span multiple columns as a composite key). Most tables use an auto-incrementing integer as their primary key.

A UNIQUE constraint also ensures no two rows share the same value in that column, but unlike a primary key, a table can have MULTIPLE unique constraints, and (in MySQL) a UNIQUE column can allow NULL values (and can allow more than one NULL, since NULL is never considered equal to another NULL for uniqueness checks).

Interview tip: None
CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  email VARCHAR(255) UNIQUE,   -- unique, but not the primary key
  phone VARCHAR(20) UNIQUE
);
005What is a FOREIGN KEY constraint, and what do ON DELETE CASCADE / SET NULL / RESTRICT actually do?Intermediate+

Easy explanation

A FOREIGN KEY links a column in one table to the PRIMARY KEY of another table, enforcing referential integrity — you can't insert a row referencing a parent that doesn't exist. It also controls what happens to CHILD rows when the referenced PARENT row is deleted or updated.

ON DELETE CASCADE automatically deletes the child rows too when the parent is deleted (useful for genuinely dependent data, like deleting a post's comments when the post itself is deleted). ON DELETE SET NULL sets the foreign key column to NULL instead of deleting the child (useful when the child can meaningfully exist without that parent). ON DELETE RESTRICT (the default in many setups) simply BLOCKS the delete entirely if any child rows still reference that parent, forcing you to handle them explicitly first.

Interview tip: None
CREATE TABLE comments (
  id INT PRIMARY KEY,
  post_id INT,
  FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
);
-- deleting a post automatically deletes all its comments
006What is the difference between NULL and an empty string, or NULL and 0?Intermediate+

Easy explanation

NULL represents the ABSENCE of a value entirely — 'unknown' or 'not applicable' — it's fundamentally different from an empty string `''` (a string that exists but has zero characters) or the number `0` (a real, known numeric value). A `phone_number` column being NULL means 'we don't have this information'; being `''` means 'we explicitly recorded that there's no phone number as an empty value'.

This distinction matters enormously in comparisons: `NULL = NULL` evaluates to NULL (not TRUE!), because MySQL can't say two unknown things are equal — you must use `IS NULL` or `IS NOT NULL` to correctly check for NULL, since `column = NULL` will never match any row, even one that IS NULL.

Interview tip: None
SELECT * FROM users WHERE phone = NULL;      -- WRONG: matches nothing, ever
SELECT * FROM users WHERE phone IS NULL;      -- correct way to check for NULL
SELECT * FROM users WHERE phone IS NOT NULL;  -- correct way to check for NOT NULL
007What are MySQL storage engines, and what is the difference between InnoDB and MyISAM?Intermediate+

Easy explanation

A storage engine is the underlying component that actually handles how MySQL stores, retrieves, and manages data on disk — different engines have different trade-offs, and MySQL lets you choose per table (though virtually all modern applications use InnoDB by default).

InnoDB supports transactions (COMMIT/ROLLBACK), foreign key constraints, and row-level locking (multiple transactions can write to different rows in the same table concurrently) — it's the default engine since MySQL 5.5 and the right choice for almost every real application. MyISAM is an older engine that doesn't support transactions or foreign keys and only offers table-level locking (the whole table locks during a write), but was historically slightly faster for read-heavy, non-transactional workloads — it's rarely the right choice for new projects today.

Interview tip: None
InnoDBMyISAM
TransactionsYesNo
Foreign keysYesNo
Locking granularityRow-levelTable-level
Modern default?YesNo — legacy
008What is AUTO_INCREMENT, and what happens to it if a transaction is rolled back?Intermediate+

Easy explanation

AUTO_INCREMENT automatically generates a new, unique, incrementing numeric value for a column (usually the primary key) every time a row is inserted, without you having to specify it manually — it's the standard way to generate simple surrogate keys in MySQL.

An important, often-surprising behavior: if a transaction that inserted a row is rolled back, the AUTO_INCREMENT value that was consumed by that failed insert is generally NOT reused — the counter still advances, leaving a 'gap' in the sequence. This is intentional (avoiding it would require locking the counter in ways that hurt concurrency), so application code should never assume auto-increment IDs are perfectly sequential with no gaps.

Interview tip: None
CREATE TABLE orders (
  id INT PRIMARY KEY AUTO_INCREMENT,
  total DECIMAL(10,2)
);
INSERT INTO orders (total) VALUES (49.99); -- id is generated automatically
009What is a CHECK constraint, and how do you use it to enforce business rules at the database level?Intermediate+

Easy explanation

A CHECK constraint lets you define a condition that every row in a table must satisfy — if an INSERT or UPDATE would violate it, MySQL rejects the statement entirely. This lets you enforce basic business rules directly in the database schema, as a safety net even if application-level validation has a bug or is bypassed.

CHECK constraints are fully supported starting MySQL 8.0.16 (earlier versions silently accepted but ignored the syntax) — common uses include ensuring a price is never negative, an age is within a sane range, or a status column only contains one of a specific set of allowed values.

Interview tip: None
CREATE TABLE products (
  id INT PRIMARY KEY,
  price DECIMAL(10,2) CHECK (price >= 0),
  status VARCHAR(20) CHECK (status IN ('active','discontinued'))
);
010What is the difference between DELETE, TRUNCATE, and DROP?Advanced+

Easy explanation

DELETE removes rows from a table based on a WHERE condition (or all rows if no WHERE is given), one row at a time, logging each deletion — it can be rolled back within a transaction, and any AUTO_INCREMENT counter is unaffected. TRUNCATE removes ALL rows from a table at once, much faster than DELETE since it deallocates the table's data pages directly rather than logging individual row deletions — but it also resets the AUTO_INCREMENT counter back to its starting value, and in most configurations it can't be rolled back.

DROP removes the ENTIRE table (or database) — not just the data, but the structure itself (columns, constraints, indexes) — it's the most destructive of the three and is not reversible without a backup. The rule of thumb: DELETE for selective row removal within a transaction, TRUNCATE for fast, total row wipeout when you're keeping the table structure, DROP only when you genuinely want the table gone entirely.

Interview tip: None
CommandRemovesResets auto-increment?Can filter with WHERE?
DELETESome or all rowsNoYes
TRUNCATEAll rowsYesNo
DROPThe entire table structureN/A — table is goneNo
SECTION 02

SELECT, Filtering & Sorting

0 / 10 done
VISUAL MODEL

Filtering a Result Set Step by Step

All RowsWHERE narrows rowsGROUP BY groups themHAVING filters groupsFinal Result

WHERE filters individual rows BEFORE grouping happens, so it can't reference an aggregate like COUNT(*). HAVING filters AFTER grouping, which is the only place you can filter on an aggregate value.

011What is the basic anatomy of a SELECT statement, and in what order should its clauses be written?Beginner+

Easy explanation

A SELECT statement is written in this order: SELECT (which columns), FROM (which table), WHERE (row filter), GROUP BY (grouping), HAVING (group filter), ORDER BY (sort order), LIMIT (row count cap). Not every clause is required — the simplest valid query is just `SELECT column FROM table`.

Remember that the WRITTEN order and the actual EVALUATION order are different — MySQL evaluates FROM/WHERE/GROUP BY/HAVING before it even computes the SELECT list, and only sorts (ORDER BY) and limits (LIMIT) after that — this explains several syntax quirks that otherwise seem arbitrary.

Interview tip: None
SELECT department, COUNT(*) AS emp_count
FROM employees
WHERE status = 'active'
GROUP BY department
HAVING COUNT(*) > 5
ORDER BY emp_count DESC
LIMIT 10;
012What is the difference between WHERE and HAVING?Beginner+

Easy explanation

WHERE filters individual ROWS before any grouping or aggregation happens — it operates on the raw table data, and cannot reference an aggregate function's result, because that result doesn't exist yet at this stage of execution.

HAVING filters GROUPS, after GROUP BY has already run — it's the only clause where you can filter based on an aggregate value like `COUNT(*) > 5` or `AVG(salary) > 50000`. A very common mistake is trying to write `WHERE COUNT(*) > 5`, which fails because COUNT(*) simply doesn't exist as a value at the point WHERE is evaluated.

Interview tip: None
SELECT department, AVG(salary) AS avg_sal
FROM employees
WHERE status = 'active'      -- filters rows first
GROUP BY department
HAVING AVG(salary) > 60000;  -- filters the resulting groups
013How do you remove duplicate rows from a result using DISTINCT?Beginner+

Easy explanation

DISTINCT removes duplicate ROWS from the result set — if you select multiple columns, DISTINCT considers the COMBINATION of all selected columns together, not each column independently, so two rows are only considered duplicates if every selected column matches.

A common use is finding the unique set of values in one column, like every distinct city customers are from — but be careful when selecting multiple columns with DISTINCT, since it's easy to accidentally get more 'unique' rows than expected if you didn't realize a second column was also being considered.

Interview tip: None
SELECT DISTINCT city FROM customers;
-- vs, considers BOTH columns together:
SELECT DISTINCT city, country FROM customers;
014What are the different types of operators you can use in a WHERE clause (comparison, logical, range, pattern, set)?Intermediate+

Easy explanation

Comparison operators (`=`, `!=`/`<>`, `<`, `>`, `<=`, `>=`) compare a column to a value. Logical operators (`AND`, `OR`, `NOT`) combine multiple conditions. Range operators use `BETWEEN x AND y` (inclusive on both ends) instead of writing two separate comparisons.

Pattern matching uses `LIKE` with wildcards — `%` matches any number of characters, `_` matches exactly one character. Set membership uses `IN (...)` to check if a value matches any item in a list, which is cleaner than chaining many `OR` conditions together.

Interview tip: None
SELECT * FROM products
WHERE price BETWEEN 10 AND 50
  AND category IN ('Electronics','Books')
  AND name LIKE 'Wireless%'    -- starts with 'Wireless'
  AND status != 'discontinued';
015How does ORDER BY work with multiple columns, and how do you sort NULLs?Intermediate+

Easy explanation

ORDER BY sorts the result set, and you can list MULTIPLE columns — MySQL sorts by the first column, and only uses the second column to break ties WITHIN groups of rows that have the same value in the first column, and so on for additional columns.

By default, MySQL sorts NULLs as if they were the SMALLEST possible value — they appear FIRST in ascending order and LAST in descending order. To control this explicitly regardless of sort direction, a common trick is adding `column IS NULL` as an extra sort expression, which evaluates to 0 (not null) or 1 (null), letting you push NULLs to a specific position.

Interview tip: None
SELECT * FROM employees
ORDER BY department ASC, salary DESC;
-- push NULLs to the end regardless of direction:
SELECT * FROM employees ORDER BY commission IS NULL, commission DESC;
016What is the difference between LIMIT and LIMIT with OFFSET, and how do you paginate results?Intermediate+

Easy explanation

`LIMIT n` restricts the result set to at most `n` rows. Adding an OFFSET (`LIMIT n OFFSET m`, or the shorthand `LIMIT m, n`) skips the first `m` rows before returning the next `n` — this is the classic way to implement 'pages' of results, like page 3 of a product listing.

The major downside of OFFSET-based pagination: for a large offset, MySQL still has to scan and discard all the skipped rows internally before it can return your actual page, making deep pagination (like page 5,000) progressively slower. For large-scale pagination, cursor-based pagination (`WHERE id > last_seen_id ORDER BY id LIMIT n`) is significantly faster since it can jump straight to the right position using an index.

Interview tip: None
-- page 3, 20 items per page (offset-based, simple but slow for deep pages)
SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 40;
-- cursor-based (fast even for 'deep' pages)
SELECT * FROM products WHERE id > 1043 ORDER BY id LIMIT 20;
017What are the common MySQL string functions you should know (CONCAT, SUBSTRING, TRIM, REPLACE, UPPER/LOWER)?Intermediate+

Easy explanation

CONCAT joins multiple strings together (`CONCAT(first_name, ' ', last_name)`). SUBSTRING extracts a portion of a string starting at a given position for a given length. TRIM removes leading/trailing whitespace (or a specified character) from a string.

REPLACE swaps every occurrence of a substring with another substring within a string. UPPER and LOWER convert a string's case, commonly used for case-insensitive comparisons in databases where the column's default collation is case-sensitive.

Interview tip: None
SELECT CONCAT(first_name, ' ', last_name) AS full_name,
       SUBSTRING(email, 1, LOCATE('@', email)-1) AS username,
       TRIM(bio) AS clean_bio,
       REPLACE(phone, '-', '') AS digits_only,
       LOWER(email) AS normalized_email
FROM users;
018What are the common MySQL date/time functions (NOW, DATEDIFF, DATE_ADD, DATE_FORMAT)?Intermediate+

Easy explanation

NOW() returns the current date and time. CURDATE() returns just today's date. DATEDIFF(date1, date2) returns the number of days between two dates. DATE_ADD (and DATE_SUB) let you add or subtract a specific interval (days, months, years) from a date.

DATE_FORMAT lets you convert a date/time value into a specific human-readable string format, using format specifiers like `%Y` for a 4-digit year or `%d` for a 2-digit day — extremely common when generating reports or displaying dates to users.

Interview tip: None
SELECT
  NOW() AS right_now,
  DATEDIFF(CURDATE(), order_date) AS days_since_order,
  DATE_ADD(order_date, INTERVAL 30 DAY) AS warranty_expires,
  DATE_FORMAT(order_date, '%M %d, %Y') AS pretty_date
FROM orders;
019What is the difference between LIKE with wildcards and REGEXP for pattern matching?Advanced+

Easy explanation

LIKE offers simple pattern matching using just two wildcards: `%` (any number of any characters) and `_` (exactly one character) — it's fast, simple, and sufficient for basic 'starts with', 'ends with', or 'contains' style searches.

REGEXP (or RLIKE) supports full regular expression pattern matching, letting you express far more complex patterns — matching one of several alternatives, character classes, repetition counts, and anchors — at the cost of being slower to evaluate and generally not able to use a standard B-tree index efficiently, since the pattern isn't necessarily a simple prefix match.

Interview tip: None
SELECT * FROM products WHERE name LIKE 'iPhone%';           -- starts with 'iPhone'
SELECT * FROM products WHERE name REGEXP '^(iPhone|iPad)[0-9]+$'; -- more complex pattern
020What is the CASE expression, and how is it used to add conditional logic directly inside a query?Advanced+

Easy explanation

CASE lets you embed IF/ELSE-style conditional logic directly inside a SELECT, WHERE, or ORDER BY clause, evaluating a series of WHEN conditions in order and returning the value associated with the first one that matches (or an ELSE value if none match).

This is extremely useful for creating computed categories on the fly (like bucketing ages into groups), for conditional aggregation (counting how many rows meet one condition vs another, all in a single query), or for custom sort orders that don't match a plain alphabetical or numeric order.

Interview tip: None
SELECT name,
  CASE
    WHEN age < 18 THEN 'Minor'
    WHEN age BETWEEN 18 AND 64 THEN 'Adult'
    ELSE 'Senior'
  END AS age_group
FROM people;

-- conditional aggregation: count active vs inactive in one query
SELECT
  SUM(CASE WHEN status='active' THEN 1 ELSE 0 END) AS active_count,
  SUM(CASE WHEN status='inactive' THEN 1 ELSE 0 END) AS inactive_count
FROM users;
SECTION 03

Joins & Set Operations

0 / 10 done
VISUAL MODEL

Visualizing the Main JOIN Types

Table A Table B INNER JOIN (overlap only) Table A Table B LEFT JOIN (all of A, matched or not)

INNER JOIN keeps only rows where both tables have a match. LEFT JOIN keeps every row from the left table regardless of a match, filling unmatched right-side columns with NULL.

021What is a JOIN, and why is it fundamental to relational databases?Beginner+

Easy explanation

A JOIN combines rows from two or more tables based on a related column between them, typically a foreign key referencing a primary key — this is what lets you keep data normalized across separate tables (avoiding duplication) while still being able to reconstruct the full, combined picture when you actually need it.

Without JOINs, you'd either have to denormalize everything into one giant table (reintroducing all the redundancy normalization was meant to remove) or make multiple separate round trips and manually stitch the results together in application code — JOIN lets the database do this combination work efficiently, in a single query.

Interview tip: None
SELECT orders.id, customers.name
FROM orders
JOIN customers ON orders.customer_id = customers.id;
022What is the difference between INNER JOIN and LEFT JOIN?Beginner+

Easy explanation

INNER JOIN returns only the rows where a match exists in BOTH tables — if a customer has never placed an order, that customer won't appear at all in an INNER JOIN between customers and orders.

LEFT JOIN (a.k.a. LEFT OUTER JOIN) keeps EVERY row from the left (first-named) table, whether or not a match exists on the right — any columns coming from the unmatched right table are filled with NULL. This is exactly the join you need for a question like 'show me every customer, including ones who've never ordered anything'.

Interview tip: None
-- INNER JOIN: only customers WITH orders appear
SELECT c.name, o.total FROM customers c
JOIN orders o ON o.customer_id = c.id;

-- LEFT JOIN: every customer appears, total is NULL if no orders
SELECT c.name, o.total FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id;
023What is a RIGHT JOIN, and why do most developers just use LEFT JOIN instead?Intermediate+

Easy explanation

RIGHT JOIN (RIGHT OUTER JOIN) is the mirror image of LEFT JOIN — it keeps every row from the RIGHT table, filling unmatched left-side columns with NULL. Logically it's identical to a LEFT JOIN with the table order simply swapped.

Because you can always rewrite any RIGHT JOIN as an equivalent LEFT JOIN by reversing which table is listed first, most style guides and developers stick to ONLY using LEFT JOIN for consistency and readability — having both directions used interchangeably in a codebase makes queries harder to scan and compare at a glance.

Interview tip: None
-- these two are logically equivalent:
SELECT * FROM a RIGHT JOIN b ON a.id = b.a_id;
SELECT * FROM b LEFT JOIN a ON a.id = b.a_id;
024What is a FULL OUTER JOIN, and how do you simulate it in MySQL (which doesn't support it natively)?Intermediate+

Easy explanation

A FULL OUTER JOIN keeps ALL rows from BOTH tables — matched rows are combined normally, and unmatched rows from either side appear with NULLs filling in for the columns from the other table. It answers 'give me everything from both tables, matched where possible'.

MySQL doesn't have native FULL OUTER JOIN support — the standard workaround is to combine a LEFT JOIN and a RIGHT JOIN (or two LEFT JOINs with table order swapped) using UNION, which also conveniently removes the duplicate rows that would otherwise appear where both joins matched the same rows.

Interview tip: None
-- simulating FULL OUTER JOIN in MySQL
SELECT a.*, b.* FROM a LEFT JOIN b ON a.id = b.a_id
UNION
SELECT a.*, b.* FROM a RIGHT JOIN b ON a.id = b.a_id;
025What is a self-join, and when is it actually useful?Intermediate+

Easy explanation

A self-join joins a table to ITSELF, treated as if it were two separate tables using two different aliases — this is useful whenever rows in a table have a relationship to OTHER rows in that same table, rather than to rows in a different table.

The classic example is an `employees` table with a `manager_id` column that references another row's `id` in the same table — a self-join lets you list every employee alongside their manager's name in one query, by joining the table to itself on that relationship.

Interview tip: None
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
026What is a CROSS JOIN, and what real use case does it actually have?Intermediate+

Easy explanation

A CROSS JOIN produces the CARTESIAN PRODUCT of two tables — every row from the first table combined with EVERY row from the second table, with no join condition at all. If Table A has 3 rows and Table B has 4 rows, a CROSS JOIN produces 12 rows total.

While an accidental CROSS JOIN (forgetting a join condition) is a very common and costly bug, a deliberate CROSS JOIN has real uses — like generating every possible combination of size and color for a product catalog, or generating a complete calendar of dates crossed with every store location for a reporting template.

Interview tip: None
-- deliberately generate every size/color combination
SELECT sizes.name, colors.name
FROM sizes
CROSS JOIN colors;
027What is the difference between UNION and UNION ALL?Advanced+

Easy explanation

UNION combines the result sets of two or more SELECT queries into one, and automatically removes any duplicate rows from the combined result — this deduplication requires MySQL to do extra work (effectively sorting and comparing every row) before returning the final result.

UNION ALL combines the result sets WITHOUT removing duplicates, keeping every row from every query exactly as returned — since it skips the deduplication step entirely, UNION ALL is meaningfully faster. Use UNION ALL whenever you know the result sets won't overlap, or when duplicates are acceptable or even expected — only pay the extra cost of UNION when you genuinely need duplicates removed.

Interview tip: None
-- combined, duplicates removed (slower)
SELECT city FROM customers UNION SELECT city FROM suppliers;
-- combined, duplicates kept (faster)
SELECT city FROM customers UNION ALL SELECT city FROM suppliers;
028Both queries have the same result columns — what rules must UNION-ed queries follow?Advanced+

Easy explanation

Every SELECT statement combined with UNION or UNION ALL must return the SAME NUMBER of columns, in a compatible order — MySQL matches columns POSITIONALLY (the 1st column of query A with the 1st column of query B), not by column name, so mismatched column meanings in the same position is a common and subtle bug.

The columns should also have compatible data types (MySQL will attempt implicit conversion, but mixing very different types, like text and numbers, in the same position can cause unexpected coercion). The FINAL result set's column names come from the FIRST query in the UNION, regardless of what the later queries' columns are named.

Interview tip: None
-- valid: both queries return 2 columns, compatible types
SELECT name, email FROM customers
UNION
SELECT company_name, contact_email FROM suppliers;
-- resulting columns are named 'name' and 'email' (from the FIRST query)
029How do you find rows in one table that do NOT have a matching row in another table (an anti-join)?Advanced+

Easy explanation

This is a very common real-world pattern — 'find customers who have never placed an order', or 'find products that have never been sold' — and MySQL doesn't have a dedicated ANTI-JOIN keyword, so it's expressed using a LEFT JOIN combined with a check for NULL on the right side.

The logic: do a LEFT JOIN so every row from the left table appears regardless of a match, then filter in the WHERE clause for rows where the right table's key came back NULL — meaning no match was found at all. An alternative, often equally efficient, approach uses `WHERE id NOT IN (subquery)` or `WHERE NOT EXISTS (subquery)`.

Interview tip: None
-- customers who have never placed an order
SELECT c.* FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;
030How do you join more than two tables together in a single query?Advanced+

Easy explanation

You simply chain multiple JOIN clauses one after another — each JOIN can reference any table already introduced earlier in the query, not just the very first one, letting you build up a complex combined result across many related tables in one statement.

The ORDER you write joins in doesn't necessarily determine the ORDER the optimizer actually executes them in — the query planner is free to reorder joins internally if it estimates a different order will be more efficient, as long as the logical result stays the same. Readability-wise, it's still good practice to write joins in a sensible, easy-to-follow order (usually following the natural parent-to-child relationship chain).

Interview tip: None
SELECT o.id, c.name AS customer, p.name AS product, oi.quantity
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON oi.product_id = p.id;
SECTION 04

Subqueries

0 / 10 done
VISUAL MODEL

Where a Subquery Can Live Inside a Query

Scalar (SELECT)Row (WHERE)Table (FROM)Correlated (per-row)CTE (WITH)

A subquery is just a query nested inside another query — but WHERE it's placed changes what it's allowed to return and how MySQL evaluates it, from a single value to a whole reusable temporary result set.

031What is a subquery, and what are the main types based on where they appear?Beginner+

Easy explanation

A subquery (or inner query) is a SELECT statement nested inside another SQL statement — it's evaluated first (or once per outer row, for correlated subqueries), and its result is used by the outer (containing) query. Subqueries let you break a complex question into smaller logical steps within a single SQL statement.

The main types, based on WHERE they're placed: a scalar subquery appears where a single value is expected (like in SELECT or WHERE with `=`); a row/column subquery returns multiple rows and is used with `IN`, `ANY`, or `ALL`; a table subquery (a.k.a. derived table) appears in the FROM clause and is treated like a temporary table; and a correlated subquery references a column from the OUTER query, re-evaluating once per outer row.

Interview tip: None
-- scalar subquery: one value
SELECT name, (SELECT AVG(salary) FROM employees) AS company_avg FROM employees LIMIT 1;
-- table subquery: used like a temporary table
SELECT * FROM (SELECT department, AVG(salary) AS avg_sal FROM employees GROUP BY department) dept_avgs;
032What is a scalar subquery, and where can it be used?Beginner+

Easy explanation

A scalar subquery is a subquery that's guaranteed to return EXACTLY ONE value — one row, one column. Because it resolves to a single value, it can be used anywhere a literal value would be valid: in the SELECT list, in a WHERE comparison with `=`, `<`, `>`, or as part of an expression.

If a scalar subquery unexpectedly returns MORE than one row at runtime, MySQL throws an error ('subquery returns more than 1 row') — so scalar subqueries should only be used when you're certain the underlying logic can only ever produce a single result, like an aggregate function with no GROUP BY, or a query filtered down to one specific unique row.

Interview tip: None
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
-- the subquery returns exactly one value: the company-wide average
033What is the difference between IN and EXISTS when used with a subquery?Intermediate+

Easy explanation

`IN` compares a value against the LIST of values returned by a subquery — MySQL typically has to materialize the full list of subquery results before it can check membership against it. `EXISTS` checks only whether the subquery returns ANY rows at all — it doesn't care about the actual VALUES returned, just whether at least one row exists, and can often short-circuit as soon as one matching row is found.

For most modern versions of MySQL, the query optimizer can rewrite one into the other and pick whichever is more efficient for the specific query, so performance differences are often smaller than commonly believed — but `EXISTS` is generally considered safer and more intuitive for CORRELATED subqueries (checking a per-row condition), while `IN` reads more naturally for checking against a fixed, independent list.

Interview tip: None
-- IN: compares against a list of values
SELECT * FROM customers WHERE id IN (SELECT customer_id FROM orders WHERE total > 1000);
-- EXISTS: just checks if any matching row exists (correlated)
SELECT * FROM customers c WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id AND o.total > 1000);
034What is a correlated subquery, and how is it different from a regular (non-correlated) subquery?Intermediate+

Easy explanation

A regular (non-correlated) subquery is completely self-contained and can be run entirely on its own — it's evaluated ONCE, and its result is reused for every row the outer query checks. A correlated subquery references a column from the OUTER query inside its own WHERE clause, meaning it can't be run in isolation — its result genuinely depends on which outer row is currently being evaluated.

Because of this dependency, a correlated subquery conceptually runs ONCE PER ROW of the outer query — for a large outer table, this can be significantly slower than an equivalent non-correlated approach (like a JOIN or a subquery in FROM), which is why correlated subqueries should be used carefully and checked with EXPLAIN if performance matters.

Interview tip: None
-- correlated: 'd.id' references the OUTER query's current row
SELECT d.name,
  (SELECT COUNT(*) FROM employees e WHERE e.department_id = d.id) AS emp_count
FROM departments d;
035What is a derived table (a subquery in the FROM clause), and why must it always have an alias?Intermediate+

Easy explanation

A derived table is a subquery placed in the FROM clause, treated by the outer query exactly like a regular, temporary table for the duration of that query — this is extremely useful for pre-aggregating or pre-filtering data before joining or filtering it further in the outer query.

MySQL REQUIRES every derived table to have an alias (`AS some_name`) — without one, the query fails with a syntax error, since the outer query needs a name to refer to this temporary result set by, exactly the same way every real table needs to be referenceable.

Interview tip: None
SELECT dept_avgs.department, dept_avgs.avg_salary
FROM (
  SELECT department, AVG(salary) AS avg_salary
  FROM employees
  GROUP BY department
) AS dept_avgs   -- alias is required
WHERE dept_avgs.avg_salary > 60000;
036What is a Common Table Expression (CTE), and how is it different from a derived table subquery?Intermediate+

Easy explanation

A CTE, defined with `WITH name AS (SELECT ...)` before the main query, defines a temporary named result set that can be referenced one or more times later in the query — functionally similar to a derived table subquery, but defined ONCE at the top, making the overall query dramatically more readable, especially when the same intermediate result is needed multiple times.

Unlike a plain derived table (which must be written inline exactly where it's used, and duplicated if needed twice), a CTE is defined once and can be referenced by name multiple times in the same query — and MySQL 8.0+ also supports RECURSIVE CTEs, which can reference themselves, essential for querying hierarchical data like an organization chart or a category tree.

Interview tip: None
WITH dept_avgs AS (
  SELECT department, AVG(salary) AS avg_salary
  FROM employees
  GROUP BY department
)
SELECT e.name, e.salary, d.avg_salary
FROM employees e
JOIN dept_avgs d ON d.department = e.department
WHERE e.salary > d.avg_salary;
037How do you use a recursive CTE to query hierarchical data, like an employee/manager org chart?Advanced+

Easy explanation

A recursive CTE has two parts, combined with `UNION ALL`: an ANCHOR member (the starting point — like the top-level manager with no manager themselves) and a RECURSIVE member (which references the CTE's own name, joining it back to the base table to find the next level down), repeating until no more matching rows are found.

This is the standard way to query tree-like or graph-like structures in a relational database — traversing an org chart, expanding a nested category tree, or finding all ancestors/descendants of a node — something that would otherwise require awkward, hard-coded multiple self-joins for each fixed depth level, or a full application-level loop.

Interview tip: None
WITH RECURSIVE org_chart AS (
  SELECT id, name, manager_id, 1 AS level
  FROM employees WHERE manager_id IS NULL      -- anchor: the top
  UNION ALL
  SELECT e.id, e.name, e.manager_id, oc.level + 1
  FROM employees e
  JOIN org_chart oc ON e.manager_id = oc.id     -- recursive: next level down
)
SELECT * FROM org_chart ORDER BY level;
038What is the difference between ANY, ALL, and SOME when comparing a value against a subquery's results?Advanced+

Easy explanation

`= ANY (subquery)` is functionally identical to `IN (subquery)` — TRUE if the value matches AT LEAST ONE row returned by the subquery. `> ANY (subquery)` is TRUE if the value is greater than AT LEAST ONE of the returned values (in other words, greater than the MINIMUM of them).

`> ALL (subquery)` is TRUE only if the value is greater than EVERY single value returned by the subquery (in other words, greater than the MAXIMUM of them) — a much stricter condition than ANY. `SOME` is simply an alias for `ANY` — the two keywords behave identically in MySQL, and `ANY` is more commonly used in practice for clarity.

Interview tip: None
-- greater than at least one department's average (i.e., greater than the smallest average)
SELECT * FROM employees WHERE salary > ANY (SELECT AVG(salary) FROM employees GROUP BY department);
-- greater than EVERY department's average (i.e., greater than the largest average)
SELECT * FROM employees WHERE salary > ALL (SELECT AVG(salary) FROM employees GROUP BY department);
039How do you use a subquery to find the "top N per group" (e.g., the highest-paid employee in each department)?Advanced+

Easy explanation

One classic subquery approach: for each employee, use a correlated subquery to count how many OTHER employees in the same department earn MORE than them — if that count is 0, they're the top earner in their department. This works but can be slow on large tables since the correlated subquery re-runs per row.

A more modern, generally faster approach (MySQL 8.0+) uses a WINDOW FUNCTION instead — `RANK()` or `ROW_NUMBER()` partitioned by department and ordered by salary descending, then filtering for rank 1 in an outer query, since window functions can compute this in a single efficient pass rather than a per-row correlated check.

Interview tip: None
-- correlated subquery approach
SELECT * FROM employees e1
WHERE 0 = (SELECT COUNT(*) FROM employees e2 WHERE e2.department = e1.department AND e2.salary > e1.salary);

-- modern window function approach (MySQL 8.0+)
SELECT * FROM (
  SELECT *, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk
  FROM employees
) ranked WHERE rnk = 1;
040What is the performance risk of a correlated subquery in the SELECT list, and how would you rewrite it as a JOIN?Advanced+

Easy explanation

A correlated subquery placed in the SELECT list (like computing an order count per customer inline) is conceptually re-executed ONCE PER ROW returned by the outer query — for a large result set, this means running that inner query potentially thousands or millions of times, which can be dramatically slower than an equivalent set-based operation.

In most cases, this pattern can be rewritten as a LEFT JOIN combined with GROUP BY, which lets MySQL compute the aggregation in one efficient pass over the joined data instead of one query execution per outer row — this rewrite is one of the most common, high-impact query optimizations in real-world MySQL performance tuning.

Interview tip: None
-- slow: correlated subquery runs once per customer row
SELECT c.name, (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) AS order_count
FROM customers c;

-- faster: single pass with JOIN + GROUP BY
SELECT c.name, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name;
SECTION 05

Aggregate Functions, Views, Indexes & Optimization

0 / 10 done
VISUAL MODEL

GROUP BY: Collapsing Rows Into Summary Groups

BEFORE: individual rows Sales | 100 Sales | 150 HR | 80 Sales | 120 HR | 90 AFTER: GROUP BY department Sales: SUM = 370 HR: SUM = 170

GROUP BY collapses many rows sharing the same value into one summary row per group, and an aggregate function (SUM, COUNT, AVG) computes one value across all rows in each group.

041What are the main aggregate functions in MySQL (COUNT, SUM, AVG, MIN, MAX)?Beginner+

Easy explanation

Aggregate functions compute a SINGLE summary value from MULTIPLE rows. COUNT(*) counts the total number of rows (including ones with NULLs), while COUNT(column) counts only rows where that specific column is NOT NULL — this distinction matters and is a common gotcha. SUM adds up a numeric column's values, and AVG computes their average.

MIN and MAX return the smallest and largest values in a column respectively, and they work on non-numeric types too (like finding the earliest or latest date, or the alphabetically first/last name). Without a GROUP BY clause, an aggregate function computes ONE value across the ENTIRE table.

Interview tip: None
SELECT
  COUNT(*) AS total_rows,
  COUNT(discount) AS rows_with_discount,
  SUM(total) AS revenue,
  AVG(total) AS avg_order,
  MIN(order_date) AS first_order,
  MAX(order_date) AS latest_order
FROM orders;
042How does GROUP BY work, and what rule governs which columns can appear in the SELECT list?Beginner+

Easy explanation

GROUP BY collapses multiple rows that share the same value in the grouped column(s) into a single summary row per group — commonly paired with an aggregate function to compute something (a total, a count, an average) FOR EACH group separately, rather than across the whole table at once.

The rule: every column in the SELECT list must either be part of the GROUP BY clause itself, or be wrapped in an aggregate function — MySQL (in strict SQL mode) will reject a query that selects a non-grouped, non-aggregated column, since it wouldn't have one single, unambiguous value to display for that group.

Interview tip: None
SELECT department, COUNT(*) AS emp_count, AVG(salary) AS avg_salary
FROM employees
GROUP BY department;
-- 'department' is grouped; count/avg are aggregated — both are valid here
043How do you count how many rows fall into each of several categories in a single query?Intermediate+

Easy explanation

GROUP BY combined with COUNT(*) is the standard pattern — grouping by the category column and counting rows within each group gives you a breakdown across every distinct value that column has, all in one pass over the data instead of running a separate COUNT query per category.

This same pattern extends naturally to any other aggregate — SUM per category, AVG per category, MIN/MAX per category — and can be combined with ORDER BY on the aggregate result to rank categories by size, which is one of the most common patterns in real-world reporting queries.

Interview tip: None
SELECT status, COUNT(*) AS how_many
FROM orders
GROUP BY status
ORDER BY how_many DESC;
044What is a MySQL VIEW, and what are its practical uses?Intermediate+

Easy explanation

A VIEW is a saved, named SELECT query that behaves like a virtual table — you can query it just like a regular table, but MySQL runs the underlying SELECT statement fresh every time the view is queried, so it's always up to date with the real underlying tables.

Practical uses: simplifying a complex, frequently-repeated query so users just query a simple view name instead of retyping a long JOIN every time; restricting access by exposing only specific columns/rows of a sensitive table through a view (giving read access to the view without giving direct access to the underlying table); and providing a stable interface that hides underlying schema changes from applications that only query through the view.

Interview tip: None
CREATE VIEW active_customers AS
SELECT id, name, email FROM customers WHERE status = 'active';
-- now query it like a regular table:
SELECT * FROM active_customers WHERE name LIKE 'A%';
045Can you INSERT, UPDATE, or DELETE through a VIEW? What restrictions apply?Intermediate+

Easy explanation

Yes, MySQL allows you to modify data through a view in many cases, and the change gets applied to the actual underlying base table — but this only works for 'simple' (updatable) views: ones based on a single table, without GROUP BY, aggregate functions, DISTINCT, UNION, or subqueries in the SELECT list.

A view built from a JOIN, an aggregate query, or anything more complex is generally NOT updatable (or only partially updatable in narrow cases) — trying to INSERT/UPDATE/DELETE through such a view either fails outright or behaves in ways that can be surprising, so most teams treat views as read-only reporting tools rather than a general-purpose way to write data.

Interview tip: None
CREATE VIEW simple_view AS SELECT id, name, email FROM customers;
UPDATE simple_view SET email = 'new@email.com' WHERE id = 1; -- works: updates the real customers table
-- but a view with a JOIN or GROUP BY generally can't be updated this way
046What is the EXPLAIN statement, and what key columns should you look at to diagnose a slow query?Intermediate+

Easy explanation

EXPLAIN shows you the execution plan MySQL's optimizer chose for a query WITHOUT actually running it (EXPLAIN ANALYZE, in MySQL 8.0.18+, actually executes it and shows real timing too) — this is the single most important diagnostic tool for understanding why a query is slow.

Key columns to check: `type` (the join/access type — `ALL` means a full table scan, which is usually bad on a large table; `ref`/`range`/`const` mean an index is being used effectively), `key` (which index, if any, was actually used), `rows` (the estimated number of rows MySQL expects to examine — a huge number here on a query that should be selective is a red flag), and `Extra` (look out for 'Using filesort' or 'Using temporary', which indicate extra, potentially expensive work).

Interview tip: None
EXPLAIN SELECT * FROM orders WHERE customer_id = 42 ORDER BY created_at DESC;
-- check: type (want ref/range, not ALL), key (is an index being used?), rows (how many examined?)
047What is a covering index, and why can it make a query fast without even touching the actual table data?Advanced+

Easy explanation

A covering index is an index that contains EVERY column a specific query needs — both the columns being filtered/sorted on AND the columns being selected — meaning MySQL can answer the entire query by reading ONLY the index itself, never needing to look up the actual row in the table at all (this extra lookup step is sometimes called a 'bookmark lookup').

This can be a significant performance win because reading from a smaller, more compact index structure is generally faster than reading full table rows, especially when a query only needs a few narrow columns from a table with many wide columns — you can check whether this is happening by looking for 'Using index' in EXPLAIN's Extra column.

Interview tip: None
-- if this index exists:
CREATE INDEX idx_covering ON orders(customer_id, order_date, total);
-- this query can be fully answered FROM THE INDEX ALONE (a covering index):
SELECT order_date, total FROM orders WHERE customer_id = 42;
048Why does a leading wildcard in LIKE (e.g., '%searchterm') prevent MySQL from using a B-tree index?Advanced+

Easy explanation

A standard B-tree index stores values in sorted order, which lets MySQL efficiently jump to and scan a range of values that share a known PREFIX — `LIKE 'John%'` can use an index because MySQL can jump straight to entries starting with 'John' in the sorted structure.

`LIKE '%John'` (a leading wildcard) can't use a standard B-tree index at all, because the matching characters could start ANYWHERE in the string — there's no useful sorted prefix to jump to, forcing MySQL to check every single row's value individually (a full table scan or full index scan). For genuine full-text 'contains anywhere' search needs, a dedicated FULLTEXT index is the appropriate tool instead of LIKE with a leading wildcard.

Interview tip: None
-- CAN use an index (prefix match):
WHERE name LIKE 'John%'
-- CANNOT use a standard B-tree index (no fixed prefix):
WHERE name LIKE '%John%'
-- for genuine contains-search at scale, use FULLTEXT instead:
SELECT * FROM articles WHERE MATCH(body) AGAINST('database performance');
049What is the difference between a stored procedure and a stored function in MySQL?Advanced+

Easy explanation

A stored procedure is a saved, reusable block of SQL logic that you INVOKE with `CALL` — it can accept input parameters, output parameters, perform multiple statements including inserts/updates/deletes, and doesn't necessarily have to return any value at all.

A stored function is similar but MUST return exactly one value, and (unlike a procedure) can be used DIRECTLY inside a regular SQL expression, like in a SELECT list or a WHERE clause, the same way you'd use a built-in function like `UPPER()`. Procedures are generally used for encapsulating multi-step business logic or batch operations; functions are used for reusable calculations you want to embed inline within other queries.

Interview tip: None
DELIMITER //
CREATE FUNCTION get_tax(amount DECIMAL(10,2)) RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
  RETURN amount * 0.08;
END //
DELIMITER ;
-- used inline, like a built-in function:
SELECT total, get_tax(total) AS tax FROM orders;
050What is a MySQL trigger, and what is a real-world use case for one?Advanced+

Easy explanation

A trigger is a piece of SQL logic that MySQL automatically executes in response to a specific event (BEFORE or AFTER an INSERT, UPDATE, or DELETE) on a specific table — you never call a trigger directly; it fires automatically whenever the matching event happens.

A common real-world use: maintaining an audit log automatically — an AFTER UPDATE trigger on a `salaries` table could automatically insert a row into a `salary_history` table every time a salary changes, guaranteeing the history is captured consistently regardless of which application code path performed the update, rather than relying on every single code path remembering to log it manually.

Interview tip: None
CREATE TRIGGER log_salary_change
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
  IF OLD.salary != NEW.salary THEN
    INSERT INTO salary_history (employee_id, old_salary, new_salary, changed_at)
    VALUES (OLD.id, OLD.salary, NEW.salary, NOW());
  END IF;
END;
MySQL Queries &amp; Subqueries Guide Field Guide · Compiled & Designed by Tanveer Qureshie