SQL & Database — 17 Topics · MySQL

SQL from Basics to Advanced.

Complete SQL reference — database design, queries, joins, window functions, indexes, normalization, and optimization. 40+ interview questions tagged for FAANG and startups. Free.

MySQL syntax Window functions covered JOIN visuals included Normalization guide
17TopicsBasics to advanced
40+Interview QsFAANG & Startup
5JOIN TypesWith visual guide
3NFNormalization1NF → 2NF → 3NF
+MongoDBNoSQLSQL vs NoSQL guide
01

What is a Database?

A database is a structured collection of related data. In MySQL, a database holds one or more tables — think of it as a folder, where each table is a file inside it.

AnalogyDatabaseTableRow
File SystemFolderFileLine in the file
ExcelWorkbookSheetExcel row
Real worldCompany recordsDepartment listOne employee
Create & Use Database
SQL
CREATE DATABASE techforge_db;
USE techforge_db;
SHOW DATABASES;   -- List all databases
DROP DATABASE techforge_db;  --  Permanent!
After creating the database, use USE database_name; or set it as default schema in your SQL client before running table queries.
02

DDL, DML, DCL & TCL

Every SQL statement belongs to one of four sub-languages. Interviewers ask this to check you know which statements can be rolled back and which cannot.

FamilyStands forStatementsRollback?
DDLData DefinitionCREATE, ALTER, DROP, TRUNCATENo — auto-commits in MySQL and Oracle
DMLData ManipulationINSERT, UPDATE, DELETE, SELECTYes
DCLData ControlGRANT, REVOKENo
TCLTransaction ControlCOMMIT, ROLLBACK, SAVEPOINT
Why it matters: TRUNCATE is DDL, so it cannot be rolled back and it resets identity counters. DELETE is DML — slower, logs every row, but recoverable inside a transaction.
03

MySQL Data Types

Data TypeDescriptionExampleInterview Note
INTWhole numbersage INTUse BIGINT for IDs at scale
VARCHAR(n)Variable-length string (up to n chars)name VARCHAR(100)Prefer over CHAR for varying lengths
TEXTLong text (65,535 chars)bio TEXTCannot be indexed directly
DECIMAL(p,s)Exact numeric (p digits, s decimal)price DECIMAL(10,2)Use for money — never FLOAT
DATEDate only (YYYY-MM-DD)dob DATEDATETIME includes time component
TIMESTAMPDate + time, auto-sets to nowcreated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMPStored in UTC
BOOLEANTRUE / FALSE (stored as 1/0)is_active BOOLEAN DEFAULT TRUEAlias for TINYINT(1)
ENUMOne value from a defined setgender ENUM('Male','Female','Other')Validates at DB level
Interview tip: FLOAT/DOUBLE store approximate values. Always use DECIMAL for financial data to avoid rounding errors.
04

Constraints

Constraints enforce rules on column data — they protect data integrity at the database level, not just the application level.

ConstraintPurposeExample
PRIMARY KEYUniquely identifies each row — NOT NULL + UNIQUEid INT PRIMARY KEY
AUTO_INCREMENTAuto-generates sequential integersid INT AUTO_INCREMENT
NOT NULLColumn must always have a valuename VARCHAR(100) NOT NULL
UNIQUEAll values must be distinct (allows NULL)email VARCHAR(100) UNIQUE
DEFAULTUses this value if none providedis_active BOOLEAN DEFAULT TRUE
CHECKValues must satisfy a conditionCHECK (salary > 0)
FOREIGN KEYReferences a primary key in another tableFOREIGN KEY(user_id) REFERENCES users(id)
PRIMARY KEY vs UNIQUE: PRIMARY KEY = NOT NULL + UNIQUE, one per table. UNIQUE allows NULL values and a table can have multiple UNIQUE constraints.
05

Create Table

