Mid-level 5 min · March 05, 2026

SQL NULL Comparison — Why = NULL Returns Zero Rows

A NULL comparison using '=' always yields unknown, causing zero rows.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • SQL (Structured Query Language) is the universal language for querying and managing relational databases.
  • Core components: SELECT (columns), FROM (table), WHERE (filter), ORDER BY (sort), LIMIT (count).
  • Performance insight: Always specify columns instead of SELECT * — less data transfer and easier to optimize with indexes.
  • Production insight: A missing WHERE clause on an UPDATE or DELETE can modify or destroy all rows in a table — always test with SELECT first.
  • Biggest beginner mistake: Using = NULL instead of IS NULL — SQL silently returns no rows without error.
Plain-English First

Imagine a massive filing cabinet full of folders, and each folder holds a spreadsheet of information — one for customers, one for orders, one for products. SQL is simply the language you use to talk to that filing cabinet: 'Hey, give me every customer who spent more than $100 last month.' The cabinet hands you exactly that list. No programming degree needed — SQL reads almost like plain English, and that's entirely by design.

Every app you've ever used — Instagram, Netflix, your bank — runs on a database humming quietly in the background. When Netflix recommends a show, a database is being asked a question. When your bank balance updates, a database is being changed. SQL (Structured Query Language) is the universal language for doing both of those things, and it has been for over 50 years. Learning it is one of the highest-ROI skills a developer — or honestly anyone who works with data — can pick up.

Before SQL existed, retrieving data from a computer system meant writing complex custom programs for every single question you wanted to ask. IBM researchers in the 1970s asked a radical question: what if non-programmers could just ask the data a question in something close to plain English? SQL was born from that idea. It solves the problem of making structured data accessible — whether you're a data analyst, a backend engineer, a product manager, or a founder trying to understand your users.

By the end of this article you'll understand what a database and a table actually are, you'll be able to write SELECT queries to fetch data, use WHERE clauses to filter it, sort results with ORDER BY, combine conditions with AND and OR, join multiple tables, and modify data with INSERT, UPDATE, DELETE. You'll also know the most common traps beginners fall into and exactly how to sidestep them. Let's build this up from the ground floor.

What a Database and a Table Actually Are (No Hand-Waving)

A database is an organised collection of data stored on a computer so it can be retrieved quickly and reliably. Think of it as the filing cabinet from the analogy — but one that can hold millions of folders and find any single piece of information in milliseconds.

Inside a database, data lives in tables. A table is exactly what the word suggests: rows and columns, just like a spreadsheet. Each column has a name and a data type — for example, a 'price' column stores numbers, a 'customer_name' column stores text. Each row is one complete record — one customer, one product, one transaction.

When tables are related to each other — for example, an 'orders' table references a 'customers' table — the whole system is called a Relational Database. SQL is the language designed specifically for relational databases.

Popular relational databases you'll hear about include PostgreSQL, MySQL, SQLite, and Microsoft SQL Server. The good news: SQL is nearly identical across all of them. Learn it once, use it everywhere. The small differences (called 'dialects') only matter for advanced features.

create_bookstore_tables.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
-- ============================================================
-- We're building a tiny bookstore database from scratch.
-- Two tables: 'authors' and 'books'.
-- Run this in any SQL environment (SQLite, MySQL, PostgreSQL).
-- ============================================================

-- Create the authors table first, because books will reference it.
CREATE TABLE authors (
    author_id   INTEGER PRIMARY KEY,   -- unique identifier for each author
    full_name   TEXT    NOT NULL,       -- author's full name; NOT NULL means it's required
    country     TEXT                    -- country of origin; optional field
);

-- Create the books table.
CREATE TABLE books (
    book_id     INTEGER PRIMARY KEY,
    title       TEXT    NOT NULL,
    author_id   INTEGER NOT NULL,       -- links back to the authors table
    genre       TEXT,
    price       REAL    NOT NULL,       -- REAL means a decimal number (e.g. 12.99)
    published   INTEGER                -- year published, stored as a number
);

-- Insert four authors into the authors table.
INSERT INTO authors (author_id, full_name, country) VALUES
    (1, 'George Orwell',        'United Kingdom'),
    (2, 'Gabriel García Márquez', 'Colombia'),
    (3, 'Chimamanda Ngozi Adichie', 'Nigeria'),
    (4, 'Haruki Murakami',      'Japan');

