A3.3 Database Programming
SQL from zero to exam ready. If you read one SQL page, make it this one.
- The shop database
- A3.3.1 The four SQL languages
- Building tables (DDL)
- A3.3.2 Queries: SELECT, WHERE, LIKE, ORDER BY
- A3.3.2 Queries between two tables: JOIN
- A3.3.3 Updating data: INSERT, UPDATE, DELETE, indexes
- A3.3.4 Aggregate functions: COUNT, SUM, AVG, GROUP BY, HAVING
- HL only: A3.3.5 Views and A3.3.6 Transactions
- The mistakes that cost marks
- How SQL questions are marked
- Practice exercises
The Shop Database
All examples use one small online art shop. It is the same Negozio shop you normalized to 3NF in A3.2. Artists make items, customers buy items, and every sale is a row in PURCHASE.
ITEM(ItemID, ItemDesc, ItemCost, ArtistID*)
CUSTOMER(CustomerID, CustomerEmail, CustomerCity, CustomerCountry)
PURCHASE(CustomerID*, ItemID*, PurchaseDate, Quantity)
Underlined = primary key. * = foreign key. PURCHASE has a composite key: one customer can buy the same item again, but not twice on the same day.
ARTIST
| ArtistID | ArtistName | ArtistEmail |
|---|---|---|
| 101 | Evie Ceramics | [email protected] |
| 102 | Rojas Artwork | [email protected] |
| 103 | Heather Needlework | [email protected] |
| 104 | Queenies Prints | [email protected] |
| 105 | Alejandro Jewellery | [email protected] |
| 106 | Clodagh Traditional | NULL |
6 rows
ITEM
| ItemID | ItemDesc | ItemCost | ArtistID |
|---|---|---|---|
| 2921 | Bracelet fine | 450 | 105 |
| 2940 | Vase featuring face | 45.05 | 101 |
| 2992 | Self portrait | 192 | 102 |
| 3935 | Decorative drum | 15 | 102 |
| 4832 | Floor vase with leaves | 45.48 | 101 |
| 4849 | Birds on the beach | 45 | 103 |
| 4944 | Abstract wall art | 384 | 102 |
| 5839 | Mountain scene | 29 | 103 |
| 5849 | Priory print | 17 | 104 |
| 5859 | Sculpture featuring two cats | 239 | 101 |
| 8993 | Necklace with rose | 234 | 105 |
| 9282 | Shell necklace | 120 | 105 |
| 9283 | The waves | 29.75 | 103 |
| 9399 | The bridges print | 25 | 104 |
| 9481 | Lighthouse print | 15 | 104 |
| 6120 | Harbour print | 19 | 104 |
| 6230 | Blue bowl | 38 | 101 |
17 rows
CUSTOMER
| CustomerID | CustomerEmail | CustomerCity | CustomerCountry |
|---|---|---|---|
| 1 | [email protected] | Midtown | UK |
| 2 | [email protected] | Midtown | UK |
| 3 | [email protected] | Lakeview | France |
| 4 | [email protected] | Winderview | Italy |
| 5 | [email protected] | Lakeview | France |
| 6 | [email protected] | Midtown | Italy |
| 7 | [email protected] | Paris | France |
| 8 | [email protected] | Vasso | Italy |
| 9 | [email protected] | Lyon | France |
| 10 | [email protected] | Lisbon | Portugal |
10 rows
PURCHASE
| CustomerID | ItemID | PurchaseDate | Quantity |
|---|---|---|---|
| 1 | 4832 | 2025-06-02 | 1 |
| 4 | 2921 | 2025-06-06 | 2 |
| 2 | 9481 | 2025-06-11 | 3 |
| 3 | 2992 | 2025-06-20 | 1 |
| 5 | 9282 | 2025-06-24 | 1 |
| 6 | 5849 | 2025-07-01 | 4 |
| 1 | 9283 | 2025-07-03 | 2 |
| 7 | 3935 | 2025-07-09 | 1 |
| 8 | 8993 | 2025-07-15 | 1 |
| 3 | 9481 | 2025-07-28 | 2 |
| 4 | 4849 | 2025-07-28 | 1 |
| 5 | 2940 | 2025-07-28 | 1 |
| 5 | 9399 | 2025-07-29 | 3 |
| 9 | 5859 | 2025-07-30 | 1 |
| 6 | 9481 | 2025-07-31 | 5 |
| 2 | 6120 | 2025-07-31 | 2 |
16 rows
Try your first query. The * means "every column".
| ArtistID | ArtistName | ArtistEmail |
|---|---|---|
| 101 | Evie Ceramics | [email protected] |
| 102 | Rojas Artwork | [email protected] |
| 103 | Heather Needlework | [email protected] |
| 104 | Queenies Prints | [email protected] |
| 105 | Alejandro Jewellery | [email protected] |
| 106 | Clodagh Traditional | NULL |
6 rows
A3.3.1 The Four SQL Languages
SQL (Structured Query Language) is the language used to talk to a relational database. Its commands fall into four groups, each with a different job. The DBMS (database management system) is the umbrella that runs all four.
| Language | Job | Commands | Think of it as |
|---|---|---|---|
| DDL Data Definition Language | Defines the structure (schema): tables, columns, data types, keys, constraints. | CREATE ALTER DROP TRUNCATE RENAME COMMENT | Building the shelves |
| DML Data Manipulation Language | Reads and changes the data inside the tables. | SELECT INSERT UPDATE DELETE (also CALL, LOCK TABLE) | Putting things on the shelves, finding them, moving them |
| DCL Data Control Language | Controls who is allowed to do what. Keeps the database secure. | GRANT REVOKE | The keys to the shop |
| TCL Transaction Control Language | Manages transactions: makes changes permanent or undoes them. | COMMIT ROLLBACK (also SAVEPOINT) | Save and undo |
DCL in action. These give a staff account permission to read and add items, then take the adding permission away again:
GRANT SELECT, INSERT ON Item TO shop_staff; REVOKE INSERT ON Item FROM shop_staff;
DELETE vs TRUNCATE vs DROP
A favourite exam trap. All three "remove" something, but not the same thing.
| Command | Language | Removes | Table still exists? |
|---|---|---|---|
DELETE FROM Item WHERE ... | DML | Only the rows that match the WHERE. With no WHERE, every row. | Yes |
TRUNCATE TABLE Item | DDL | Every row, fast, in one go. No WHERE allowed. | Yes, empty |
DROP TABLE Item | DDL | The whole table: structure and data. | No |
DDL is used to define and change the structure of the database, such as creating or altering tables (CREATE, ALTER, DROP) [1]. DML is used to access and change the data stored in those tables, such as SELECT, INSERT, UPDATE and DELETE [1].
The live boxes run SQLite, which has no user accounts and no TRUNCATE. So GRANT, REVOKE and TRUNCATE only work in full systems like MySQL. In SQLite, DELETE FROM Item; does the job of TRUNCATE.
Building Tables (DDL)
CREATE DATABASE and CREATE TABLE
CREATE DATABASE ShopDB;
CREATE TABLE TableName (
Column1 DATATYPE constraints,
Column2 DATATYPE constraints,
...
PRIMARY KEY (Column1),
FOREIGN KEY (Column2) REFERENCES OtherTable(OtherColumn)
);
| Constraint | Meaning |
|---|---|
PRIMARY KEY | Uniquely identifies each row. Can't be empty, can't repeat. |
PRIMARY KEY (A, B) | Composite key: the two columns together are unique. |
FOREIGN KEY (X) REFERENCES T(Y) | X must match an existing Y in table T. This creates the relationship and protects referential integrity. |
NOT NULL | The column must always have a value. |
UNIQUE | No two rows can share this value (e.g. an email). |
DEFAULT value | Value used when none is given. |
Common data types: INT whole numbers, REAL or DECIMAL(8,2) decimals, VARCHAR(n) text up to n characters, CHAR(n) text of exactly n characters, DATE written as 'YYYY-MM-DD', BOOLEAN. More in A3.2.
Here a new table is created, linked to CUSTOMER with a foreign key, given one row, then displayed:
✓ Table created
✓ 1 row inserted
| DeliveryID | CustomerID | Courier | DeliveryDate |
|---|---|---|---|
| 1 | 4 | DHL | 2025-06-09 |
1 row
- A comma after every line inside the brackets, except the last one.
- Create the "one" table before the "many" table. ARTIST must exist before ITEM can reference it.
- The textbook calls PURCHASE's key "concatenated". It is a composite key: whole columns combined (see A3.1).
ALTER TABLE
Changes the structure of a table that already exists.
ALTER TABLE Customer ADD Phone VARCHAR(20); -- add a column ALTER TABLE Customer DROP COLUMN Phone; -- remove a column ALTER TABLE Customer RENAME TO Client; -- rename the table
✓ Table altered
| CustomerID | CustomerEmail | CustomerCity | CustomerCountry | Phone |
|---|---|---|---|---|
| 1 | [email protected] | Midtown | UK | NULL |
| 2 | [email protected] | Midtown | UK | NULL |
| 3 | [email protected] | Lakeview | France | NULL |
| 4 | [email protected] | Winderview | Italy | NULL |
| 5 | [email protected] | Lakeview | France | NULL |
| 6 | [email protected] | Midtown | Italy | NULL |
| 7 | [email protected] | Paris | France | NULL |
| 8 | [email protected] | Vasso | Italy | NULL |
| 9 | [email protected] | Lyon | France | NULL |
| 10 | [email protected] | Lisbon | Portugal | NULL |
10 rows
The new column is empty (NULL) for every existing row until you fill it in.
DROP TABLE
✓ Table dropped
✗ Error: no such table: Purchase
A3.3.2 Queries
A query is an SQL instruction that extracts information from a database. Queries do three things: filter (find exactly the rows you want), pattern match (search when you only know part of a value) and order (sort the results).
The shape of every query
SELECT columns -- what to show FROM table -- where it lives JOIN table2 ON link -- glue on a second table (if needed) WHERE condition -- which rows GROUP BY column -- make groups (if needed) HAVING condition -- which groups ORDER BY column -- sort
Only SELECT and FROM are required. The rest are optional, but they must always come in this order. Remember it with Some French Jugglers Wear Green Hats Often.
Choosing columns: SELECT, *, DISTINCT, AS
List the columns you want, separated by commas.
| ItemDesc | ItemCost |
|---|---|
| Bracelet fine | 450 |
| Vase featuring face | 45.05 |
| Self portrait | 192 |
| Decorative drum | 15 |
| Floor vase with leaves | 45.48 |
| Birds on the beach | 45 |
| Abstract wall art | 384 |
| Mountain scene | 29 |
| Priory print | 17 |
| Sculpture featuring two cats | 239 |
| Necklace with rose | 234 |
| Shell necklace | 120 |
| The waves | 29.75 |
| The bridges print | 25 |
| Lighthouse print | 15 |
| Harbour print | 19 |
| Blue bowl | 38 |
17 rows
DISTINCT removes duplicate rows. Ten customers, but only four different countries:
| CustomerCountry |
|---|
| UK |
| France |
| Italy |
| Portugal |
4 rows
AS renames a column in the output (an alias). It does not change the table.
| Product | Price |
|---|---|
| Bracelet fine | 450 |
| Vase featuring face | 45.05 |
| Self portrait | 192 |
| Decorative drum | 15 |
| Floor vase with leaves | 45.48 |
| Birds on the beach | 45 |
| Abstract wall art | 384 |
| Mountain scene | 29 |
| Priory print | 17 |
| Sculpture featuring two cats | 239 |
| Necklace with rose | 234 |
| Shell necklace | 120 |
| The waves | 29.75 |
| The bridges print | 25 |
| Lighthouse print | 15 |
| Harbour print | 19 |
| Blue bowl | 38 |
17 rows
Filtering rows: WHERE
WHERE works like an if statement. Only rows where the condition is true are shown.
| Operator | Meaning | Example |
|---|---|---|
= | equal to | CustomerCountry = 'Italy' |
<> or != | not equal to | CustomerCountry <> 'UK' |
> < >= <= | greater, less, or equal | ItemCost >= 100 |
BETWEEN a AND b | in a range, including both ends | ItemCost BETWEEN 25 AND 45 |
IN (...) | matches any value in a list | CustomerCountry IN ('Italy', 'Portugal') |
LIKE | matches a pattern | ItemDesc LIKE '%print%' |
IS NULL / IS NOT NULL | empty / not empty | ArtistEmail IS NULL |
Text and dates go in single quotes. Numbers don't. 'France', '2025-07-28', but 200.
| ItemDesc | ItemCost |
|---|---|
| Bracelet fine | 450 |
| Abstract wall art | 384 |
| Sculpture featuring two cats | 239 |
| Necklace with rose | 234 |
4 rows
| CustomerEmail | CustomerCity |
|---|---|
| [email protected] | Lakeview |
| [email protected] | Lakeview |
| [email protected] | Paris |
| [email protected] | Lyon |
4 rows
BETWEEN includes both ends, so an item costing exactly 25 or exactly 45 is included:
| ItemDesc | ItemCost |
|---|---|
| Birds on the beach | 45 |
| Mountain scene | 29 |
| The waves | 29.75 |
| The bridges print | 25 |
| Blue bowl | 38 |
5 rows
| CustomerEmail | CustomerCountry |
|---|---|
| [email protected] | Italy |
| [email protected] | Italy |
| [email protected] | Italy |
| [email protected] | Portugal |
4 rows
Pattern matching: LIKE
Use LIKE when you only know part of the value. It has two wildcards.
| Wildcard | Means |
|---|---|
% | any number of characters (zero, one or many) |
_ | exactly one character |
| Pattern | Finds |
|---|---|
'S%' | starts with S |
'%print' | ends with print |
'%print%' | contains print anywhere |
'_i%' | second letter is i |
'____' | exactly four characters |
| ItemDesc |
|---|
| Priory print |
| The bridges print |
| Lighthouse print |
| Harbour print |
4 rows
| CustomerEmail |
|---|
| [email protected] |
1 row
In SQLite and MySQL, LIKE ignores upper and lower case. LIKE without any wildcard behaves exactly like =.
Empty values: NULL
NULL means "no value". It is not zero and not an empty string. Nothing is ever = NULL, not even NULL itself, so you must write IS NULL.
| ArtistName |
|---|
| Clodagh Traditional |
1 row
Combining conditions: AND, OR, NOT
| Operator | Row is shown when |
|---|---|
AND | both conditions are true |
OR | at least one condition is true |
NOT | the condition is false (use it to exclude) |
| CustomerEmail | CustomerCity | CustomerCountry |
|---|---|---|
| [email protected] | Lakeview | France |
| [email protected] | Lakeview | France |
| [email protected] | Paris | France |
| [email protected] | Lyon | France |
4 rows
Brackets fix it. Now only Lakeview customers appear:
| CustomerEmail | CustomerCity | CustomerCountry |
|---|---|---|
| [email protected] | Lakeview | France |
| [email protected] | Lakeview | France |
2 rows
Rule: whenever a WHERE mixes AND with OR, add brackets.
| CustomerEmail | CustomerCountry |
|---|---|
| [email protected] | France |
| [email protected] | Italy |
| [email protected] | France |
| [email protected] | Italy |
| [email protected] | France |
| [email protected] | Italy |
| [email protected] | France |
| [email protected] | Portugal |
8 rows
Sorting: ORDER BY
ASC is smallest first (A to Z, oldest date first) and is the default. DESC is largest first. Add a second column to break ties: here items with the same price are sorted alphabetically.
| ItemDesc | ItemCost |
|---|---|
| Bracelet fine | 450 |
| Abstract wall art | 384 |
| Sculpture featuring two cats | 239 |
| Necklace with rose | 234 |
| Self portrait | 192 |
| Shell necklace | 120 |
| Floor vase with leaves | 45.48 |
| Vase featuring face | 45.05 |
| Birds on the beach | 45 |
| Blue bowl | 38 |
| The waves | 29.75 |
| Mountain scene | 29 |
| The bridges print | 25 |
| Harbour print | 19 |
| Priory print | 17 |
| Decorative drum | 15 |
| Lighthouse print | 15 |
17 rows
Not in the syllabus but handy: LIMIT n keeps only the first n rows. Perfect for "top 3" questions.
| ItemDesc | ItemCost |
|---|---|
| Bracelet fine | 450 |
| Abstract wall art | 384 |
| Sculpture featuring two cats | 239 |
3 rows
A3.3.2 Queries Between Two Tables: JOIN
Normalization split the data into separate tables. ITEM only stores ArtistID = 105, not the artist's name. To show the item and the name, you join the tables back together. The join matches rows where the foreign key equals the primary key.
SELECT Table1.column, Table2.column FROM Table1 INNER JOIN Table2 ON Table1.ForeignKey = Table2.PrimaryKey;
Item.ItemDesc means "the ItemDesc column of the Item table". This dot notation is required when both tables have a column with the same name (both have ArtistID, so plain ArtistID gives an "ambiguous column" error). Using it everywhere in a join is good practice and examiners like it.
| ItemDesc | ItemCost | ArtistName |
|---|---|---|
| Bracelet fine | 450 | Alejandro Jewellery |
| Vase featuring face | 45.05 | Evie Ceramics |
| Self portrait | 192 | Rojas Artwork |
| Decorative drum | 15 | Rojas Artwork |
| Floor vase with leaves | 45.48 | Evie Ceramics |
| Birds on the beach | 45 | Heather Needlework |
| Abstract wall art | 384 | Rojas Artwork |
| Mountain scene | 29 | Heather Needlework |
| Priory print | 17 | Queenies Prints |
| Sculpture featuring two cats | 239 | Evie Ceramics |
| Necklace with rose | 234 | Alejandro Jewellery |
| Shell necklace | 120 | Alejandro Jewellery |
| The waves | 29.75 | Heather Needlework |
| The bridges print | 25 | Queenies Prints |
| Lighthouse print | 15 | Queenies Prints |
| Harbour print | 19 | Queenies Prints |
| Blue bowl | 38 | Evie Ceramics |
17 rows
A join is still a normal query. You can add WHERE and ORDER BY after it:
| ItemDesc | ItemCost | ArtistName |
|---|---|---|
| Abstract wall art | 384 | Rojas Artwork |
| Self portrait | 192 | Rojas Artwork |
| Birds on the beach | 45 | Heather Needlework |
| The waves | 29.75 | Heather Needlework |
| Mountain scene | 29 | Heather Needlework |
| Decorative drum | 15 | Rojas Artwork |
6 rows
Joining PURCHASE to CUSTOMER to see what Italian customers bought and when:
| PurchaseDate | CustomerEmail | Quantity |
|---|---|---|
| 2025-06-06 | [email protected] | 2 |
| 2025-07-01 | [email protected] | 4 |
| 2025-07-15 | [email protected] | 1 |
| 2025-07-28 | [email protected] | 1 |
| 2025-07-31 | [email protected] | 5 |
5 rows
INNER, LEFT and RIGHT joins
only rows that match in both
every row of A, plus matches from B
every row of B, plus matches from A
| Join | Returns |
|---|---|
INNER JOIN (or just JOIN) | Only rows with a matching value in both tables. Unmatched rows disappear. |
LEFT JOIN | All rows from the left table (the one after FROM). Where there is no match on the right, those columns are NULL. |
RIGHT JOIN | All rows from the right table (the one after JOIN). Where there is no match on the left, those columns are NULL. |
With an INNER JOIN, Clodagh Traditional vanished because she has no items. A LEFT JOIN from ARTIST keeps her, with NULL for the item:
| ArtistName | ItemDesc |
|---|---|
| Alejandro Jewellery | Bracelet fine |
| Alejandro Jewellery | Necklace with rose |
| Alejandro Jewellery | Shell necklace |
| Clodagh Traditional | NULL |
| Evie Ceramics | Blue bowl |
| Evie Ceramics | Floor vase with leaves |
| Evie Ceramics | Sculpture featuring two cats |
| Evie Ceramics | Vase featuring face |
| Heather Needlework | Birds on the beach |
| Heather Needlework | Mountain scene |
| Heather Needlework | The waves |
| Queenies Prints | Harbour print |
| Queenies Prints | Lighthouse print |
| Queenies Prints | Priory print |
| Queenies Prints | The bridges print |
| Rojas Artwork | Abstract wall art |
| Rojas Artwork | Decorative drum |
| Rojas Artwork | Self portrait |
18 rows
A RIGHT JOIN is the same idea from the other side. Adding WHERE ... IS NULL keeps only the rows with no partner. This is the classic way to answer "which artists have no items?" or "which items were never sold?".
| ArtistName | ItemDesc |
|---|---|
| Clodagh Traditional | NULL |
1 row
| RowsReturned |
|---|
| 102 |
1 row
Bonus: you can keep joining. Each extra table needs its own JOIN ... ON. PURCHASE links CUSTOMER and ITEM, so three tables give "who bought what":
| CustomerEmail | ItemDesc | Quantity |
|---|---|---|
| [email protected] | Lighthouse print | 2 |
| [email protected] | Birds on the beach | 1 |
| [email protected] | Vase featuring face | 1 |
3 rows
How to write any query in six questions
Task: show the description and price of every item made by Queenies Prints, cheapest first.
- What columns do I show? ItemDesc, ItemCost →
SELECT - Which tables hold everything I need, including the condition? The items are in ITEM, the name "Queenies Prints" is in ARTIST →
FROM Item JOIN Artist - How are they linked? Item's foreign key matches Artist's primary key →
ON Item.ArtistID = Artist.ArtistID - Which rows? Only that artist →
WHERE Artist.ArtistName = 'Queenies Prints' - Any totals or counts per group? No → no GROUP BY
- What order? Cheapest first →
ORDER BY Item.ItemCost ASC
| ItemDesc | ItemCost |
|---|---|
| Lighthouse print | 15 |
| Priory print | 17 |
| Harbour print | 19 |
| The bridges print | 25 |
4 rows
A3.3.3 Updating Data
There are three ways to update data in a database: add, modify and remove. All three are DML.
| Operation | Command | Pattern |
|---|---|---|
| Add a new record | INSERT INTO | INSERT INTO table (col1, col2) VALUES (val1, val2); |
| Modify an existing record | UPDATE ... SET | UPDATE table SET col1 = val1 WHERE condition; |
| Remove a record | DELETE FROM | DELETE FROM table WHERE condition; |
INSERT INTO
The values go in the same order as the column list. You can leave the column list out, but then you must give a value for every column in the table's order, so naming the columns is safer.
✓ 1 row inserted
| ArtistID | ArtistName | ArtistEmail |
|---|---|---|
| 101 | Evie Ceramics | [email protected] |
| 102 | Rojas Artwork | [email protected] |
| 103 | Heather Needlework | [email protected] |
| 104 | Queenies Prints | [email protected] |
| 105 | Alejandro Jewellery | [email protected] |
| 106 | Clodagh Traditional | NULL |
| 107 | Marta Tiles | [email protected] |
7 rows
The database checks constraints on every insert. Here the foreign key blocks an item from an artist that does not exist. This is referential integrity protecting the data:
✗ Error: FOREIGN KEY constraint failed
UPDATE ... SET ... WHERE
Change one or more columns at once, separated by commas. WHERE picks which rows change.
✓ 1 row updated
| ArtistID | ArtistName | ArtistEmail |
|---|---|---|
| 101 | Evie Ceramics | [email protected] |
| 102 | Rojas Artwork | [email protected] |
| 103 | Heather Needlework | [email protected] |
| 104 | Vantage Prints | [email protected] |
| 105 | Alejandro Jewellery | [email protected] |
| 106 | Clodagh Traditional | NULL |
6 rows
The new value can be a calculation using the old one:
✓ 3 rows updated
| ItemDesc | NewCost |
|---|---|
| Bracelet fine | 495 |
| Necklace with rose | 257.4 |
| Shell necklace | 132 |
3 rows
DELETE FROM ... WHERE
✓ 1 row deleted
| CustomerID | CustomerEmail | CustomerCity | CustomerCountry |
|---|---|---|---|
| 1 | [email protected] | Midtown | UK |
| 2 | [email protected] | Midtown | UK |
| 3 | [email protected] | Lakeview | France |
| 4 | [email protected] | Winderview | Italy |
| 5 | [email protected] | Lakeview | France |
| 6 | [email protected] | Midtown | Italy |
| 7 | [email protected] | Paris | France |
| 8 | [email protected] | Vasso | Italy |
| 9 | [email protected] | Lyon | France |
9 rows
Customer 4 has purchases pointing at them. Deleting them would leave orphan rows in PURCHASE, so the foreign key refuses. With ON DELETE CASCADE on the foreign key, their purchases would be deleted too (see cascading in A3.2).
✗ Error: FOREIGN KEY constraint failed
UPDATE Item SET ItemCost = 0; sets every price to zero. DELETE FROM Customer; empties the whole table. Pro habit: write the WHERE first, and test it with a SELECT before running the UPDATE or DELETE.
Indexes: why updates have a cost
An index is a separate, sorted lookup structure that stores the values of one column with a pointer to each row. It works like the index at the back of a book: instead of reading every page, you jump straight to the right one.
| Without an index | With an index |
|---|---|
| To find rows, the DBMS reads every row of the table (a full scan). | The DBMS looks the value up in the sorted index and goes straight to the rows. |
| Fine for 15 items. Painfully slow for 50 million. | Much faster SELECT, WHERE, ORDER BY and JOIN. |
| Inserts, updates and deletes only change the table. | Every insert, update and delete must also update the index, which adds overhead. The index also uses extra storage. |
Primary keys are indexed automatically. You add others yourself on columns you search often:
✓ Index created
| ItemDesc | ItemCost |
|---|---|
| Necklace with rose | 234 |
| Sculpture featuring two cats | 239 |
| Abstract wall art | 384 |
| Bracelet fine | 450 |
4 rows
Records are added with INSERT INTO, changed with UPDATE ... SET and removed with DELETE FROM [1]. A WHERE clause is used to target only the specific records to modify or remove [1]. On each update the DBMS checks constraints such as primary key uniqueness and foreign keys, so integrity is maintained [1]. Any indexes on the table must also be updated and reorganised each time, which adds processing overhead to the operation [1].
A3.3.4 Aggregate Functions and Calculations
An aggregate function takes many rows and returns one value calculated from them.
| Function | Returns | Example |
|---|---|---|
COUNT(*) | number of rows | how many items |
COUNT(column) | number of rows where that column is not NULL | how many artists have an email |
SUM(column) | total | total quantity sold |
AVG(column) | mean average | average price |
MIN(column) | smallest value | cheapest item |
MAX(column) | largest value | most expensive item |
| NumberOfItems | Cheapest | Dearest | AveragePrice | TotalIfOneOfEach |
|---|---|---|---|---|
| 17 | 15 | 450 | 114.25 | 1942.28 |
1 row
COUNT(*) counts rows. COUNT(column) skips NULLs. Clodagh has no email:
| AllArtists | ArtistsWithEmail |
|---|---|
| 6 | 5 |
1 row
Calculations in SELECT
You can do maths with + - * / on columns and name the result with AS. ROUND(value, 2) rounds to 2 decimal places. A calculated column exists only in the output, not in the table.
| PurchaseDate | ItemDesc | Quantity | ItemCost | LineTotal |
|---|---|---|---|---|
| 2025-06-06 | Bracelet fine | 2 | 450 | 900 |
| 2025-07-30 | Sculpture featuring two cats | 1 | 239 | 239 |
| 2025-07-15 | Necklace with rose | 1 | 234 | 234 |
| 2025-06-20 | Self portrait | 1 | 192 | 192 |
| 2025-06-24 | Shell necklace | 1 | 120 | 120 |
| 2025-07-29 | The bridges print | 3 | 25 | 75 |
| 2025-07-31 | Lighthouse print | 5 | 15 | 75 |
| 2025-07-01 | Priory print | 4 | 17 | 68 |
| 2025-07-03 | The waves | 2 | 29.75 | 59.5 |
| 2025-06-02 | Floor vase with leaves | 1 | 45.48 | 45.48 |
| 2025-07-28 | Vase featuring face | 1 | 45.05 | 45.05 |
| 2025-06-11 | Lighthouse print | 3 | 15 | 45 |
| 2025-07-28 | Birds on the beach | 1 | 45 | 45 |
| 2025-07-31 | Harbour print | 2 | 19 | 38 |
| 2025-07-28 | Lighthouse print | 2 | 15 | 30 |
| 2025-07-09 | Decorative drum | 1 | 15 | 15 |
16 rows
GROUP BY: one result per group
On its own, COUNT(*) gives one number for the whole table. GROUP BY splits the rows into groups that share a value, then runs the aggregate once per group. The output has one row per group.
| CustomerCountry | Customers |
|---|---|
| France | 4 |
| Italy | 3 |
| UK | 2 |
| Portugal | 1 |
4 rows
| ArtistName | ItemsListed | AvgPrice |
|---|---|---|
| Alejandro Jewellery | 3 | 268 |
| Rojas Artwork | 3 | 197 |
| Evie Ceramics | 4 | 91.88 |
| Heather Needlework | 3 | 34.58 |
| Queenies Prints | 4 | 19 |
5 rows
SELECT CustomerCountry, COUNT(*) works because the country is grouped and the count is aggregated. The textbook's SELECT * ... GROUP BY breaks this rule: most database systems reject it, and the ones that run it show a random row from each group.
HAVING vs WHERE
Both filter, but at different times.
This is the order the database actually runs a query. WHERE filters single rows before they are grouped. HAVING filters whole groups after grouping. Counts and averages don't exist yet when WHERE runs, so an aggregate in a condition always needs HAVING.
| CustomerCountry | Customers |
|---|---|
| France | 4 |
| Italy | 3 |
2 rows
Both together: WHERE keeps only items under 50, then HAVING keeps only artists with at least two of them.
| ArtistName | CheapItems |
|---|---|
| Evie Ceramics | 3 |
| Heather Needlework | 3 |
| Queenies Prints | 4 |
3 rows
HL ONLY SL students can skip this section.
A3.3.5 Views
A view is a virtual table built from a saved SELECT query. It stores the query, not the data. Every time you use the view, the query runs on the current data, so it is always up to date.
✓ View created
| ItemDesc | ItemCost | ArtistName |
|---|---|---|
| Decorative drum | 15 | Rojas Artwork |
| Mountain scene | 29 | Heather Needlework |
| Priory print | 17 | Queenies Prints |
| The waves | 29.75 | Heather Needlework |
| The bridges print | 25 | Queenies Prints |
| Lighthouse print | 15 | Queenies Prints |
| Harbour print | 19 | Queenies Prints |
7 rows
| Type | What it is |
|---|---|
| Simple view | Based on one table, no aggregates. Usually updatable: changes through the view reach the real table. |
| Complex view | Uses joins, aggregates or GROUP BY. Usually read only. |
| Materialized view | The result is physically stored. Very fast to read, but uses storage and must be refreshed, otherwise it shows stale data. |
Why use views: security (show staff the items without the artists' emails), simplicity (hide a complicated join behind one name), and consistency (everyone uses the same query).
A3.3.6 Transactions
A transaction is a group of operations that is treated as one single unit: either all of them happen or none of them do. Classic example, a bank transfer:
BEGIN TRANSACTION; UPDATE Account SET Balance = Balance - 100 WHERE AccountID = 1; UPDATE Account SET Balance = Balance + 100 WHERE AccountID = 2; COMMIT;
If the power fails after the first UPDATE, money would vanish. Because it is one transaction, the half finished work is undone with ROLLBACK and the database returns to its last committed state.
| ACID property | Meaning |
|---|---|
| Atomicity | All or nothing. The transaction is never left half done. |
| Consistency | The database moves from one valid state to another. All constraints still hold. |
| Isolation | Transactions running at the same time don't see each other's unfinished changes. |
| Durability | Once committed, changes are permanent, even after a crash or power cut. |
Watch ROLLBACK undo a disaster:
✓ Transaction started
✓ 17 rows updated
| Stage | ItemDesc | ItemCost |
|---|---|---|
| inside transaction | Bracelet fine | 0 |
| inside transaction | Vase featuring face | 0 |
| inside transaction | Self portrait | 0 |
3 rows
✓ Done
| Stage | ItemDesc | ItemCost |
|---|---|---|
| after ROLLBACK | Bracelet fine | 450 |
| after ROLLBACK | Vase featuring face | 45.05 |
| after ROLLBACK | Self portrait | 192 |
3 rows
The Mistakes That Cost Marks
| Wrong | Right | Why |
|---|---|---|
| WHERE Country = France | WHERE Country = 'France' | Text needs single quotes. |
| WHERE Email = NULL | WHERE Email IS NULL | Nothing equals NULL. |
| WHERE COUNT(*) > 2 | HAVING COUNT(*) > 2 | Aggregates need HAVING. |
| WHERE A = 1 OR A = 2 AND B = 3 | WHERE (A = 1 OR A = 2) AND B = 3 | AND runs before OR. |
| SELECT ArtistID FROM Item JOIN Artist ... | SELECT Item.ArtistID ... | Ambiguous column in a join. |
| FROM Item JOIN Artist | ... ON Item.ArtistID = Artist.ArtistID | No ON gives a cross join. |
| UPDATE Item SET ItemCost = 10; | ... WHERE ItemID = 2921; | No WHERE changes every row. |
| ItemDesc LIKE 'print' | ItemDesc LIKE '%print%' | No wildcard means exact match. |
| ORDER BY ItemCost WHERE ... | WHERE ... ORDER BY ItemCost | Clauses have a fixed order. |
| SELECT *, COUNT(*) ... GROUP BY Country | SELECT Country, COUNT(*) ... GROUP BY Country | The GROUP BY rule. |
How SQL Questions Are Marked
"Construct" questions give roughly one mark per correct part of the query. Small slips like a missing semicolon are usually not penalised, but a wrong table, a missing join condition or a missing quote around text is.
Construct an SQL query that shows the email address of every customer in Italy and the dates of their purchases, most recent first. [4]
SELECT Customer.CustomerEmail, Purchase.PurchaseDatecorrect columns [1]FROM Purchase INNER JOIN Customer ON Purchase.CustomerID = Customer.CustomerIDboth tables with the correct join condition [1]WHERE Customer.CustomerCountry = 'Italy'correct condition with quotes [1]ORDER BY Purchase.PurchaseDate DESCcorrect order [1]
| CustomerEmail | PurchaseDate |
|---|---|
| [email protected] | 2025-07-31 |
| [email protected] | 2025-07-28 |
| [email protected] | 2025-07-15 |
| [email protected] | 2025-07-01 |
| [email protected] | 2025-06-06 |
5 rows
On paper: put each clause on its own line, use dot notation in joins, and check that every table you name in SELECT or WHERE appears in FROM or a JOIN.
Practice Exercises
Write your answer in the editor and run it. Compare your output with the answer. Different SQL that gives the same correct output is also right.
List the description and cost of every item, cheapest first.
Press Run to see your result.
SELECT ItemDesc, ItemCost FROM Item ORDER BY ItemCost ASC;
| ItemDesc | ItemCost |
|---|---|
| Decorative drum | 15 |
| Lighthouse print | 15 |
| Priory print | 17 |
| Harbour print | 19 |
| The bridges print | 25 |
| Mountain scene | 29 |
| The waves | 29.75 |
| Blue bowl | 38 |
| Birds on the beach | 45 |
| Vase featuring face | 45.05 |
| Floor vase with leaves | 45.48 |
| Shell necklace | 120 |
| Self portrait | 192 |
| Necklace with rose | 234 |
| Sculpture featuring two cats | 239 |
| Abstract wall art | 384 |
| Bracelet fine | 450 |
17 rows
Show the email and city of every customer in the UK.
Press Run to see your result.
SELECT CustomerEmail, CustomerCity FROM Customer WHERE CustomerCountry = 'UK';
| CustomerEmail | CustomerCity |
|---|---|
| [email protected] | Midtown |
| [email protected] | Midtown |
2 rows
Find items that cost from 15 up to and including 30. Show description and cost.
Press Run to see your result.
SELECT ItemDesc, ItemCost FROM Item WHERE ItemCost BETWEEN 15 AND 30;
| ItemDesc | ItemCost |
|---|---|
| Decorative drum | 15 |
| Mountain scene | 29 |
| Priory print | 17 |
| The waves | 29.75 |
| The bridges print | 25 |
| Lighthouse print | 15 |
| Harbour print | 19 |
7 rows
Find every item whose description starts with the letter S.
Press Run to see your result.
SELECT ItemDesc FROM Item WHERE ItemDesc LIKE 'S%';
| ItemDesc |
|---|
| Self portrait |
| Sculpture featuring two cats |
| Shell necklace |
3 rows
Show each artist ID with how many items it has listed, most items first.
Press Run to see your result.
SELECT ArtistID, COUNT(*) AS Items FROM Item GROUP BY ArtistID ORDER BY Items DESC;
| ArtistID | Items |
|---|---|
| 104 | 4 |
| 101 | 4 |
| 105 | 3 |
| 103 | 3 |
| 102 | 3 |
5 rows
Show the artist IDs whose average item cost is more than 100.
Press Run to see your result.
SELECT ArtistID, ROUND(AVG(ItemCost), 2) AS AvgCost FROM Item GROUP BY ArtistID HAVING AVG(ItemCost) > 100;
| ArtistID | AvgCost |
|---|---|
| 102 | 197 |
| 105 | 268 |
2 rows
List every item with the name of the artist who made it, ordered by artist name.
Press Run to see your result.
SELECT Artist.ArtistName, Item.ItemDesc FROM Item INNER JOIN Artist ON Item.ArtistID = Artist.ArtistID ORDER BY Artist.ArtistName;
| ArtistName | ItemDesc |
|---|---|
| Alejandro Jewellery | Bracelet fine |
| Alejandro Jewellery | Necklace with rose |
| Alejandro Jewellery | Shell necklace |
| Evie Ceramics | Vase featuring face |
| Evie Ceramics | Floor vase with leaves |
| Evie Ceramics | Sculpture featuring two cats |
| Evie Ceramics | Blue bowl |
| Heather Needlework | Birds on the beach |
| Heather Needlework | Mountain scene |
| Heather Needlework | The waves |
| Queenies Prints | Priory print |
| Queenies Prints | The bridges print |
| Queenies Prints | Lighthouse print |
| Queenies Prints | Harbour print |
| Rojas Artwork | Self portrait |
| Rojas Artwork | Decorative drum |
| Rojas Artwork | Abstract wall art |
17 rows
Show the date, email and quantity of every purchase made in July 2025 by customers in France.
Press Run to see your result.
SELECT Purchase.PurchaseDate, Customer.CustomerEmail, Purchase.Quantity FROM Purchase INNER JOIN Customer ON Purchase.CustomerID = Customer.CustomerID WHERE Customer.CustomerCountry = 'France' AND Purchase.PurchaseDate BETWEEN '2025-07-01' AND '2025-07-31';
| PurchaseDate | CustomerEmail | Quantity |
|---|---|---|
| 2025-07-28 | [email protected] | 2 |
| 2025-07-28 | [email protected] | 1 |
| 2025-07-29 | [email protected] | 3 |
| 2025-07-09 | [email protected] | 1 |
| 2025-07-30 | [email protected] | 1 |
5 rows
Change the email of customer 9 to [email protected], then show that customer.
Press Run to see your result.
UPDATE Customer SET CustomerEmail = '[email protected]' WHERE CustomerID = 9; SELECT * FROM Customer WHERE CustomerID = 9;
Challenge: find the items that have never been bought.
Press Run to see your result.
SELECT Item.ItemID, Item.ItemDesc FROM Item LEFT JOIN Purchase ON Item.ItemID = Purchase.ItemID WHERE Purchase.ItemID IS NULL;
| ItemID | ItemDesc |
|---|---|
| 4944 | Abstract wall art |
| 5839 | Mountain scene |
| 6230 | Blue bowl |
3 rows
Challenge: total money earned by each artist (quantity times cost), highest first.
Press Run to see your result.
SELECT Artist.ArtistName,
SUM(Purchase.Quantity * Item.ItemCost) AS Revenue
FROM Purchase
INNER JOIN Item ON Purchase.ItemID = Item.ItemID
INNER JOIN Artist ON Item.ArtistID = Artist.ArtistID
GROUP BY Artist.ArtistName
ORDER BY Revenue DESC;
| ArtistName | Revenue |
|---|---|
| Alejandro Jewellery | 1254 |
| Queenies Prints | 331 |
| Evie Ceramics | 329.53 |
| Rojas Artwork | 207 |
| Heather Needlework | 104.5 |
5 rows
From the textbook: create CURIO (Curio_ID PK, Curio_Name, Curio_Type), CABINET (Cabinet_ID PK, Cabinet_Description, Cabinet_Location) and COLLECTION (Cabinet_ID, Curio_ID), where both COLLECTION columns form the primary key and are foreign keys.
Press Run to see your result.
CREATE TABLE Curio (
Curio_ID INT NOT NULL PRIMARY KEY,
Curio_Name VARCHAR(50),
Curio_Type VARCHAR(30)
);
CREATE TABLE Cabinet (
Cabinet_ID INT NOT NULL PRIMARY KEY,
Cabinet_Description VARCHAR(100),
Cabinet_Location VARCHAR(50)
);
CREATE TABLE Collection (
Cabinet_ID INT NOT NULL,
Curio_ID INT NOT NULL,
PRIMARY KEY (Cabinet_ID, Curio_ID),
FOREIGN KEY (Cabinet_ID) REFERENCES Cabinet(Cabinet_ID),
FOREIGN KEY (Curio_ID) REFERENCES Curio(Curio_ID)
);
SELECT name FROM sqlite_master WHERE type = 'table';
✓ Table created
✓ Table created
✓ Table created
| name |
|---|
| Artist |
| Item |
| Customer |
| Purchase |
| Curio |
| Cabinet |
| Collection |
7 rows
- DDL = structure, DML = data, DCL = permissions, TCL = transactions. One word each, learn them.
- Clause order: SELECT, FROM, JOIN, WHERE, GROUP BY, HAVING, ORDER BY.
- Text and dates in single quotes, numbers without.
- BETWEEN includes both ends.
%is any number of characters,_is exactly one. - Every join needs ON foreign key = primary key. Use Table.Column in joins.
- WHERE filters rows before grouping, HAVING filters groups after.
- UPDATE and DELETE without WHERE hit every row.
- Indexes make searching faster but every insert, update and delete slower.
I hope you like my work! Please let me know what you think of these notes. Any feedback and suggestions are very welcome. There is a Let us know button in the navigation bar. Send us an email!