SQL
CREATE TABLE users (
    id            INT AUTO_INCREMENT PRIMARY KEY,
    name          VARCHAR(100) NOT NULL,
    email         VARCHAR(100) UNIQUE NOT NULL,
    gender        ENUM('Male', 'Female', 'Other'),
    salary        DECIMAL(10,2) DEFAULT 0,
    date_of_birth DATE,
    is_active     BOOLEAN DEFAULT TRUE,
    created_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Verify structure
DESCRIBE users;

-- Remove table entirely (irreversible)
DROP TABLE IF EXISTS users;
06

Alter Table

SQL
-- Add a column
ALTER TABLE users ADD COLUMN phone VARCHAR(15);

-- Drop a column
ALTER TABLE users DROP COLUMN phone;

-- Modify column type
ALTER TABLE users MODIFY COLUMN name VARCHAR(150) NOT NULL;

-- Rename column (MySQL 8.0+)
ALTER TABLE users RENAME COLUMN name TO full_name;

-- Rename table
RENAME TABLE users TO members;
07

Insert Data

Best practice: Always specify column names explicitly. It makes inserts resilient to future schema changes and easier to read.
SQL
-- Single row
INSERT INTO users (name, email, gender, salary, date_of_birth)
VALUES ('Alice', 'alice@example.com', 'Female', 85000, '1995-04-12');

-- Multiple rows at once (more efficient)
INSERT INTO users (name, email, gender, salary, date_of_birth)
VALUES
    ('Bob',     'bob@example.com',     'Male',   72000, '1990-11-23'),
    ('Charlie', 'charlie@example.com', 'Other',  91000, '1988-02-17'),
    ('Divya',   'divya@example.com',   'Female', 67000, '2000-08-09');
08

SELECT Query

SQL
-- Basic SELECT
SELECT * FROM users;
SELECT name, email, salary FROM users;

-- WHERE with operators
SELECT * FROM users WHERE gender = 'Female';
SELECT * FROM users WHERE salary BETWEEN 60000 AND 90000;
SELECT * FROM users WHERE name LIKE 'A%';   -- starts with A
SELECT * FROM users WHERE email IS NOT NULL;

-- AND / OR / NOT
SELECT * FROM users WHERE salary > 70000 AND gender = 'Male';
SELECT * FROM users WHERE department IN ('Eng', 'Design');

-- ORDER and LIMIT
SELECT * FROM users ORDER BY salary DESC LIMIT 10;
SELECT * FROM users ORDER BY name ASC LIMIT 5 OFFSET 10; -- pagination

-- Aliases
SELECT name AS full_name, salary * 12 AS annual_salary FROM users;
09

UPDATE & DELETE

SQL
-- UPDATE specific rows
UPDATE users SET salary = 95000 WHERE id = 1;
UPDATE users SET salary = salary * 1.10 WHERE gender = 'Female'; -- 10% raise

-- DELETE specific rows
DELETE FROM users WHERE id = 3;
DELETE FROM users WHERE is_active = FALSE;

-- TRUNCATE: delete all rows, keep table structure (faster than DELETE)
TRUNCATE TABLE users;
Always use WHERE with UPDATE and DELETE unless you intentionally want to modify every row. Run a SELECT with the same WHERE first to verify which rows are affected.
10

SQL Functions

Aggregate Functions
SQL
SELECT COUNT(*)                    AS total_users    FROM users;
SELECT COUNT(DISTINCT department)  AS dept_count      FROM users;
SELECT MIN(salary), MAX(salary)    AS max_sal        FROM users;
SELECT SUM(salary)                  AS total_payroll  FROM users;
SELECT ROUND(AVG(salary), 2)        AS avg_salary     FROM users;
String & Date Functions
SQL
-- String functions
SELECT UPPER(name), LOWER(email), LENGTH(name) FROM users;
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;
SELECT SUBSTRING(email, 1, INSTR(email, '@') - 1) AS username FROM users;

-- Date functions
SELECT YEAR(date_of_birth), MONTH(date_of_birth) FROM users;
SELECT DATEDIFF(NOW(), date_of_birth) / 365 AS age FROM users;
SELECT DATE_FORMAT(created_at, '%Y-%m') AS month FROM users;
11

NULLs & the CASE Expression

NULL is not a value — it is the absence of one. That is why it never equals anything, not even itself, and why comparisons against it need IS NULL rather than =.

SQL
SELECT NULL = NULL;       -- NULL, not true
SELECT * FROM users WHERE deleted_at IS NULL;   -- correct

-- Aggregates skip NULLs; COUNT(*) does not
SELECT COUNT(*), COUNT(phone), AVG(score) FROM users;
FunctionDoesPortable?
COALESCE(a, b, c)Returns the first non-NULL argumentStandard SQL — use this
ISNULL(a, b)Two arguments onlySQL Server
NVL(a, b)Two arguments onlyOracle
NULLIF(a, b)NULL when a equals b, else aStandard — good for guarding division

CASE is SQL's if/else, usable anywhere an expression is allowed — in SELECT, in ORDER BY, even inside an aggregate to count conditionally.

SQL
SELECT name,
       CASE WHEN score >= 90 THEN 'A'
            WHEN score >= 80 THEN 'B'
            ELSE 'C'
       END AS grade,
       COUNT(CASE WHEN status = 'paid' THEN 1 END) AS paid_orders
FROM students LEFT JOIN orders USING(id)
GROUP BY name, grade;
A CASE with no matching WHEN and no ELSE returns NULL — a common source of silently missing rows once that column is filtered on later.
12

Window Functions

Window functions perform calculations across rows related to the current row — without collapsing them into groups like GROUP BY does. They're one of the most-tested advanced SQL topics at FAANG companies.

COMMON WINDOW FUNCTIONS
ROW_NUMBER()RANK()DENSE_RANK() LAG()LEAD()NTILE() SUM() OVERAVG() OVERFIRST_VALUE()
SQL
-- ROW_NUMBER: unique rank per partition
SELECT name, department, salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num
FROM users;

-- RANK vs DENSE_RANK: ties behave differently
-- RANK skips numbers after ties; DENSE_RANK does not
SELECT name, salary,
    RANK()       OVER (ORDER BY salary DESC) AS rank_val,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank_val
FROM users;

-- LAG/LEAD: compare current row with previous/next
SELECT name, salary,
    LAG(salary)  OVER (ORDER BY salary) AS prev_salary,
    LEAD(salary) OVER (ORDER BY salary) AS next_salary
FROM users;

-- Running total
SELECT name, salary,
    SUM(salary) OVER (ORDER BY id ROWS UNBOUNDED PRECEDING) AS running_total
FROM users;
Key difference: GROUP BY collapses rows into one per group. Window functions keep all rows and add a calculated column alongside them.
13

Transactions & ACID

A transaction is a sequence of SQL statements treated as a single unit — either all succeed or all fail (atomicity).

ACID PropertyMeaning
AtomicityAll operations succeed or all are rolled back
ConsistencyDB moves from one valid state to another
IsolationConcurrent transactions don't interfere
DurabilityCommitted data persists even after crashes
SQL
START TRANSACTION;

UPDATE accounts SET balance = balance - 500 WHERE id = 1;  -- debit
UPDATE accounts SET balance = balance + 500 WHERE id = 2;  -- credit

-- If both succeed:
COMMIT;

-- If something fails, undo everything:
ROLLBACK;
14

Isolation Levels & Deadlocks

The I in ACID is a dial, not a switch. Weaker isolation lets more transactions run at once and admits specific anomalies; stronger isolation removes them at the cost of blocking.

LevelDirty readNon-repeatable readPhantom read
READ UNCOMMITTEDPossiblePossiblePossible
READ COMMITTEDPreventedPossiblePossible
REPEATABLE READPreventedPreventedPossible*
SERIALIZABLEPreventedPreventedPrevented

A dirty read sees another transaction's uncommitted change. A non-repeatable read gets a different value when it reads the same row twice. A phantom read gets a different set of rows when it repeats the same query. (*InnoDB's REPEATABLE READ blocks phantoms too, via next-key locking — the standard does not require that.)