-- Insert six books into the books table.
INSERT INTO books (book_id, title, author_id, genre, price, published) VALUES
    (1, 'Nineteen Eighty-Four',         1, 'Dystopian',  9.99,  1949),
    (2, 'Animal Farm',                  1, 'Satire',     7.49,  1945),
    (3, 'One Hundred Years of Solitude',2, 'Magical Realism', 12.99, 1967),
    (4, 'Americanah',                   3, 'Literary Fiction', 10.99, 2013),
    (5, 'Purple Hibiscus',              3, 'Literary Fiction',  8.99, 2003),
    (6, 'Norwegian Wood',               4, 'Literary Fiction', 11.50, 1987);
Output
-- No visible output for CREATE TABLE and INSERT statements.
-- The database engine confirms each statement silently.
-- In most SQL tools you'll see: 'Query OK' or '4 rows affected' etc.
-- The data now lives in the database, ready to be queried.
Why PRIMARY KEY matters:
A PRIMARY KEY is a column (or set of columns) that uniquely identifies every row. Think of it as a social security number for your data — no two rows can share the same value, and it can never be empty. Without it, you'd have no reliable way to point to a specific row later.
Production Insight
A missing PRIMARY KEY can lead to duplicate rows and slow joins.
Always define a PRIMARY KEY on every table — even small ones.
Production databases enforce this; don't skip it in dev either.
Key Takeaway
A database = organised file cabinet.
A table = spreadsheet with rows and columns.
PRIMARY KEY = unique row identifier — always required.

Your First SQL Query — SELECT, FROM, and WHERE Explained

SELECT is the most important SQL command you'll ever learn. It answers the question: 'Show me data.' The basic structure is almost a sentence: SELECT [what columns] FROM [which table].

The asterisk (*) is a wildcard meaning 'all columns'. Use it when exploring data. In production code, always name the columns explicitly — it's faster and far easier to read six months later.

The WHERE clause is how you filter. Without WHERE, SQL returns every single row in the table. WHERE adds a condition that each row must pass to be included in the results. Think of it as a bouncer at the door: only rows that satisfy the condition get in.

Conditions in WHERE can use these comparison operators: = (equals), != or <> (not equals), > (greater than), < (less than), >= (greater than or equal), <= (less than or equal). You can also combine multiple conditions using AND (both must be true) and OR (either can be true).

select_and_filter_books.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
-- ============================================================
-- Querying the bookstore database we created in the last step.
-- Run each query block one at a time to see the output.
-- ============================================================

-- Query 1: Get every column from every row in the books table.
-- Good for exploration, but avoid SELECT * in real apps.
SELECT * FROM books;

-- Query 2: Get only the title and price — we don't need everything.
SELECT title, price
FROM books;

-- Query 3: Filter with WHERE — only books cheaper than $10.00
SELECT title, price
FROM books
WHERE price < 10.00;

-- Query 4: Combine conditions with AND
-- Only books under $10 that were also published after 1980
SELECT title, price, published
FROM books
WHERE price < 10.00
  AND published > 1980;

-- Query 5: Use OR — books that are either Satire OR Dystopian genre
SELECT title, genre
FROM books
WHERE genre = 'Satire'
   OR genre = 'Dystopian';

