SQL NULL Comparison — Why = NULL Returns Zero Rows
A NULL comparison using '=' always yields unknown, causing zero rows.
20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.
- ✓Basic programming fundamentals
- ✓A computer with internet access
- ✓Willingness to follow along with examples
- 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.
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.
SQL is the language your database speaks, and if you don't know it, you're guessing at what your data actually says. This isn't theory—it's the difference between a query that returns exactly what you need and a query that silently returns nothing because you forgot how NULL works. Skip the mental model, and you'll spend hours debugging why your WHERE clause is lying to you.
Why = NULL Returns Zero Rows
In SQL, NULL is not a value — it is a marker for the absence of a value. Because of this, the equality operator (=) cannot evaluate NULL on either side. Any comparison like column = NULL or NULL = NULL yields UNKNOWN, not TRUE, so the row is excluded from results. This is a direct consequence of SQL's three-valued logic: TRUE, FALSE, and UNKNOWN.
When you write WHERE column = NULL, the database engine does not treat NULL as a known value to match. Instead, it evaluates the expression as UNKNOWN, and since WHERE clauses only return rows where the condition is TRUE, zero rows are returned. The correct way to test for NULL is with IS NULL or IS NOT NULL. This is not a quirk — it is intentional and consistent across all SQL databases.
Use IS NULL whenever you need to filter for missing data. In real systems, this distinction matters for data integrity checks, ETL pipelines, and reporting queries. A common mistake is to write WHERE column = NULL expecting matches, which silently returns nothing — no error, no warning, just zero rows.
t1.merchant_id = t2.merchant_id — but both sides were NULL for cash payments, so the rows were silently dropped.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).
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.
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.
AVG() — the database engine will throw an error because those values don't exist yet at the WHERE stage.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.
- 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.
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.
Database Design: Why Your Schema Is Probably Wrong
Most developers jump straight to writing queries before they've thought about how their data actually relates. That's like building a house without a foundation. It works until it doesn't—and when it fails, it fails hard.
A relational database isn't just a bucket for data. It's a model of your business logic. Every table should represent one entity. Every column should be atomic. This is called normalization, and ignoring it is why you end up with columns like tags_csv or user_preferences_json that you're forced to parse in application code.
Here's the rule: If you can't explain what a table represents in one sentence, your schema is broken. A table called orders_items_products_users is a cry for help. Split it. Name things clearly. Define foreign keys upfront—not as an afterthought when your JOINs stop making sense.
Design for queries you'll write six months from now, not the one you're writing today. The database doesn't care about your feelings. It cares about constraints, indexes, and referential integrity.
SQL Basics: The Mental Model That Saves Hours
SQL is not Python. It's not JavaScript. Stop trying to think procedurally—SQL is declarative. You tell it WHAT you want, not HOW to get it. This trips up everyone who comes from imperative languages.
Here's the mental model: Imagine SQL as a pipeline. Data flows through each clause in a specific order. Most developers think SELECT runs first. It doesn't. The actual execution order is:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
This matters because you can't filter on an alias you defined in SELECT within WHERE—the WHERE clause hasn't seen it yet. I've seen junior devs spend an hour debugging this exact issue.
Another fundamental: SQL works on sets, not loops. When you write SELECT * FROM users WHERE id IN (1,2,3), the database is doing set operations, not iterating through a list. This is why SQL is faster at data operations than application code—it's built for batch processing.
Understand this mental model, and you'll stop writing queries that work accidentally and start writing queries that work by design.
DBMS vs. SQL: Why You Need Both to Survive
Newcomers confuse SQL with the database itself. They're not the same thing. SQL is the language. DBMS (Database Management System) is the engine that runs it. You can write perfect SQL and still get destroyed by the DBMS you're running it on.
MySQL, PostgreSQL, SQL Server, SQLite—they all speak SQL, but each has its own quirks. PostgreSQL handles JSON natively. MySQL has GROUP BY behavior that'll bite you if you're not careful. SQLite doesn't support RIGHT JOINs at all. Knowing the difference keeps you from shipping code that works locally but explodes in production.
A DBMS does four things: store data, retrieve data, manage concurrent access, and ensure data integrity. That last one is why transactions exist. If you're not using transactions for operations that modify multiple tables, you're asking for data corruption.
Here's what matters: Pick one DBMS and learn it deeply before jumping to others. PostgreSQL is the industry standard for production systems. SQLite is fine for local dev. MySQL is popular but has enough foot-guns to fill a quarry. Know your toolkit.
ONLY_FULL_GROUP_BY mode to prevent this. Or just use PostgreSQL.Subqueries: When One Query Isn't Enough
You've mastered SELECT, JOIN, and GROUP BY. But real-world data doesn't always cooperate. You need to filter by aggregated results, compare against averages, or find rows that don't exist in another table. That's where subqueries — queries inside queries — save your ass.
A subquery can live in WHERE, FROM, or even SELECT. It runs first, producing a value or a set of rows. The outer query uses that result like a variable. This isn't just theory: you'll use it to find customers who spent above average, products never ordered, or employees in the bottom quartile.
Don't nest too deep. Two levels is usually enough. Three is a code smell. If you need four, you're not breaking down the problem properly. Subqueries are surgical tools, not a substitute for proper schema design.
Window Functions: Rank, Lag, and Moving Averages Without Self-Joins
You need a running total. Or to rank salespeople by region. Or find the difference between each month's revenue and the previous month. You could write a self-join with GROUP BY — ugly, slow, and hard to read. Or you could use window functions, the unsung heroes of analytical SQL.
Window functions compute across a set of rows related to the current row, without collapsing them into one result. OVER() defines the window. PARTITION BY splits into groups. ORDER BY within the window sets the sequence. ROW_NUMBER(), RANK(), LAG(), LEAD(), SUM() OVER() — these are your new tools.
Every senior dev uses window functions for reporting, deduplication, and time-series analysis. They're faster than subqueries and cleaner than self-joins. Learn them once, use them daily.
ROW_NUMBER() with PARTITION BY to deduplicate rows — assign 1 to the first occurrence, then filter WHERE row_num = 1. No GROUP BY needed.Why SQL Optimization Matters Before Your Query Runs
Slow queries don't fail—they just waste seconds that compound into hours. SQL optimization is the practice of reducing query execution time and resource consumption. The why: every unoptimized SELECT or JOIN scans more rows than necessary, locking tables, consuming memory, and frustrating users. The how starts with understanding the query execution plan—EXPLAIN ANALYZE reveals whether your database is doing full table scans when it could use an index. Indexes are the single most impactful tool: a B-tree index on a WHERE column can turn a 10-second query into 10 milliseconds. But indexes aren't free—they slow down writes. Other techniques include avoiding SELECT *, using EXISTS instead of IN for subqueries, filtering early with WHERE before JOINs, and choosing the correct join type. The result: faster APIs, lower hosting costs, and happier DBAs.
The Most Dreaded Topics: NULLs, JOIN Confusion, and Subquery Bloat
Three topics make experienced developers sweat: NULL behavior, multi-table JOINs, and nested subqueries. NULL is not a value—it's the absence of a value. Comparing anything to NULL with = returns NULL, not TRUE or FALSE, which is why WHERE column = NULL gives zero rows. The fix: use IS NULL or IS DISTINCT FROM. JOIN confusion happens because developers forget LEFT JOIN keeps all rows from the left table, while INNER JOIN drops non-matching rows. Mixing OUTER and INNER joins in the same query is the top cause of unexpected row loss. Subquery bloat occurs when developers wrap multiple subqueries inside SELECT instead of using JOINs or CTEs. A subquery inside a WHERE clause runs once per row—this is a performance disaster. Use EXISTS for correlated subqueries or rewrite with JOIN. The mental model: every query is a set operation—if you can't picture the Venn diagram, you'll get wrong results.
Course Syllabus: From Zero to Production-Ready SQL
This course is structured to build your mental model of SQL from the ground up, then rapidly push you into practical, complex querying. The syllabus is divided into four parts. Part One covers the foundational mental model: how a database engine interprets SELECT, WHERE, and FROM clauses differently than you read them. We then explore schema design — why most beginners (and many seniors) end up with conflicting, redundant tables. Part Two dives into aggregate functions, GROUP BY behavior, and the silent pitfalls of NULLs in calculations. Part Three is the core of real-world SQL: joining tables with INNER and LEFT JOINs, subqueries as derived tables, and window functions that eliminate fragile self-joins. Part Four focuses on data modification (INSERT, UPDATE, DELETE) with transactional safety, and finally SQL optimization — reading execution plans before your query hits production. Each module ends with a short quiz, and the final project is a multi-join warehouse report.
Projects, Certification & Paid Features
This course includes two capstone projects: first, you'll design and populate a normalized inventory database for a fictitious e-commerce store — including customers, orders, products, and a shared many-to-many order_items table. You'll write queries that answer real business questions: 'Which products have the highest average order value?' and 'Which customers have not ordered in 90 days?' The second project is a performance analysis: you're given a slow production query (a 6-table JOIN with subqueries) and must rewrite it using window functions and indexing hints to cut execution time by 80%. Upon completing both projects and passing the final assessment (80%+ on a 20-question mixed quiz), you'll earn a certificate of completion from TheCodeForge, verifiable via a unique link. Current course rating: 4.7/5 from 1,200+ reviews. A paid plan unlocks downloadable PDF cheatsheets, a private Slack community for live query reviews, and early access to the 'Advanced Indexing & Query Tuning' module. Free tier includes all lessons and community forum access.
Modern SQL: SQL:2023 Standard Features
The SQL:2023 standard introduces several modern features that enhance query capabilities and developer productivity. Key additions include property graph queries for graph-based data relationships, improved JSON support with JSON_TABLE and JSON_VALUE functions, and new aggregate functions like ANY_VALUE for non-deterministic grouping. Additionally, SQL:2023 standardizes the GREATEST and LEAST functions, which were previously vendor-specific, and introduces the NULLS NOT DISTINCT clause for unique constraints that treat NULLs as equal. These features simplify complex queries and align SQL with modern data modeling needs. For example, JSON_TABLE allows you to parse JSON documents into relational rows directly within SQL:
``sql SELECT FROM JSON_TABLE( '{"items": [{"id": 1, "name": "A"}, {"id": 2, "name": "B"}]}', '$.items[]' COLUMNS ( id INT PATH '$.id', name VARCHAR(10) PATH '$.name' ) ) AS jt; ``
This returns two rows with id and name columns. The property graph query syntax enables pattern matching on graphs using MATCH clauses, similar to Cypher. While full support varies by database, these features represent the future of SQL standardization.
SQL Dialects: PostgreSQL vs MySQL vs SQL Server vs Oracle Differences
While SQL is standardized, each database vendor implements its own dialect with unique syntax, functions, and behaviors. Understanding these differences is crucial for writing portable code and avoiding surprises. Key areas of divergence include:
- String Concatenation: PostgreSQL and Oracle use
||, MySQL usesC, SQL Server usesONCAT()+. - LIMIT/OFFSET: PostgreSQL and MySQL use
LIMIT ... OFFSET, SQL Server usesOFFSET ... FETCH NEXT, Oracle usesOFFSET ... ROWS FETCH NEXTorROWNUM. - Date/Time Functions: PostgreSQL uses
N, MySQL usesOW()N, SQL Server usesOW()G, Oracle usesETDATE()SYSDATE. - NULL Ordering: PostgreSQL treats NULLs as largest by default, MySQL as smallest, SQL Server as smallest, Oracle as largest. Use
NULLS FIRST/LASTfor control. - Auto-increment: PostgreSQL uses
SERIALorIDENTITY, MySQL usesAUTO_INCREMENT, SQL Server usesIDENTITY, Oracle usesSEQUENCEorIDENTITY. - Window Functions: All support them, but syntax for frames and aggregates varies slightly.
Example of LIMIT differences:
```sql -- PostgreSQL / MySQL SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 20;
-- SQL Server SELECT * FROM users ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
-- Oracle (12c+) SELECT * FROM users ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY; ```
When writing cross-database applications, use an ORM or abstract the SQL layer to handle these differences.
SQL in the Cloud: BigQuery, Redshift, Snowflake Dialect Differences
Cloud data warehouses like Google BigQuery, Amazon Redshift, and Snowflake extend SQL with proprietary features optimized for large-scale analytics. While they support standard SQL, key differences affect query writing and optimization:
- BigQuery: Uses GoogleSQL, which is ANSI-compliant but has unique features like
SELECT EXCEPT,UNNESTfor arrays, andPARTITION BYfor clustering. It charges per query (data scanned), soSELECTis expensive. BigQuery requires backticks for table names with hyphens.
- Redshift: Based on PostgreSQL 8.x, but with columnar storage and distribution keys. It uses
SORTKEYandDISTKEYfor physical design. Redshift does not supportSELECT * EXCEPTorUNNESTnatively; useJSON_EXTRACT_PATH_TEXTfor JSON.
- Snowflake: Uses a variant of PostgreSQL with features like
LATERAL FLATTENfor semi-structured data,CLUSTERING KEY, andZEROIFNULL. Snowflake supportsSELECT * EXCLUDEandQUALIFYfor window filters.
Example of semi-structured data querying:
```sql -- BigQuery: UNNEST arrays SELECT id, value FROM dataset.table, UNNEST(JSON_EXTRACT_ARRAY(json_column, '$.items')) AS value;
-- Snowflake: LATERAL FLATTEN SELECT id, value FROM dataset.table, LATERAL FLATTEN(INPUT => PARSE_JSON(json_column):items) AS value;
-- Redshift: JSON_EXTRACT_PATH_TEXT (limited) SELECT id, JSON_EXTRACT_PATH_TEXT(json_column, 'items') AS value; ```
Cost optimization differs: BigQuery charges by bytes processed, Redshift by cluster hours, Snowflake by credits. Use SELECT with specific columns and filter early to reduce costs.
The Silent NULL – How a Missing IS NULL Filtered Out Half Your Customers
- 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.
SELECT COUNT(*) FROM table_name;SELECT * FROM table_name LIMIT 5;| File | Command / Code | Purpose |
|---|---|---|
| select_and_filter_books.sql | SELECT * FROM books; | Your First SQL Query |
| sort_and_limit_books.sql | SELECT title, price | Sorting, Limiting and Shaping Results with ORDER BY and LIMI |
| aggregate_and_group_books.sql | SELECT COUNT(*) AS total_books | Aggregate Functions |
| join_books_authors.sql | SELECT books.title, | Joining Tables with INNER JOIN |
| modify_data.sql | INSERT INTO authors (author_id, full_name, country) | Modifying Data |
| SchemaDesignNightmare.sql | CREATE TABLE orders ( | Database Design |
| ExecutionOrder.sql | SELECT | SQL Basics |
| DBMSQuirks.sql | SELECT | DBMS vs. SQL |
| AboveAverageOrders.sql | SELECT first_name, last_name | Subqueries |
| MonthlyRevenueChange.sql | SELECT | Window Functions |
| Index_Optimization.sql | SELECT * FROM orders WHERE status = 'pending'; | Why SQL Optimization Matters Before Your Query Runs |
| Null_Join_Trap.sql | SELECT id, name FROM users WHERE middle_name = NULL; | The Most Dreaded Topics |
| course_syllabus_check.sql | SELECT module_id, title, duration_min | Course Syllabus |
| certificate_eligibility.sql | SELECT student_id | Projects, Certification & Paid Features |
| modern_sql_example.sql | SELECT * | Modern SQL |
| dialect_comparison.sql | SELECT 'Hello' || ' ' || 'World'; | SQL Dialects |
| cloud_sql_examples.sql | SELECT * EXCEPT (sensitive_column) FROM dataset.table; | SQL in the Cloud |
Key takeaways
Interview Questions on This Topic
What's the difference between WHERE and HAVING in SQL, and can you give a concrete example of when you'd use each?
Frequently Asked Questions
20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.
That's SQL Basics. Mark it forged?
11 min read · try the examples if you haven't