SQL
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
SAVEPOINT after_debit;              -- a partial rollback point
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
ROLLBACK TO after_debit;            -- undo the credit, keep the debit
COMMIT;
Deadlock: two transactions each hold a lock the other needs, so neither can proceed. The database detects the cycle and kills one with an error. Avoid them by locking rows in a consistent order everywhere, keeping transactions short, and touching the smallest set of rows you can — then retry the loser, because under load a deadlock is normal rather than exceptional.
15

Primary & Foreign Keys

PRIMARY KEY
  • Uniquely identifies each row
  • Cannot be NULL
  • Only one per table
  • Often combined with AUTO_INCREMENT
FOREIGN KEY
  • References a PK in another table
  • Enforces referential integrity
  • Can be NULL (optional relationship)
  • Multiple FKs allowed per table
SQL
CREATE TABLE orders (
    id         INT AUTO_INCREMENT PRIMARY KEY,
    user_id    INT NOT NULL,
    total      DECIMAL(10,2),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- ON DELETE CASCADE: deleting a user also deletes their orders
-- ON DELETE SET NULL: sets user_id to NULL when user is deleted

A natural key is data that is already unique and meaningful — an ISBN, an email. A surrogate key is a meaningless identifier the database generates, usually an auto-increment or identity column. Surrogates win in practice because business meaning changes: people change email addresses, and a key that changes has to be updated everywhere it was referenced.

${table(['Term', 'Meaning'], [ ['Composite key', 'A primary key made of two or more columns together'], ['Candidate key', 'Any column set that could serve as the primary key'], ['Surrogate key', 'A generated identifier with no business meaning'], ['Orphan record', 'A row whose foreign key points at something that no longer exists'], ])}

Referential integrity is what prevents orphans, and ON DELETE decides what happens when a parent goes away.

${codeBlock(`${kw('CREATE TABLE')} orders ( id ${kw('INT')} ${kw('PRIMARY KEY')} ${kw('AUTO_INCREMENT')}, ${cm('-- surrogate key')} user_id ${kw('INT')} ${kw('NOT NULL')}, ${kw('FOREIGN KEY')} (user_id) ${kw('REFERENCES')} users(id) ${kw('ON DELETE CASCADE')} ${cm('-- delete the orders too')} ${kw('ON UPDATE CASCADE')} ${cm('-- follow a changed parent id')} ); ${cm('-- Alternatives: RESTRICT (refuse), SET NULL (orphan it deliberately)')}`)}
ON DELETE CASCADE is convenient and dangerous: one delete can silently remove rows several tables away. Use it where the child genuinely cannot exist without the parent, and RESTRICT everywhere else so the deletion has to be deliberate.
16

SQL JOINs

JOINs combine rows from two or more tables based on a related column. The most tested SQL concept in interviews.

INNER JOIN
Only rows matching in both tables
LEFT JOIN
All from left + matching from right (NULL if no match)
RIGHT JOIN
All from right + matching from left (NULL if no match)
FULL JOIN
All rows from both tables (NULL where no match)
SQL
-- INNER JOIN: users who have placed orders
SELECT u.name, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

-- LEFT JOIN: ALL users, with orders if they exist
SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

-- Find users with NO orders
SELECT u.name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

-- SELF JOIN: find users in the same department
SELECT a.name, b.name, a.department
FROM users a
JOIN users b ON a.department = b.department AND a.id != b.id;
17

UNION & Subqueries

SQL
-- UNION: combines results, removes duplicates
SELECT name FROM users
UNION
SELECT name FROM admin_users;

-- UNION ALL: keeps duplicates (faster)
SELECT name FROM users
UNION ALL
SELECT name FROM admin_users;

-- Subquery in WHERE
SELECT name, salary FROM users
WHERE salary > (SELECT AVG(salary) FROM users);

-- Subquery in FROM (derived table)
SELECT dept, avg_sal
FROM (
    SELECT department AS dept, AVG(salary) AS avg_sal
    FROM users GROUP BY department
) dept_avg
WHERE avg_sal > 80000;
18

Advanced Query Patterns

Four shapes that come up constantly once the basics are behind you.

Correlated subquery — an inner query that references the outer row, so it re-evaluates per row. Readable, but often slower than the join or window function that replaces it.

SQL
-- Correlated: runs once per employee
SELECT name, salary FROM employees e
WHERE salary > (SELECT AVG(salary) FROM employees WHERE dept = e.dept);

-- Same answer with a window function, one pass
SELECT name, salary FROM (
  SELECT name, salary, AVG(salary) OVER (PARTITION BY dept) AS dept_avg
  FROM employees
) t WHERE salary > dept_avg;

Recursive CTE — a query that refers to itself, which is how you walk hierarchical data (org charts, category trees, bills of materials) to arbitrary depth.

SQL
WITH RECURSIVE chain AS (
  SELECT id, name, manager_id, 1 AS depth
  FROM employees WHERE manager_id IS NULL      -- anchor: the root
  UNION ALL
  SELECT e.id, e.name, e.manager_id, c.depth + 1
  FROM employees e JOIN chain c ON e.manager_id = c.id  -- recursive step
)
SELECT * FROM chain ORDER BY depth;

Lateral join (LATERAL, or CROSS APPLY in SQL Server) lets the right-hand subquery see columns from the left — the standard way to fetch "the top N rows per group".

SQL
SELECT c.name, o.total, o.placed_at
FROM customers c
CROSS JOIN LATERAL (
  SELECT total, placed_at FROM orders
  WHERE customer_id = c.id ORDER BY placed_at DESC LIMIT 3
) o;

Pivot and unpivot rotate rows into columns and back. PIVOT exists in SQL Server and Oracle; elsewhere the portable form is conditional aggregation, which is the CASE-inside-SUM pattern above.

Dynamic SQL — building statement text at runtime — is what you need when the pivot columns are not known in advance. It is also the fastest route to an injection hole: never concatenate user input into it. Bind values as parameters, and allow-list any identifier that has to be interpolated.
19

Indexes & Performance

An index is a data structure (typically a B-Tree) that speeds up data retrieval at the cost of extra storage and slower writes. Think of it like a book's index — you find the page number without reading the whole book.

SQL
-- Create index on frequently queried column
CREATE INDEX idx_email ON users(email);

-- Composite index: column order matters!
CREATE INDEX idx_dept_salary ON users(department, salary);

-- EXPLAIN: see if index is being used
EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com';

-- Drop index
DROP INDEX idx_email ON users;
Don't over-index. Each index slows down INSERT/UPDATE/DELETE because the index must be updated too. Index columns that appear in WHERE, JOIN ON, and ORDER BY clauses.
20

Reading the Execution Plan

The optimiser turns your SQL into a plan: which indexes to use, which join algorithm, in what order. EXPLAIN shows the plan it chose, and EXPLAIN ANALYZE runs the query and shows what actually happened alongside the estimate.

SQL
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id)
FROM users u JOIN orders o ON o.user_id = u.id
WHERE u.created_at > '2026-01-01'
GROUP BY u.name;
What you seeMeansUsually fine when
Seq / full table scanEvery row was readThe table is small, or you really do want most of it
Index scanThe index was walkedA range query
Index seek / unique scanStraight to the rowsAlmost always what you want
Nested loop joinFor each left row, look up the rightThe left side is small and the right is indexed
Hash joinBuild a hash of one side, probe with the otherBoth sides are large, no useful index