-- Query 6: A tidier way to write the OR above, using IN
-- IN checks if a value matches any item in a list
SELECT title, genre
FROM books
WHERE genre IN ('Satire', 'Dystopian');
Output
-- Query 1 output (all rows, all columns):
-- book_id | title | author_id | genre | price | published
-- 1 | Nineteen Eighty-Four | 1 | Dystopian | 9.99 | 1949
-- 2 | Animal Farm | 1 | Satire | 7.49 | 1945
-- 3 | One Hundred Years of Solitude | 2 | Magical Realism | 12.99 | 1967
-- 4 | Americanah | 3 | Literary Fiction | 10.99 | 2013
-- 5 | Purple Hibiscus | 3 | Literary Fiction | 8.99 | 2003
-- 6 | Norwegian Wood | 4 | Literary Fiction | 11.50 | 1987
-- Query 3 output (price < 10.00):
-- title | price
-- Nineteen Eighty-Four | 9.99
-- Animal Farm | 7.49
-- Purple Hibiscus | 8.99
-- Query 4 output (price < 10.00 AND published > 1980):
-- title | price | published
-- Purple Hibiscus | 8.99 | 2003
-- Query 6 output (Satire OR Dystopian):
-- title | genre
-- Nineteen Eighty-Four | Dystopian
-- Animal Farm | Satire
Watch Out: AND vs OR Operator Precedence
SQL evaluates AND before OR, just like multiplication before addition in maths. If you write WHERE price < 10 OR genre = 'Satire' AND published > 1960, SQL reads the AND part first. Always use parentheses to make your intent explicit: WHERE price < 10 OR (genre = 'Satire' AND published > 1960). This single habit will save you hours of debugging wrong results.
Production Insight
A missing WHERE can return millions of rows from a large table.
Always use LIMIT when exploring, and never deploy SELECT * in code.
The AND/OR precedence trap is one of the most common bugs in production queries.
Key Takeaway
SELECT + FROM + WHERE = the core of every query.
Always name columns explicitly in production.
Use parentheses to control AND/OR logic.

Sorting, Limiting and Shaping Results with ORDER BY and LIMIT

Fetching data is one thing — getting it back in a useful order is another. ORDER BY sorts your results by one or more columns. ASC means ascending (A→Z, 0→9) and is the default. DESC means descending (Z→A, 9→0).

You can sort by multiple columns: ORDER BY genre ASC, price DESC sorts alphabetically by genre first, then within each genre sorts by price from highest to lowest. This is enormously useful for leaderboards, product listings, and reports.

LIMIT controls how many rows come back. Databases can hold millions of rows — you almost never want all of them at once. LIMIT 5 gives you the top five. Pair it with ORDER BY and you can answer questions like 'What are the 3 most expensive books?' in a single clean query.

ALIAS (the AS keyword) lets you rename a column in the output. This is purely cosmetic — it doesn't change the database — but it makes results far more readable, especially when column names are long or ambiguous.

sort_and_limit_books.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
-- ============================================================
-- Sorting, limiting and renaming output columns.
-- These techniques are used in virtually every real-world query.
-- ============================================================

-- Query 1: Sort all books by price, cheapest first (ASC is default).
SELECT title, price
FROM books
ORDER BY price ASC;

-- Query 2: Sort by price, most expensive first.
SELECT title, price
FROM books
ORDER BY price DESC;

-- Query 3: The 3 most expensive books — combine ORDER BY with LIMIT.
SELECT title, price
FROM books
ORDER BY price DESC
LIMIT 3;

-- Query 4: Sort by genre alphabetically, then by price within each genre.
-- This is multi-column sorting.
SELECT title, genre, price
FROM books
ORDER BY genre ASC, price ASC;

-- Query 5: Use AS to give columns friendlier names in the output.
-- 'title' becomes 'Book Title', 'price' becomes 'Price (USD)'.
-- Notice: multi-word aliases need to be wrapped in double quotes.
SELECT title   AS "Book Title",
       price   AS "Price (USD)",
       published AS "Year"
FROM books
ORDER BY price DESC
LIMIT 4;
Output
-- Query 1 output (cheapest first):
-- title | price
-- Animal Farm | 7.49
-- Purple Hibiscus | 8.99
-- Nineteen Eighty-Four | 9.99
-- Americanah | 10.99
-- Norwegian Wood | 11.50
-- One Hundred Years of Solitude | 12.99
-- Query 3 output (top 3 most expensive):
-- title | price
-- One Hundred Years of Solitude | 12.99
-- Norwegian Wood | 11.50
-- Americanah | 10.99
-- Query 5 output (aliased columns, top 4 by price):
-- Book Title | Price (USD) | Year
-- One Hundred Years of Solitude | 12.99 | 1967
-- Norwegian Wood | 11.50 | 1987
-- Americanah | 10.99 | 2013
-- Nineteen Eighty-Four | 9.99 | 1949
Pro Tip: Always LIMIT when exploring unknown data
If you're querying a production table for the first time and you have no idea how many rows it contains, always add LIMIT 10 to your SELECT. Running SELECT * FROM transactions on a table with 50 million rows without a LIMIT will either time out, crash your client, or make your DBA very unhappy. Explore first, then remove the limit once you know what you're working with.
Production Insight
Sorting on unindexed columns causes full table scans — slow on large tables.
Combining ORDER BY and LIMIT can enable optimised sorts.
Without LIMIT, your application might fetch millions of rows and crash the network.
Key Takeaway
ORDER BY sorts results (ASC/DEFAULT, DESC).
LIMIT restricts row count — always use it for exploration.
Aliases (AS) make output readable but don't affect the database.

