61.3% of developers use SQL daily. Master all 30 SQL interview questions: joins, window functions, CTEs, ACID, and query optimization, with runnable examples.
SQL is the most in-demand data skill for backend developers and data analysts in 2026. This guide covers all 30 SQL interview questions from basic SELECT statements to advanced window functions, CTEs, stored procedures, ACID transactions, and query optimization - each with a concise explanation and a runnable SQL snippet.
SQL has been the backbone of data work for over 50 years, and it's more in demand than ever. 61.3% of professional developers use SQL regularly, making it the second most-used language after JavaScript (Stack Overflow Developer Survey 2025). The global database management system market is projected to grow from $149.65B in 2026 to $406.03B by 2034 at a 13.29% CAGR (IACT). SQL is required in 79.4% of data engineer job postings, ahead of Python and every cloud platform.
Whether you're a backend developer, data analyst, or fresher preparing for a technical round, this guide covers all 30 SQL interview questions you're likely to face. We go from basic SELECT queries to advanced window functions, CTEs, transactions, and query optimization, each with a clear answer and a runnable example.
Key Takeaways
SQL is used by 61.3% of professional developers and appears in 79.4% of data engineer job postings (Stack Overflow, 2025)
JOINs, window functions, and query optimization are the three areas interviewers probe hardest at mid-to-senior level
PostgreSQL is now the most popular database at 55.6% adoption; expect dialect-specific questions on EXPLAIN ANALYZE and CTEs
Average SQL developer salary ranges from $82,426 (entry) to $120,063 (senior 15+ years) in the US (Coursera / Glassdoor, 2025)
Interviewers still open with basics because fundamentals reveal how deeply you understand the language versus how well you've memorized syntax. Getting these right signals clean foundations before you move into joins and window functions.
SQL (Structured Query Language) is a declarative language for querying and managing relational databases. You describe what you want; the database engine decides how to retrieve it. SQL is grouped into four command subsets:
CREATE, ALTER, DROP - modifies database structureSELECT, INSERT, UPDATE, DELETE - manages dataGRANT, REVOKE - controls access permissionsCOMMIT, ROLLBACK, SAVEPOINT - manages transactions-- DDL: create a table
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
salary DECIMAL(10, 2)s
);
-- DML: insert and query
INSERT INTO employees VALUES (1, 'Alice', 95000);
SELECT * FROM employees WHERE salary > 80000;Interview tip: Interviewers often ask you to classify a command. SELECT is DML, not DDL - a common slip.
WHERE filters rows before aggregation happens. HAVING filters after GROUP BY aggregates rows. You can't use aggregate functions like COUNT() or SUM() inside a WHERE clause. That's exactly what HAVING is for.
-- WHERE filters raw rows before grouping
SELECT * FROM employees WHERE salary > 70000;
-- HAVING filters aggregated groups
SELECT department_id, COUNT(*) AS headcount
FROM employees
GROUP BY department_id
HAVING COUNT(*) > 5;Interview tip: A quick memory aid - WHERE works on rows, HAVING works on groups.
A primary key uniquely identifies each row in a table and cannot be NULL. Every table should have exactly one. A foreign key is a column in one table that references the primary key of another table, establishing a relationship and enforcing referential integrity, preventing orphan records.
CREATE TABLE departments (
dept_id INT PRIMARY KEY,
dept_name VARCHAR(50) NOT NULL
);
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
name VARCHAR(100),
dept_id INT,
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
);Constraints enforce rules on data at the column or table level. The six main ones:
| Constraint | Purpose |
|---|---|
NOT NULL | Prevents empty/null values |
UNIQUE | Ensures all values in a column are distinct |
PRIMARY KEY | NOT NULL + UNIQUE, identifies each row |
FOREIGN KEY | Enforces referential integrity across tables |
CHECK | Validates values against a condition |
DEFAULT | Assigns a fallback value if none is provided |
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
price DECIMAL(10, 2) CHECK (price >= 0),
category VARCHAR(30) DEFAULT 'General',
sku VARCHAR(20) UNIQUE
);CHAR(n) stores fixed-length strings. It always uses exactly n bytes, padding with spaces if the value is shorter. VARCHAR(n) stores variable-length strings and uses only the space the value actually needs, plus 1-2 bytes for length metadata. Use CHAR for truly fixed-length data like country codes and status flags. Use VARCHAR for names, emails, and anything variable.
-- CHAR always occupies 10 bytes
name CHAR(10) -- "Alice " (padded to 10 chars)
-- VARCHAR uses only what's needed
name VARCHAR(10) -- "Alice" (5 bytes + length overhead)All three remove data, but at very different scopes:
| Command | Removes | Rollback | WHERE clause | Speed |
|---|---|---|---|---|
DELETE | Specific rows | Yes | Yes | Slow (logged) |
TRUNCATE | All rows | No (most DBs) | No | Fast |
DROP | Entire table + structure | No | No | Instant |
DELETE FROM logs WHERE created_at < '2025-01-01'; -- removes old rows, rollback-safe
TRUNCATE TABLE sessions; -- wipes all rows instantly, no rollback
DROP TABLE temp_cache; -- removes table entirelyInterview tip: DELETE fires triggers and logs each row deletion. TRUNCATE bypasses both, making it much faster for clearing large tables.
Group by the column(s) that should be unique and filter for groups with a count greater than 1.
-- Find duplicate emails
SELECT email, COUNT(*) AS occurrences
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
-- Delete duplicates, keep the lowest id
DELETE FROM users
WHERE id NOT IN (
SELECT MIN(id) FROM users GROUP BY email
);Worth knowing: The DELETE + subquery pattern for deduplication is a consistently high-signal question. It tests both GROUP BY understanding and correlated subquery thinking in one go. Many candidates can write the SELECT but freeze on the DELETE.
JOINs are the most probed area in SQL interviews at every level. Weak JOIN answers are the most common reason candidates get screened out at the mid-level. Master all five types and know when each applies.
SQL has five core JOIN types. Each determines which rows from each table appear in the result:
| JOIN Type | Returns |
|---|---|
INNER JOIN | Only rows with matching values in both tables |
LEFT JOIN | All rows from left + matching rows from right (NULLs where no match) |
RIGHT JOIN | All rows from right + matching rows from left (NULLs where no match) |
FULL OUTER JOIN | All rows from both tables; NULLs where no match on either side |
CROSS JOIN | Every combination of rows from both tables (cartesian product) |
-- INNER JOIN: only matched employees with departments
SELECT e.name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;
-- LEFT JOIN: all employees, NULL dept_name if unassigned
SELECT e.name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id;Use FULL OUTER JOIN when you need to see unmatched rows from both tables simultaneously, for example when reconciling two systems where either side may have records the other doesn't.
-- Find mismatches between two payment systems
SELECT
COALESCE(a.txn_id, b.txn_id) AS txn_id,
a.amount AS system_a_amount,
b.amount AS system_b_amount
FROM system_a a
FULL OUTER JOIN system_b b ON a.txn_id = b.txn_id
WHERE a.amount IS NULL OR b.amount IS NULL;LEFT JOIN only keeps orphans from the left table. FULL OUTER JOIN keeps orphans from both, making it essential for data reconciliation and audit queries.
A SELF JOIN joins a table to itself. It's used when rows within a single table have a hierarchical or peer relationship. The most classic example is an employee-manager structure stored in the same table.
-- Show each employee alongside their manager's name
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;Both e and m alias the same employees table. The LEFT JOIN ensures employees without a manager (the CEO) still appear with a NULL in the manager column.
A CROSS JOIN produces the cartesian product of two tables: every row from the left paired with every row from the right. If table A has 5 rows and table B has 4 rows, the result has 20 rows. It has no ON condition.
-- Generate all size/color combinations for a product catalog
SELECT sizes.size, colors.color
FROM sizes
CROSS JOIN colors;
-- 3 sizes × 4 colors = 12 rowsInterview tip: CROSS JOIN is rarely used intentionally in production. Interviewers ask about it to confirm you understand the cartesian product concept and can recognize when an accidental cross join causes a performance explosion.
Both combine the result sets of two SELECT queries with identical column structures. UNION removes duplicate rows by adding an implicit DISTINCT pass, which is slower. UNION ALL keeps all rows including duplicates with no deduplication step, which is faster.
-- UNION: unique department names from two tables
SELECT dept_name FROM departments_us
UNION
SELECT dept_name FROM departments_uk;
-- UNION ALL: all rows, duplicates included (faster when data doesn't overlap)
SELECT dept_name FROM departments_us
UNION ALL
SELECT dept_name FROM departments_uk;Use UNION ALL by default when you know there's no overlap. It avoids the expensive deduplication sort.
A JOIN combines rows from two tables horizontally based on a matching condition. A subquery nests one query inside another and passes the result to the outer query. JOINs are generally faster because the database optimizer can choose the most efficient algorithm (hash join, nested loop, merge join) based on table sizes and available indexes.
-- Subquery approach (correlated - re-executes per row, slow)
SELECT name FROM employees
WHERE dept_id IN (
SELECT dept_id FROM departments WHERE location = 'New York'
);
-- JOIN approach (optimizer can use indexes on both sides)
SELECT e.name
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE d.location = 'New York';Rule of thumb: Write whichever reads more clearly, then run
EXPLAIN ANALYZEto confirm the plan. Modern optimizers often rewrite correlated subqueries as JOINs automatically, but an explicit JOIN gives the optimizer more control.
US Bureau of Labor Statistics forecasts 9% job growth for database administrators and architects through 2033, with approximately 13,200 annual openings (BLS via Coursera, 2025). Intermediate SQL skills - subqueries, indexing, normalization - are what get you past the screening round.
A subquery is a query nested inside another query. A non-correlated subquery runs independently and its result is passed to the outer query once. A correlated subquery references the outer query's columns, so it re-executes once per row: slower but more powerful.
-- Non-correlated: runs once, result used by outer query
SELECT name FROM employees
WHERE dept_id IN (
SELECT dept_id FROM departments WHERE location = 'New York'
);
-- Correlated: runs once per employee row
SELECT name, salary FROM employees e
WHERE salary > (
SELECT AVG(salary) FROM employees e2
WHERE e2.dept_id = e.dept_id -- references outer query
);Correlated subqueries are among the most misunderstood SQL concepts at the mid-level. The key question to ask yourself: "Does this inner query reference anything from the outer query?" If yes, it's correlated.
An index is a separate data structure (typically a B-tree) that stores pointers to rows, ordered by one or more column values. Instead of scanning every row (a full table scan), the database engine navigates the B-tree to find matching rows in O(log n) steps, similar to how a book index works.
-- Create an index on a frequently-filtered column
CREATE INDEX idx_employees_dept ON employees (dept_id);
-- Now this query uses the index instead of a full scan
SELECT * FROM employees WHERE dept_id = 5;Trade-off: Indexes speed up reads but slow down writes (INSERT, UPDATE, DELETE) because the index must be updated on every change. Don't index every column. Index columns that appear in WHERE, JOIN ON, and ORDER BY clauses of slow queries.
Normalization is the process of organizing tables to eliminate redundancy and improve data integrity. It proceeds through a series of normal forms:
-- Violates 1NF: skills column holds multiple values
-- employee_id | name | skills
-- 1 | Alice | SQL,Python,Excel
-- 1NF fix: one row per skill
CREATE TABLE employee_skills (
employee_id INT,
skill VARCHAR(50),
PRIMARY KEY (employee_id, skill)
);Senior-level tip: In practice, BCNF (Boyce-Codd Normal Form) is the target for most OLTP schemas, but interviewers typically only ask about 1NF-3NF. Knowing BCNF as a follow-up answer consistently signals senior-level thinking and separates prepared candidates from ones who stopped at the textbook definition.
A VIEW is a virtual table defined by a saved SELECT query. It doesn't store data itself; it re-executes the underlying query each time it's accessed. Views simplify complex queries, enforce read-only access, and can mask sensitive columns from certain users.
CREATE VIEW active_employees AS
SELECT emp_id, name, dept_id, salary
FROM employees
WHERE status = 'active';
-- Use it just like a real table
SELECT * FROM active_employees WHERE dept_id = 3;Interview follow-up: "What's the difference between a VIEW and a MATERIALIZED VIEW?" A materialized view stores the query result to disk and must be refreshed manually. It's much faster for expensive aggregations but trades freshness for speed.
-- Method 1: ROW_NUMBER() window function (recommended, works in all major DBs)
SELECT salary
FROM (
SELECT salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num
FROM employees
) ranked
WHERE row_num = 3; -- replace 3 with N
-- Method 2: Subquery with LIMIT/OFFSET (MySQL/PostgreSQL)
SELECT DISTINCT salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 2; -- OFFSET = N-1Always clarify whether duplicate salaries should count as one rank or separate ranks. That determines whether to use ROWNUMBER() (unique ranks) or DENSERANK().
| Clustered Index | Non-Clustered Index | |
|---|---|---|
| Physical order | Sorts the actual table rows | Separate structure, points to rows |
| Count per table | One (the table is the index) | Multiple allowed |
| Speed | Fastest for range scans | Adds a lookup step |
| Default | Primary key in most DBs | Any other index |
-- Clustered (one per table, usually auto-created on PRIMARY KEY)
CREATE CLUSTERED INDEX idx_emp_id ON employees(emp_id);
-- Non-clustered (multiple allowed)
CREATE NONCLUSTERED INDEX idx_emp_dept ON employees(dept_id);
CREATE NONCLUSTERED INDEX idx_emp_name ON employees(last_name, first_name);SQL is written in one order but executed in a different order. Knowing this prevents mistakes like referencing a SELECT alias in a WHERE clause, which runs before SELECT.
FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMITSELECT dept_id, AVG(salary) AS avg_sal -- 6. SELECT (alias created here)
FROM employees -- 1. FROM
WHERE status = 'active' -- 3. WHERE (can't use avg_sal here)
GROUP BY dept_id -- 4. GROUP BY
HAVING AVG(salary) > 70000 -- 5. HAVING (must repeat AVG, not alias)
ORDER BY avg_sal DESC -- 7. ORDER BY (alias now available)
LIMIT 5; -- 8. LIMITThe average senior SQL developer earns $120,063 per year in the US, up from $74,967 at entry level (Glassdoor via Coursera, 2025). Advanced topics like window functions, CTEs, and ACID transactions are what separate mid-level candidates from senior-level offers.
Window functions perform calculations across a set of rows related to the current row without collapsing the result into groups like GROUP BY does. They use the OVER() clause to define the window.
The three ranking functions differ in how they handle ties:
ROW_NUMBER() - unique sequential numbers; ties broken arbitrarilyRANK() - tied rows get the same rank; the next rank skips numbers (1, 1, 3)DENSE_RANK() - tied rows get the same rank; no skipping (1, 1, 2)SELECT
name,
salary,
RANK() OVER (ORDER BY salary DESC) AS rank_val,
DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank_val,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num
FROM employees;
-- salary: 100k, 100k, 90k
-- RANK: 1, 1, 3 (skips 2)
-- DENSE_RANK: 1, 1, 2 (no skip)
-- ROW_NUMBER: 1, 2, 3 (always unique)Window functions also include LAG(), LEAD(), SUM() OVER(), AVG() OVER(), and NTILE(). All run after WHERE and GROUP BY but before SELECT.
A CTE is a temporary named result set defined with WITH at the top of a query. It exists only for the duration of that query. CTEs make complex queries readable by breaking them into named logical steps, and they can be referenced multiple times without repeating the subquery.
WITH dept_averages AS (
SELECT dept_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY dept_id
),
high_earners AS (
SELECT e.name, e.salary, da.avg_salary
FROM employees e
JOIN dept_averages da ON e.dept_id = da.dept_id
WHERE e.salary > da.avg_salary
)
SELECT * FROM high_earners ORDER BY salary DESC;Use CTEs over subqueries when: (1) the same subquery appears more than once, (2) you need more than two levels of nesting, or (3) readability matters for a team setting.
A recursive CTE references itself to traverse hierarchical data: org charts, file systems, category trees, and bill-of-materials structures. It has two parts: an anchor (base case) and a recursive member joined with UNION ALL.
WITH RECURSIVE org_chart AS (
-- Anchor: top-level employees (no manager)
SELECT emp_id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Recursive: find direct reports
SELECT e.emp_id, e.name, e.manager_id, oc.level + 1
FROM employees e
JOIN org_chart oc ON e.manager_id = oc.emp_id
)
SELECT level, name FROM org_chart ORDER BY level, name;Interview tip: Always include a termination condition (the join eventually finds no more rows) to prevent infinite loops.
A stored procedure is a precompiled set of SQL statements stored in the database and executed by name. It accepts parameters, runs server-side, and can contain conditional logic, loops, and transactions.
CREATE PROCEDURE GetDeptEmployees
@DeptID INT
AS
BEGIN
SELECT name, hire_date, salary
FROM employees
WHERE dept_id = @DeptID
ORDER BY hire_date DESC;
END;
EXEC GetDeptEmployees @DeptID = 3;Advantages:
EXECUTE on the procedure without exposing table-level accessACID is the set of guarantees that make database transactions reliable. All four properties must hold for a transaction to be considered safe.
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 101;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 202;
COMMIT; -- both updates succeed together
-- ROLLBACK; -- or both are undone if any step failsIsolation levels (from least to most strict): Read Uncommitted, Read Committed, Repeatable Read, Serializable. Higher isolation prevents more anomalies but increases locking and reduces throughput.
A deadlock occurs when two transactions each hold a lock the other needs, creating a circular wait where neither can proceed. Most databases detect and resolve deadlocks automatically by killing one transaction (the victim).
-- Transaction A: locks row 1, then tries to lock row 2
-- Transaction B: locks row 2, then tries to lock row 1
-- Result: both wait forever (deadlock)Prevention strategies:
1. Always access tables and rows in the same consistent order across transactions
2. Keep transactions short: acquire locks late, release early
3. Use lower isolation levels (READ COMMITTED) where strong isolation isn't required
4. Add appropriate indexes so transactions lock fewer rows
Performance questions are where senior candidates prove their real-world value. PostgreSQL is the most adopted database at 55.6% (Stack Overflow 2025), and its EXPLAIN ANALYZE output is the primary tool for finding slow query bottlenecks in production.
Start by understanding what the query actually does, not what you think it does. Use this systematic approach:
1. Run EXPLAIN ANALYZE - identifies full table scans, missing indexes, and row estimate errors
2. Check indexes - add indexes on columns used in WHERE, JOIN ON, and ORDER BY
3. Avoid functions on indexed columns - WHERE YEAR(createdat) = 2025 can't use an index on createdat; use a range instead
4. Replace correlated subqueries with JOINs - a JOIN is almost always faster
5. Use LIMIT early - if you only need 10 rows, tell the database upfront
6. **Avoid SELECT * - fetch only the columns you need to reduce I/O and network transfer
7. Consider partitioning** - split huge tables by date or region to limit scan scope
-- Slow: function on indexed column prevents index use
SELECT * FROM orders WHERE YEAR(created_at) = 2025;
-- Fast: range query uses the index
SELECT * FROM orders
WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01';EXPLAIN shows the query execution plan the database engine has chosen: which indexes it will use, how it will join tables, and the estimated cost. EXPLAIN ANALYZE actually executes the query and shows the real row counts and timing alongside the estimates.
EXPLAIN ANALYZE
SELECT e.name, d.dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE e.salary > 80000;
-- Look for:
-- "Seq Scan" (full table scan) → consider adding an index
-- "Rows=1000 vs actual rows=50000" → stale statistics, run ANALYZE
-- "Hash Join" vs "Nested Loop" → join strategy depends on table sizesKey red flags in EXPLAIN output:
Seq Scan on a large table usually means a missing or unused indexLAG() accesses a value from a previous row in the result set. LEAD() accesses a value from a future row. Both are window functions used for year-over-year comparisons, trend analysis, and detecting sequential changes.
-- Calculate month-over-month revenue growth
SELECT
month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_month_revenue,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY month)) /
NULLIF(LAG(revenue) OVER (ORDER BY month), 0) * 100, 2
) AS mom_growth_pct
FROM monthly_revenue
ORDER BY month;NULLIF(expr, 0) prevents division-by-zero errors when previous revenue is 0. The ORDER BY inside OVER() defines the window ordering; without it, the function would be non-deterministic.
Both are named query results, but they behave very differently:
| View | Materialized View | |
|---|---|---|
| Data storage | None - re-executes query each access | Stores result on disk |
| Freshness | Always current | Stale until refreshed |
| Performance | As slow as the underlying query | Fast (reads cached data) |
| Refresh | Automatic (no stored data) | Manual: REFRESH MATERIALIZED VIEW |
| Best for | Simple abstractions, security | Expensive aggregations, reporting |
-- Standard view (always fresh, always recalculates)
CREATE VIEW dept_summary AS
SELECT dept_id, COUNT(*) AS headcount, AVG(salary) AS avg_salary
FROM employees GROUP BY dept_id;
-- Materialized view (fast reads, refresh periodically)
CREATE MATERIALIZED VIEW dept_summary_mat AS
SELECT dept_id, COUNT(*) AS headcount, AVG(salary) AS avg_salary
FROM employees GROUP BY dept_id;
-- Refresh when you need updated data
REFRESH MATERIALIZED VIEW dept_summary_mat;Interview tip: If asked "why not always use materialized views?" - the answer is data freshness. For dashboards updated once daily, a materialized view is ideal. For real-time transactional data, a standard view is safer.
From experience: The candidates who do best in SQL rounds aren't the ones who memorize queries. They're the ones who can explain why the query is written that way. Every answer above has a "why" embedded in it. When you practice, don't just run the code - explain the reasoning out loud as if you're in the interview.
A structured 2-week plan for SQL interview readiness:
Week 1 - Foundations and JOINs (Q1–Q13)
Week 2 - Advanced and Performance (Q14–Q30)
PostgreSQL is the safest choice. It's the most adopted database at 55.6% of developers (Stack Overflow 2025) and is the dialect used in most technical interviews. MySQL is a close second for web-focused roles. SQL Server is common in enterprise and data warehouse interviews. The core concepts (JOINs, window functions, CTEs, indexes) transfer across all dialects with minor syntax differences.
Data analyst interviews heavily test window functions (especially RANK(), LAG(), LEAD()), aggregation with GROUP BY and HAVING, subqueries for filtering aggregated results, and date/time manipulation. Expect at least one question where you calculate month-over-month or year-over-year metrics using LAG(). Self JOINs and pivot queries with CASE WHEN also appear frequently in analyst rounds.
Most developers need 1–2 weeks of focused prep covering basic queries, JOINs, subqueries, and at least one advanced topic (window functions or CTEs). Data engineer and analyst roles typically require an additional week on performance optimization, indexing strategy, and normalization. Writing queries against a real database (not just reading) cuts preparation time significantly.
JOINs are generally faster because the database optimizer can choose the most efficient join algorithm (hash join, nested loop, merge join) based on table sizes and indexes. Correlated subqueries re-execute for every row, making them O(n²) in the worst case. That said, modern query optimizers often rewrite equivalent subqueries as JOINs automatically. Write whichever is more readable, then check EXPLAIN ANALYZE to confirm the plan.
SQL injection is an attack where malicious SQL is inserted into an input field and executed by the database. Prevention is always via parameterized queries (prepared statements): never string-concatenate user input into SQL. ORMs and query builders handle this automatically by default.
-- Vulnerable (never do this)
query = "SELECT * FROM users WHERE email = '" + userInput + "'";
-- Safe: parameterized query
query = "SELECT * FROM users WHERE email = ?";
execute(query, [userInput]);SQL has been the language of data for over 50 years and the demand keeps growing. The 30 questions in this guide cover every tier you'll encounter: basic clauses, all JOIN types, subqueries, indexes, normalization, window functions, CTEs, stored procedures, ACID transactions, and query optimization.
Don't just read the answers. Open a SQL editor and run each query. The gap between recognizing a correct query and writing one under pressure is exactly what this guide is designed to close.