Two numbers matter most. Cost is the optimiser's estimate in arbitrary units — useful for comparing two plans for the same query, meaningless across queries. The gap between estimated and actual rows is the real signal: when they differ by orders of magnitude, statistics are stale and every choice downstream was made on bad information. Refreshing statistics fixes more "slow query" tickets than rewriting the SQL does.

A tuning pass, in order: confirm the query is the problem, read the plan for the biggest actual-time node, check whether an index would turn a scan into a seek, and only then consider rewriting. Databases cache plans so repeated statements skip planning — one reason parameterised queries are faster as well as safer, since a literal baked into the text produces a new plan every time.

Optimizer hints force a particular index or join. They are a last resort: a hint that is right today locks the plan against a future where the data has grown or the statistics have changed. Fix the statistics or the index first.

An indexing strategy follows from all this: index the columns you filter and join on, put the most selective column first in a composite index, cover the query where you can so the index alone answers it, and drop indexes nothing uses — each one is paid for on every insert and update.

21

GROUP BY & HAVING

WHERE vs HAVING: WHERE filters rows before grouping. HAVING filters groups after aggregation. You cannot use aggregate functions in WHERE.
SQL
-- Count employees per department
SELECT department, COUNT(*) AS headcount
FROM users
GROUP BY department
ORDER BY headcount DESC;