Aggregate Functions — COUNT, SUM, AVG, MIN, MAX and GROUP BY

So far every query has returned individual rows. But sometimes you don't want individual records — you want a summary. How many books does each author have? What's the average price per genre? What's the most expensive book? Aggregate functions answer these questions by collapsing many rows into a single calculated value.

The five core aggregate functions: COUNT() counts rows, SUM() adds up numeric values, AVG() calculates the average, MIN() finds the smallest value, MAX() finds the largest.

Aggregate functions become truly powerful when combined with GROUP BY. GROUP BY splits your table into groups (one group per unique value in a column) and then runs the aggregate function on each group separately. The result is one summary row per group.

HAVING is like WHERE but for aggregated results. You can't use WHERE to filter on a COUNT or SUM because those values don't exist until after grouping happens. HAVING runs after the groups are formed, so it can filter on them.

aggregate_and_group_books.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
-- ============================================================
-- Aggregate functions and GROUP BY on our bookstore data.
-- This is where SQL starts to feel genuinely powerful.
-- ============================================================

-- Query 1: How many books are in the entire table?
-- COUNT(*) counts every row regardless of NULL values.
SELECT COUNT(*) AS total_books
FROM books;

-- Query 2: What is the average price of all books?
SELECT ROUND(AVG(price), 2) AS average_price
FROM books;
-- ROUND(value, 2) rounds the result to 2 decimal places.

-- Query 3: Most expensive and cheapest single book.
SELECT MAX(price) AS most_expensive,
       MIN(price) AS cheapest
FROM books;

-- Query 4: How many books does each author have?
-- GROUP BY splits the table into one group per author_id,
-- then COUNT(*) counts the rows in each group.
SELECT author_id,
       COUNT(*) AS number_of_books
FROM books
GROUP BY author_id
ORDER BY number_of_books DESC;

-- Query 5: Average price per genre.
SELECT genre,
       COUNT(*)            AS book_count,
       ROUND(AVG(price),2) AS avg_price
FROM books
GROUP BY genre
ORDER BY avg_price DESC;

-- Query 6: HAVING — only show genres that have MORE than 1 book.
-- We cannot use WHERE here because book_count doesn't exist yet
-- at the point WHERE runs. HAVING runs AFTER GROUP BY.
SELECT genre,
       COUNT(*) AS book_count
FROM books
GROUP BY genre
HAVING book_count > 1;
Output
-- Query 1 output:
-- total_books
-- 6
-- Query 2 output:
-- average_price
-- 10.16
-- Query 3 output:
-- most_expensive | cheapest
-- 12.99 | 7.49
-- Query 4 output (books per author):
-- author_id | number_of_books
-- 3 | 2 (Chimamanda — Americanah + Purple Hibiscus)
-- 1 | 2 (Orwell — 1984 + Animal Farm)
-- 2 | 1
-- 4 | 1
-- Query 5 output (avg price per genre):
-- genre | book_count | avg_price
-- Magical Realism | 1 | 12.99
-- Literary Fiction | 3 | 10.49
-- Dystopian | 1 | 9.99
-- Satire | 1 | 7.49
-- Query 6 output (genres with more than 1 book):
-- genre | book_count
-- Literary Fiction | 3
Interview Gold: WHERE vs HAVING
This is one of the most common SQL interview questions at every level. The rule is clean and memorable: WHERE filters individual rows BEFORE grouping happens. HAVING filters groups AFTER grouping happens. You literally cannot use WHERE to filter on an aggregate like COUNT(*) or AVG() — the database engine will throw an error because those values don't exist yet at the WHERE stage.
Production Insight
Aggregating without GROUP BY gives a single row — not always what you want.
Missing columns in GROUP BY cause errors in strict SQL databases.
Using WHERE instead of HAVING on aggregates silently returns wrong results.
Key Takeaway
COUNT, SUM, AVG, MIN, MAX collapse many rows into one.
GROUP BY creates groups; aggregates run per group.
WHERE filters rows before grouping; HAVING filters groups after.

