← All 45 books SQL starters, one query per page Get the full edition · £10
One query per page

SQL starters, one query per page

The twenty-two first queries that work: SELECT and WHERE, joins, grouping, sorting, NULL, and inserting and updating safely, one move per page.


Steve Hodgkiss 5 queries

You have the data. These are the questions.

SQL starters, one query per page

The twenty-two first queries that work: SELECT and WHERE, joins, grouping, sorting, NULL, and inserting and updating safely, one move per page.


Set in Space Grotesk, Inter and JetBrains Mono (SIL Open Font License).

SQL behaviour verified against the PostgreSQL documentation (postgresql.org/docs, SQL language reference), the SQLite documentation (sqlite.org/lang.html), and the MySQL 8.0 Reference Manual (dev.mysql.com/doc/refman/8.0). PostgreSQL is a trademark of the PostgreSQL Community Association of Canada; MySQL is a trademark of Oracle; SQLite is in the public domain. This book is an independent guide and is not affiliated with or endorsed by any of them.

Your purchase is for personal use only. You do not have redistribution rights: please do not share, resell, or republish this book or its pages.

© 2026 Steve Hodgkiss. All rights reserved. Personal use only; no redistribution rights.

Edition 1.0 · stevehodgkiss.net

Contents

Contents


Part 1 · First queries4
Your first query5
WHERE: the real question6
Part 2 · Reading
NULL is not a value7
Part 3 · Two tables
The inner join8
Part 4 · Writing
UPDATE, safely9
Part 1 of 4
From spreadsheet to table
1

First queries

What a database is, the query that reads it, the clause that asks the real question, and the two traps in every first WHERE.


In this part
  1. 01Your first query
  2. 02WHERE: the real question

SELECT lists the output columns and FROM names the table; SELECT * FROM products returns every row and column. From the PostgreSQL documentation, Tutorial, Querying a Table. postgresql.org/docs.

SQL · No. 01
First queries

Your first query

SELECT, FROM, and the star you'll outgrow

SELECT SAYS WHAT YOU WANT BACK. FROM SAYS WHERE IT LIVES.productsnamepricesupplierTravel mug12.5NordicEspresso cup7.5NordicFrench press26.0BodumSELECT name, price FROM products;the resultnamepriceTravel mug12.5Espresso cup7.5French press26.0* = every columntwo columns asked for, two columns returned, every row

Let's say you want to see what's in products. In SQL you don't open the table. You ask for it.

SELECT name, price FROM products; returns the two columns you name, for every row. The star, *, means every column: fine for a look, sloppy in code. Column names are the table's real columns; guessing one that isn't there is the first error you'll meet.

SELECT says what you want back. FROM says where it lives.

RUN THIS TODAY

Run SELECT name, price FROM products; then SELECT * FROM products; and count the columns in each.

WHERE eliminates rows for which the condition is not true; each row is tested against the search condition. From the PostgreSQL documentation, Tutorial, Where, and the SQL language reference. postgresql.org/docs.

SQL · No. 02
First queries

WHERE: the real question

One condition, tested row by row

EACH ROW ON ITS OWN: KEEP OR DROP.productsnamepriceEspresso cup7.5French press26.0Steel mug9.0Server kettle35.0price < 10row by rowkeptnamepriceEspresso cup7.5Steel mug9.02 of 4dropped:test not trueone wrong character matches nothing, quietly

Let's say products has four hundred rows and you need the ones under £10. Scrolling isn't a question.

WHERE price < 10 keeps only the rows for which the test is true. The test runs row by row: each row on its own, keep or drop. One wrong character in the condition matches nothing, and SQL doesn't warn you; it returns nothing, quietly.

WHERE is a question the table answers row by row.

RUN THIS TODAY

Run SELECT name FROM products WHERE price < 10; then change < to <= and find which extra row joins.

NULL means unknown; comparisons with NULL are not true, so WHERE col = NULL matches nothing and IS NULL is the test that works; COUNT(*) counts all rows while COUNT(col) skips NULLs. From the PostgreSQL documentation, Comparison Operators and Aggregate Functions. postgresql.org/docs.

SQL · No. 03
Reading

NULL is not a value

The missing answer that isn't zero

ZERO IS A VALUE. NULL IS THE QUESTION MARK.productsnamesupplierTravel mugNordicEspresso cupNordicFrench pressNULLSteel mugNULLsupplier = NULL0 rows. never truesupplier IS NULL2 rows. the test that worksCOUNT(*)4COUNT(supplier)2NULL isn't zero, isn't '', and skips the column counts

Let's say some products have no supplier yet. You put a dash, a zero and a blank in the column, and now three different mistakes look like data.

NULL means unknown: not zero, not an empty string. Comparing against unknown isn't true, so = NULL never matches; IS NULL is the test that works. COUNT(*) counts every row, COUNT(supplier) skips the NULLs: one table, two numbers, both right.

Zero is a value. NULL is the question mark.

RUN THIS TODAY

Run SELECT COUNT(*), COUNT(supplier) FROM products; and let the gap between the numbers count the missing suppliers.

JOIN with ON combines rows from two tables into one result row for each pair of rows that satisfies the condition. From the PostgreSQL documentation, SELECT, Table Expression, Joined Tables. postgresql.org/docs.

SQL · No. 04
Two tables

The inner join

Two tables, one question

THE ON SAYS WHICH ROWS BELONG TOGETHER.ordersidproduct_id10111022productsidname1Travel mug2Espresso cupeach pair, one result rowthe joined resultorder idproduct name101Travel mug102Espresso cupno pairing rule = every combination: a wall of nonsense

Let's say orders has product_id and you want names, not numbers. The answer is half in each table.

FROM orders JOIN products ON orders.product_id = products.id pairs each order with its product row. Every pair of rows for which the ON condition is true becomes one row of the result. A join condition that's always true builds every combination, a cross join: it runs, and returns a wall of nonsense.

A join is a pairing rule. The ON says which rows belong together.

RUN THIS TODAY

Run SELECT orders.id, products.name FROM orders JOIN products ON orders.product_id = products.id;

UPDATE table SET column = expression WHERE condition changes the named columns of matching rows; without WHERE it updates every row in the table. From the PostgreSQL documentation, UPDATE reference. postgresql.org/docs.

SQL · No. 05
Writing

UPDATE, safely

Change rows the way you read them

SELECT THE WHERE FIRST. THEN UPDATE THROUGH IT.productsnamepriceEspresso cup7.5Travel mug12.5French press26.012.513.5UPDATE products SET price = 13.5WHERE name = 'Travel mug';test it first:SELECT ... WHERE name = 'Travel mug'the rows the SELECT shows are the rows the UPDATE hits

Let's say the travel mug's price goes up a pound. You don't edit a cell. You change the rows that match.

UPDATE products SET price = 13.5 WHERE name = 'Travel mug'; changes only the matching rows. Test the WHERE as a SELECT first: the rows the SELECT shows are the rows the UPDATE hits. An UPDATE with no WHERE changes every row in the table, and reports success.

Select the WHERE first. Then update through it.

RUN THIS TODAY

Write the SELECT for your UPDATE's WHERE, check the rows it shows, then run the UPDATE and select the row back.

Index

Index


NULL is not a value7
The inner join8
UPDATE, safely9
WHERE: the real question6
Your first query5