-- HAVING: only departments with avg salary > 75k
SELECT department, ROUND(AVG(salary),2) AS avg_sal
FROM users
GROUP BY department
HAVING AVG(salary) > 75000
ORDER BY avg_sal DESC;

-- WHERE + GROUP BY + HAVING together
SELECT department, COUNT(*) AS cnt
FROM users
WHERE is_active = TRUE              -- filter rows first
GROUP BY department                 -- then group
HAVING COUNT(*) > 5               -- then filter groups
ORDER BY cnt DESC;                  -- then sort
22

Normalization

Normalization is the process of organizing a database to reduce redundancy and improve data integrity by dividing data into related tables.

1NF — FIRST NORMAL FORM
Eliminate repeating groups
Each column must hold atomic (indivisible) values. No arrays or comma-separated lists in a single cell. Each row must be unique.
2NF — SECOND NORMAL FORM
Remove partial dependencies
Must be in 1NF. Every non-key column must depend on the whole primary key — not just part of it. Relevant when PK is composite.
3NF — THIRD NORMAL FORM
Remove transitive dependencies
Must be in 2NF. Non-key columns must depend only on the primary key — not on other non-key columns. Move transitive data to its own table.
Interview tip: Most production schemas target 3NF. Sometimes denormalization is intentional for read-performance — know when to justify it.
23