Joining Tables with INNER JOIN — Connecting Data Across Tables

So far we've worked with one table. In real databases, data lives spread across multiple tables that relate to each other — customers -> orders -> products. To bring that data together in a single query, you need JOINs.

INNER JOIN returns only rows where the join condition is true in both tables. It's the most common JOIN type. The syntax: SELECT columns FROM table1 INNER JOIN table2 ON table1.column = table2.column. The ON clause specifies how the tables relate.

In our bookstore, books have an author_id that links to the authors table. To get each book's title alongside the author's full name, we join books to authors on books.author_id = authors.author_id.

If a book has an author_id that doesn't exist in authors, that book is excluded from the result. That's the 'inner' part — only matching records survive.

join_books_authors.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- ============================================================
-- Joining books and authors to show book title + author name.
-- INNER JOIN = only rows where the match exists in both tables.
-- ============================================================

-- Basic INNER JOIN: get every book with its author's full name.
SELECT books.title,
       authors.full_name
FROM books
INNER JOIN authors ON books.author_id = authors.author_id;

-- Add sorting to see them in a nice order.
SELECT books.title AS "Book Title",
       authors.full_name AS "Author"
FROM books
INNER JOIN authors ON books.author_id = authors.author_id
ORDER BY authors.full_name ASC;

-- Use table aliases to keep the query readable.
SELECT b.title,
       a.full_name
FROM books   AS b
INNER JOIN authors AS a ON b.author_id = a.author_id;
Output
-- Basic INNER JOIN output:
-- title | full_name
-- Nineteen Eighty-Four | George Orwell
-- Animal Farm | George Orwell
-- One Hundred Years of Solitude | Gabriel García Márquez
-- Americanah | Chimamanda Ngozi Adichie
-- Purple Hibiscus | Chimamanda Ngozi Adichie
-- Norwegian Wood | Haruki Murakami
Mental Model: JOIN is like a Venn diagram
  • INNER JOIN: rows that match in both tables.
  • LEFT JOIN: all rows from left table, matching from right (NULL if none).
  • RIGHT JOIN: all rows from right table, matching from left (NULL if none).
  • FULL JOIN: all rows from both tables, NULLs where no match.
Production Insight
A missing ON clause produces a CROSS JOIN — every pair of rows.
On two tables with 10k rows each, that's 100 million rows.
Always alias tables to avoid ambiguous column references.
Test JOINs with LIMIT 5 first to verify the row count is sane.
Key Takeaway
INNER JOIN returns only matching rows from both tables.
Always specify the JOIN condition in ON — never forget it.
Table aliases make queries readable and protect against ambiguous columns.
Which JOIN to use?
IfNeed only rows that exist in both tables
UseUse INNER JOIN
IfNeed all rows from the first table, with optional matches from the second
UseUse LEFT JOIN
IfNeed all rows from both tables regardless of matches
UseUse FULL OUTER JOIN (if supported)

Modifying Data — INSERT, UPDATE, and DELETE Basics

Querying data is half the story. To manage data, you need to add, change, and remove it. SQL provides three core commands for that: INSERT adds new rows, UPDATE modifies existing rows, and DELETE removes rows.

INSERT INTO table (columns) VALUES (values) adds one row at a time. You can also insert multiple rows in a single statement.

UPDATE table SET column = new_value WHERE condition changes data. The WHERE clause is critical here — if you omit it, every row in the table gets updated.

DELETE FROM table WHERE condition removes rows. Same warning: missing WHERE deletes every row.

Always run a SELECT with the same WHERE first to verify which rows will be affected before running UPDATE or DELETE.

modify_data.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
-- ============================================================
-- Adding, updating, and deleting data in our bookstore.
-- Run each block separately and verify with SELECT afterward.
-- ============================================================

-- 1. INSERT a new author.
INSERT INTO authors (author_id, full_name, country)
VALUES (5, 'Elena Ferrante', 'Italy');

-- 2. INSERT multiple books at once.
INSERT INTO books (book_id, title, author_id, genre, price, published)
VALUES
    (7, 'My Brilliant Friend',       5, 'Literary Fiction',  14.99, 2012),
    (8, 'The Story of a New Name',   5, 'Literary Fiction',  14.99, 2013);

-- 3. UPDATE the price of all books by a specific author (author_id = 1).
-- FIRST, preview which rows will be updated:
SELECT book_id, title, price
FROM books
WHERE author_id = 1;

-- THEN, run the UPDATE (10% price increase for George Orwell's books).
UPDATE books
SET price = price * 1.10
WHERE author_id = 1;

-- 4. DELETE a specific book by its ID.
-- Preview first:
SELECT * FROM books WHERE book_id = 8;

-- Then delete:
DELETE FROM books
WHERE book_id = 8;

-- 5. Danger: UPDATE without WHERE updates ALL rows.
-- Uncomment the next line only if you really mean it.
-- UPDATE books SET price = 0.00;
Output
-- INSERT: '2 rows affected' (for the two books)
-- After UPDATE (author_id = 1): Nineteen Eighty-Four price becomes 10.99, Animal Farm becomes 8.24.
-- After DELETE: book_id = 8 removed.
-- Always SELECT first to confirm scope.
Production Nightmare: UPDATE or DELETE without WHERE
Every DBA has a story about a junior dev running DELETE FROM users without a WHERE clause. The data is gone instantly — with no undo button (unless you have a backup or transaction). Always wrap modifications in a transaction (BEGIN; UPDATE ...; ROLLBACK; to test) and always SELECT first to preview affected rows.
Production Insight
A missing WHERE on UPDATE/DELETE is unrecoverable without backups.
Always run SELECT with the same WHERE first to count affected rows.
Use transactions: BEGIN, UPDATE, ROLLBACK to test, then COMMIT.
Schedule regular backups — modifications are irreversible without them.
Key Takeaway
INSERT adds rows, UPDATE modifies, DELETE removes.
Never run UPDATE or DELETE without a WHERE clause.
Preview with SELECT, protect with transactions, backup regularly.
● Production incidentPOST-MORTEMseverity: high

The Silent NULL – How a Missing IS NULL Filtered Out Half Your Customers

Symptom
SELECT COUNT(*) FROM customers WHERE country = NULL returned 0, but the country column clearly had NULL values.
Assumption
The developer assumed that = works with NULL like any other value, and that NULL means 'empty'.
Root cause
In SQL, NULL represents 'unknown'. Any comparison with =, !=, <, > on NULL yields UNKNOWN, which is not TRUE, so the row is excluded. WHERE country = NULL is always false for every row.
Fix
Replace WHERE country = NULL with WHERE country IS NULL, or for non-null check use WHERE country IS NOT NULL.
Key lesson
  • NULL is not a value — it's the absence of a value. Never use = or != to compare against NULL.
  • Always use IS NULL or IS NOT NULL for NULL checks.
  • When debugging, first check if NULLs exist: SELECT COUNT(*) FROM table WHERE column IS NULL.
  • Use COALESCE or ISNULL to provide defaults for NULL values in queries.
Production debug guideCommon SQL query failures and the exact actions engineers take to resolve them5 entries
Symptom · 01
Query returns zero rows when you expect data
Fix
Check WHERE clause logic: are you using = NULL? Are you comparing strings that differ in case or whitespace? Are the column names correct? Run SELECT * LIMIT 5 to see raw data.
Symptom · 02
Query returns too many rows (cartesian product)
Fix
If using JOIN, check that you have a proper JOIN condition. Missing ON clause or cross join multiplies rows. Use SELECT COUNT(*) on each table individually.
Symptom · 03
Error: 'column must appear in GROUP BY clause or be used in an aggregate function'
Fix
Add the unaggregated column to the GROUP BY clause. For PostgreSQL, this is strict. For MySQL with ONLY_FULL_GROUP_BY disabled, it may run but give wrong results.
Symptom · 04
Query runs extremely slowly on large tables
Fix
Run EXPLAIN (or EXPLAIN ANALYZE) to see query plan. Look for full table scans. Ensure indexes exist on columns used in WHERE, JOIN, and ORDER BY. Consider adding LIMIT.
Symptom · 05
Subquery returns more than one row when you expected one
Fix
Check if the subquery can return multiple rows. Use LIMIT 1, or use aggregation (MAX, MIN), or restructure with JOIN and DISTINCT.
★ Quick SQL Debugging Cheat SheetThree commands to diagnose any SQL query problem fast
Unexpected result count (too many or zero)
Immediate action
Count rows in each involved table separately to isolate the issue
Commands
SELECT COUNT(*) FROM table_name;
SELECT * FROM table_name LIMIT 5;
Fix now
Check WHERE clause conditions and ensure correct NULL handling.
Syntax error or ambiguous column reference+
Immediate action
Check column names and table aliases, and verify quotes are correct
Commands
SHOW COLUMNS FROM table_name; (or \d table_name in psql)
SELECT column FROM table WHERE x = 'value'; (simplify to debug)
Fix now
Alias columns explicitly and use proper quote types.
Slow query performance+
Immediate action
Obtain the query execution plan to find bottlenecks
Commands
EXPLAIN SELECT ...; (or EXPLAIN ANALYZE)
SHOW INDEX FROM table_name;
Fix now
Add index on columns used in WHERE and JOIN, avoid SELECT *, use LIMIT.
FeatureWHEREHAVING
What it filtersIndividual rowsGroups of rows (post-aggregation)
When it runsBefore GROUP BYAfter GROUP BY
Works with aggregates?No — causes an errorYes — designed for it
Example use caseWHERE price > 10HAVING COUNT(*) > 2
Required clauseNo — optionalNo — only needed with GROUP BY
Can replace the other?NoNo — they serve different stages

Key takeaways

1
SQL reads like English on purpose
SELECT [columns] FROM [table] WHERE [condition] is the skeleton of almost every query you'll ever write.
2
NULL is not a value
it means 'unknown'. Never compare it with = or !=. Always use IS NULL or IS NOT NULL, or you'll silently filter out data that should be there.
3
WHERE filters rows before grouping; HAVING filters groups after grouping
confusing the two is one of the most common SQL errors at every experience level.
4
Always add LIMIT when exploring an unfamiliar table
running an unbounded SELECT * on a production table with millions of rows can lock up your client, slow the database, and make you very unpopular very fast.
5
INNER JOIN returns only matching rows across tables
test with SELECT and LIMIT before running on production.
6
UPDATE and DELETE without WHERE affect all rows. Always preview with SELECT first, and use transactions to allow rollback.

Common mistakes to avoid

3 patterns
×

Confusing single and double quotes

Symptom
Syntax error like 'column Satire does not exist' or unexpected results when quoting identifiers or strings.
Fix
Use single quotes for string literals: WHERE genre = 'Satire'. Use double quotes for column/table names that need quoting (e.g., if they contain spaces): SELECT "Book Title".
×

Using = NULL instead of IS NULL

Symptom
Query returns zero rows even though NULL values exist, with no error message.
Fix
Always use IS NULL for NULL checks and IS NOT NULL for non-NULL checks. Never use = NULL or != NULL.
×

Non-aggregated column in SELECT without including it in GROUP BY

Symptom
Error: 'column must appear in the GROUP BY clause or be used in an aggregate function' (PostgreSQL) or arbitrary/incorrect results (MySQL with sql_mode off).
Fix
Include the column in the GROUP BY clause or wrap it in an aggregate function like MAX(), MIN(), or AVG().
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What's the difference between WHERE and HAVING in SQL, and can you give ...
Q02JUNIOR
If I run SELECT * FROM users WHERE last_login = NULL and get zero rows b...
Q03SENIOR
What does GROUP BY actually do to the rows in a table, and why does ever...
Q01 of 03SENIOR

What's the difference between WHERE and HAVING in SQL, and can you give a concrete example of when you'd use each?

ANSWER
WHERE filters individual rows before any grouping occurs. HAVING filters groups after GROUP BY is applied. For example: to find authors with more than 2 books, you cannot use WHERE COUNT() > 2 because COUNT is an aggregate. You must use GROUP BY author_id HAVING COUNT() > 2. Conversely, to find books with price > 10, you use WHERE price > 10 (no GROUP BY).
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Do I need to install anything to start learning SQL?
02
What's the difference between SQL and MySQL (or PostgreSQL)?
03
Is SQL still worth learning in 2025 with NoSQL and AI tools around?
🔥

That's SQL Basics. Mark it forged?

5 min read · try the examples if you haven't

1 / 16 · SQL Basics
Next
SQL CREATE TABLE and Data Types