Views & Materialized Views

A view is a stored query that behaves like a table. It holds no data of its own — every read re-runs the underlying query — so it is a way to name a complicated join once and hide columns a caller should not see.

SQL
CREATE VIEW active_customers AS
SELECT id, name, email FROM customers WHERE status = 'active';

SELECT * FROM active_customers;   -- queried like a table

-- Materialized: stores the result, must be refreshed
CREATE MATERIALIZED VIEW sales_by_month AS
SELECT date_trunc('month', sold_at) AS m, SUM(total) FROM orders GROUP BY 1;

REFRESH MATERIALIZED VIEW sales_by_month;
ViewMaterialized view
StorageNone — query re-runs each timeResult is stored on disk
FreshnessAlways currentStale until refreshed
Read costCost of the underlying queryCost of reading a table
Use whenSimplifying access, hiding columnsExpensive aggregate read far more often than it changes
Schema binding ties a view to the tables it reads, so the database refuses a DROP or ALTER that would break it. Worth turning on for views other systems depend on.
24

Stored Procedures, Functions & Triggers

SQL can hold logic as well as data. The three forms differ in what invokes them and what they are allowed to do.

Called byReturnsCan modify data
Stored procedureExplicitly, with CALLNothing, or output paramsYes
Function (UDF)Inside an expressionA single value, or a tableUsually no
TriggerThe database, on an eventNothingYes
SQL
-- Procedure: called explicitly
CREATE PROCEDURE give_raise(IN emp_id INT, IN pct DECIMAL(4,2))
BEGIN
  UPDATE employees SET salary = salary * (1 + pct) WHERE id = emp_id;
END;

CALL give_raise(42, 0.10);

-- Trigger: the database calls it for you
CREATE TRIGGER audit_salary AFTER UPDATE ON employees
FOR EACH ROW
  INSERT INTO salary_audit(emp_id, old_val, new_val, changed_at)
  VALUES(OLD.id, OLD.salary, NEW.salary, NOW());

A scalar function returns one value per call (UPPER, ROUND); a table-valued function returns a result set you can join against. A cursor walks a result set one row at a time — occasionally necessary for row-by-row procedural work, but almost always slower than the set-based statement that would replace it.

Triggers are invisible logic. They fire without appearing at the call site, which makes them hard to debug and easy to forget during a migration. Reach for them for audit trails and integrity that the schema cannot express — not for business rules.
25

SQL Injection

If user input is concatenated into a query string, the user can end your statement and start their own. It remains one of the most exploited web vulnerabilities, and the fix has been known for decades.

SQL
-- Vulnerable: input becomes part of the statement
query = "SELECT * FROM users WHERE email = '" + input + "'";

-- input:  ' OR '1'='1
SELECT * FROM users WHERE email = '' OR '1'='1';   -- returns every row

-- Safe: the value is sent separately from the statement
SELECT * FROM users WHERE email = ?;

The defence is parameterised queries (prepared statements): the SQL text is parsed once, and the value can never be read as syntax however it is spelled. Escaping input by hand is not equivalent — it is a blocklist, and blocklists lose. Add least-privilege database accounts so an application that only reads cannot DROP, and validate input for shape as a second layer, not the first.

An ORM parameterises for you — but only while you use its query builder. The moment you drop to raw SQL with string interpolation, the protection is gone.
26

Partitioning, Sharding & Replication

When one table or one machine stops being enough, these are the four moves — and the vocabulary interviewers expect you to keep straight.

TechniqueWhat it splitsAcross
PartitioningOne table into segments by key or rangeOne database
ShardingThe dataset into independent databasesMany servers
ReplicationNothing — it copiesA primary and its replicas
ClusteringNothing — it groups serversNodes behind one endpoint

Partitioning lets the planner skip whole segments that cannot match — partition pruning — so a query filtered by date reads one month rather than ten years. Sharding buys write throughput but costs you cross-shard joins and transactions. Replication gives read scale-out and a failover target, at the price of replica lag: a read straight after a write may not see it.

OLTP vs OLAP: OLTP systems serve many small, concurrent reads and writes and are normalised for integrity. OLAP systems answer few, large aggregate queries and are denormalised (star schemas, columnar storage) for scan speed. ETL is the pipeline between them — extract from the OLTP source, transform to the analytical shape, load into the warehouse. CDC streams the changes instead of re-copying, so the warehouse tracks the source continuously.
27

JSON, Text Search & Collation

Relational databases stopped being purely relational some time ago. Three capabilities come up often enough to know by name.

JSON columns store a document in a single field and let you query inside it. Postgres has json and jsonb (binary, indexable — use this one); MySQL has JSON. The older XML type does the same job with XPath, and survives mostly in enterprise systems.

SQL
ALTER TABLE events ADD COLUMN payload JSONB;

-- query inside the document
SELECT id FROM events WHERE payload->>'type' = 'signup';

-- and index it, or every read is a full scan
CREATE INDEX idx_events_type ON events ((payload->>'type'));
A JSON column is the right home for genuinely variable data — a webhook payload, per-tenant settings. It is the wrong home for fields you filter and join on every day: those want real columns, with real types and real constraints behind them.

Full-text search indexes words rather than whole strings, so it can rank by relevance and handle stemming — LIKE '%term%' can do neither, and cannot use a normal index at all. Postgres has tsvector and tsquery; MySQL has FULLTEXT indexes with MATCH ... AGAINST.

Collation decides how text sorts and compares: whether 'a' equals 'A', where accented characters fall, which language's alphabet order applies. It is set per database, table or column, and a mismatch between two columns is a classic cause of a join that refuses to use its index.

28

Quick Quiz

Five questions on the parts interviewers reach for first: joins, filtering, indexes and normal forms.

1. A LEFT JOIN returns which rows?
2. You want departments whose headcount exceeds 10. Which clause filters that?
3. What does an index cost you?
4. A column stores "php,mysql,react" as a single comma-separated value. Which normal form does that break?
5. You create INDEX idx_dept_salary ON users(department, salary). Which query can use it?
Ready to test your SQL knowledge?
40+ SQL interview questions — joins, window functions, subqueries, indexes — tagged for FAANG and startups, with progress tracking.
SQL Interview Prep →