A3.3 Database Programming

SQL from zero to exam ready. If you read one SQL page, make it this one.

Every dark code box on this page is live. Change the SQL, press Run (or Ctrl+Enter) and the real result appears underneath. Each run starts from a fresh copy of the shop database, so you cannot break anything. Experiment as much as you like. Want a blank editor? Use the SQL Sandbox.
Contents
  1. The shop database
  2. A3.3.1 The four SQL languages
  3. Building tables (DDL)
  4. A3.3.2 Queries: SELECT, WHERE, LIKE, ORDER BY
  5. A3.3.2 Queries between two tables: JOIN
  6. A3.3.3 Updating data: INSERT, UPDATE, DELETE, indexes
  7. A3.3.4 Aggregate functions: COUNT, SUM, AVG, GROUP BY, HAVING
  8. HL only: A3.3.5 Views and A3.3.6 Transactions
  9. The mistakes that cost marks
  10. How SQL questions are marked
  11. 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.

ARTIST ArtistIDPK ArtistName ArtistEmail ITEM ItemIDPK ItemDesc ItemCost ArtistIDFK PURCHASE CustomerIDPK FK ItemIDPK FK PurchaseDatePK Quantity CUSTOMER CustomerIDPK CustomerEmail CustomerCity CustomerCountry One artist makes many items. One item appears in many purchases. One customer makes many purchases.
ARTIST(ArtistID, ArtistName, ArtistEmail)
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

ArtistIDArtistNameArtistEmail
101Evie Ceramics[email protected]
102Rojas Artwork[email protected]
103Heather Needlework[email protected]
104Queenies Prints[email protected]
105Alejandro Jewellery[email protected]
106Clodagh TraditionalNULL

6 rows

ITEM

ItemIDItemDescItemCostArtistID
2921Bracelet fine450105
2940Vase featuring face45.05101
2992Self portrait192102
3935Decorative drum15102
4832Floor vase with leaves45.48101
4849Birds on the beach45103
4944Abstract wall art384102
5839Mountain scene29103
5849Priory print17104
5859Sculpture featuring two cats239101
8993Necklace with rose234105
9282Shell necklace120105
9283The waves29.75103
9399The bridges print25104
9481Lighthouse print15104
6120Harbour print19104
6230Blue bowl38101

17 rows

CUSTOMER

CustomerIDCustomerEmailCustomerCityCustomerCountry
1[email protected]MidtownUK
2[email protected]MidtownUK
3[email protected]LakeviewFrance
4[email protected]WinderviewItaly
5[email protected]LakeviewFrance
6[email protected]MidtownItaly
7[email protected]ParisFrance
8[email protected]VassoItaly
9[email protected]LyonFrance
10[email protected]LisbonPortugal

10 rows

PURCHASE

CustomerIDItemIDPurchaseDateQuantity
148322025-06-021
429212025-06-062
294812025-06-113
329922025-06-201
592822025-06-241
658492025-07-014
192832025-07-032
739352025-07-091
889932025-07-151
394812025-07-282
448492025-07-281
529402025-07-281
593992025-07-293
958592025-07-301
694812025-07-315
261202025-07-312

16 rows

Try your first query. The * means "every column".

Ctrl+Enter to run
ArtistIDArtistNameArtistEmail
101Evie Ceramics[email protected]
102Rojas Artwork[email protected]
103Heather Needlework[email protected]
104Queenies Prints[email protected]
105Alejandro Jewellery[email protected]
106Clodagh TraditionalNULL

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.

LanguageJobCommandsThink of it as
DDL
Data Definition Language
Defines the structure (schema): tables, columns, data types, keys, constraints.CREATE ALTER DROP TRUNCATE RENAME COMMENTBuilding 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 REVOKEThe 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.

CommandLanguageRemovesTable still exists?
DELETE FROM Item WHERE ...DMLOnly the rows that match the WHERE. With no WHERE, every row.Yes
TRUNCATE TABLE ItemDDLEvery row, fast, in one go. No WHERE allowed.Yes, empty
DROP TABLE ItemDDLThe whole table: structure and data.No
Model answer. Outline the difference between DDL and DML. [2]
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)
);
ConstraintMeaning
PRIMARY KEYUniquely 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 NULLThe column must always have a value.
UNIQUENo two rows can share this value (e.g. an email).
DEFAULT valueValue 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:

Ctrl+Enter to run

✓ Table created

✓ 1 row inserted

DeliveryIDCustomerIDCourierDeliveryDate
14DHL2025-06-09

1 row

Rules that catch people out:
  • 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
Ctrl+Enter to run

✓ Table altered

CustomerIDCustomerEmailCustomerCityCustomerCountryPhone
1[email protected]MidtownUKNULL
2[email protected]MidtownUKNULL
3[email protected]LakeviewFranceNULL
4[email protected]WinderviewItalyNULL
5[email protected]LakeviewFranceNULL
6[email protected]MidtownItalyNULL
7[email protected]ParisFranceNULL
8[email protected]VassoItalyNULL
9[email protected]LyonFranceNULL
10[email protected]LisbonPortugalNULL

10 rows

The new column is empty (NULL) for every existing row until you fill it in.

DROP TABLE

Ctrl+Enter to run

✓ 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.

Ctrl+Enter to run
ItemDescItemCost
Bracelet fine450
Vase featuring face45.05
Self portrait192
Decorative drum15
Floor vase with leaves45.48
Birds on the beach45
Abstract wall art384
Mountain scene29
Priory print17
Sculpture featuring two cats239
Necklace with rose234
Shell necklace120
The waves29.75
The bridges print25
Lighthouse print15
Harbour print19
Blue bowl38

17 rows

DISTINCT removes duplicate rows. Ten customers, but only four different countries:

Ctrl+Enter to run
CustomerCountry
UK
France
Italy
Portugal

4 rows

AS renames a column in the output (an alias). It does not change the table.

Ctrl+Enter to run
ProductPrice
Bracelet fine450
Vase featuring face45.05
Self portrait192
Decorative drum15
Floor vase with leaves45.48
Birds on the beach45
Abstract wall art384
Mountain scene29
Priory print17
Sculpture featuring two cats239
Necklace with rose234
Shell necklace120
The waves29.75
The bridges print25
Lighthouse print15
Harbour print19
Blue bowl38

17 rows

Filtering rows: WHERE

WHERE works like an if statement. Only rows where the condition is true are shown.

OperatorMeaningExample
=equal toCustomerCountry = 'Italy'
<> or !=not equal toCustomerCountry <> 'UK'
> < >= <=greater, less, or equalItemCost >= 100
BETWEEN a AND bin a range, including both endsItemCost BETWEEN 25 AND 45
IN (...)matches any value in a listCustomerCountry IN ('Italy', 'Portugal')
LIKEmatches a patternItemDesc LIKE '%print%'
IS NULL / IS NOT NULLempty / not emptyArtistEmail IS NULL

Text and dates go in single quotes. Numbers don't. 'France', '2025-07-28', but 200.

Ctrl+Enter to run
ItemDescItemCost
Bracelet fine450
Abstract wall art384
Sculpture featuring two cats239
Necklace with rose234

4 rows

Ctrl+Enter to run
CustomerEmailCustomerCity
[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:

Ctrl+Enter to run
ItemDescItemCost
Birds on the beach45
Mountain scene29
The waves29.75
The bridges print25
Blue bowl38

5 rows

Ctrl+Enter to run
CustomerEmailCustomerCountry
[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.

WildcardMeans
%any number of characters (zero, one or many)
_exactly one character
PatternFinds
'S%'starts with S
'%print'ends with print
'%print%'contains print anywhere
'_i%'second letter is i
'____'exactly four characters
Ctrl+Enter to run
ItemDesc
Priory print
The bridges print
Lighthouse print
Harbour print

4 rows

Ctrl+Enter to run
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.

Ctrl+Enter to run
ArtistName
Clodagh Traditional

1 row

Combining conditions: AND, OR, NOT

OperatorRow is shown when
ANDboth conditions are true
ORat least one condition is true
NOTthe condition is false (use it to exclude)
⚠ The bracket trap. SQL does AND before OR, just like maths does × before +. Without brackets, the query below is read as France OR (Italy AND Lakeview), so French customers from every city slip in. Even the textbook makes this mistake in its OR example.
Ctrl+Enter to run
CustomerEmailCustomerCityCustomerCountry
[email protected]LakeviewFrance
[email protected]LakeviewFrance
[email protected]ParisFrance
[email protected]LyonFrance

4 rows

Brackets fix it. Now only Lakeview customers appear:

Ctrl+Enter to run
CustomerEmailCustomerCityCustomerCountry
[email protected]LakeviewFrance
[email protected]LakeviewFrance

2 rows

Rule: whenever a WHERE mixes AND with OR, add brackets.

Ctrl+Enter to run

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.

Ctrl+Enter to run
ItemDescItemCost
Bracelet fine450
Abstract wall art384
Sculpture featuring two cats239
Necklace with rose234
Self portrait192
Shell necklace120
Floor vase with leaves45.48
Vase featuring face45.05
Birds on the beach45
Blue bowl38
The waves29.75
Mountain scene29
The bridges print25
Harbour print19
Priory print17
Decorative drum15
Lighthouse print15

17 rows

Not in the syllabus but handy: LIMIT n keeps only the first n rows. Perfect for "top 3" questions.

Ctrl+Enter to run
ItemDescItemCost
Bracelet fine450
Abstract wall art384
Sculpture featuring two cats239

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.

ITEM ItemDescArtistID Bracelet fine105 Self portrait102 Shell necklace105 Priory print104 ARTIST ArtistIDArtistName 102Rojas Artwork 104Queenies Prints 105Alejandro Jewellery 106Clodagh Traditional ON Item.ArtistID = Artist.ArtistID. Artist 106 has no items, so it has no partner.
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.

Ctrl+Enter to run
ItemDescItemCostArtistName
Bracelet fine450Alejandro Jewellery
Vase featuring face45.05Evie Ceramics
Self portrait192Rojas Artwork
Decorative drum15Rojas Artwork
Floor vase with leaves45.48Evie Ceramics
Birds on the beach45Heather Needlework
Abstract wall art384Rojas Artwork
Mountain scene29Heather Needlework
Priory print17Queenies Prints
Sculpture featuring two cats239Evie Ceramics
Necklace with rose234Alejandro Jewellery
Shell necklace120Alejandro Jewellery
The waves29.75Heather Needlework
The bridges print25Queenies Prints
Lighthouse print15Queenies Prints
Harbour print19Queenies Prints
Blue bowl38Evie Ceramics

17 rows

A join is still a normal query. You can add WHERE and ORDER BY after it:

Ctrl+Enter to run
ItemDescItemCostArtistName
Abstract wall art384Rojas Artwork
Self portrait192Rojas Artwork
Birds on the beach45Heather Needlework
The waves29.75Heather Needlework
Mountain scene29Heather Needlework
Decorative drum15Rojas Artwork

6 rows

Joining PURCHASE to CUSTOMER to see what Italian customers bought and when:

Ctrl+Enter to run
PurchaseDateCustomerEmailQuantity
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

AB
INNER JOIN
only rows that match in both
AB
LEFT JOIN
every row of A, plus matches from B
AB
RIGHT JOIN
every row of B, plus matches from A
JoinReturns
INNER JOIN (or just JOIN)Only rows with a matching value in both tables. Unmatched rows disappear.
LEFT JOINAll rows from the left table (the one after FROM). Where there is no match on the right, those columns are NULL.
RIGHT JOINAll 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:

Ctrl+Enter to run
ArtistNameItemDesc
Alejandro JewelleryBracelet fine
Alejandro JewelleryNecklace with rose
Alejandro JewelleryShell necklace
Clodagh TraditionalNULL
Evie CeramicsBlue bowl
Evie CeramicsFloor vase with leaves
Evie CeramicsSculpture featuring two cats
Evie CeramicsVase featuring face
Heather NeedleworkBirds on the beach
Heather NeedleworkMountain scene
Heather NeedleworkThe waves
Queenies PrintsHarbour print
Queenies PrintsLighthouse print
Queenies PrintsPriory print
Queenies PrintsThe bridges print
Rojas ArtworkAbstract wall art
Rojas ArtworkDecorative drum
Rojas ArtworkSelf 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?".

Ctrl+Enter to run
ArtistNameItemDesc
Clodagh TraditionalNULL

1 row

⚠ Forgetting ON. Without a join condition, SQL pairs every row of one table with every row of the other (a cross join). 17 items × 6 artists = 102 rows of nonsense.
Ctrl+Enter to run
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":

Ctrl+Enter to run
CustomerEmailItemDescQuantity
[email protected]Lighthouse print2
[email protected]Birds on the beach1
[email protected]Vase featuring face1

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.

  1. What columns do I show? ItemDesc, ItemCost → SELECT
  2. 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
  3. How are they linked? Item's foreign key matches Artist's primary key → ON Item.ArtistID = Artist.ArtistID
  4. Which rows? Only that artist → WHERE Artist.ArtistName = 'Queenies Prints'
  5. Any totals or counts per group? No → no GROUP BY
  6. What order? Cheapest first → ORDER BY Item.ItemCost ASC
Ctrl+Enter to run
ItemDescItemCost
Lighthouse print15
Priory print17
Harbour print19
The bridges print25

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.

OperationCommandPattern
Add a new recordINSERT INTOINSERT INTO table (col1, col2) VALUES (val1, val2);
Modify an existing recordUPDATE ... SETUPDATE table SET col1 = val1 WHERE condition;
Remove a recordDELETE FROMDELETE 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.

Ctrl+Enter to run

✓ 1 row inserted

ArtistIDArtistNameArtistEmail
101Evie Ceramics[email protected]
102Rojas Artwork[email protected]
103Heather Needlework[email protected]
104Queenies Prints[email protected]
105Alejandro Jewellery[email protected]
106Clodagh TraditionalNULL
107Marta 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:

Ctrl+Enter to run

✗ Error: FOREIGN KEY constraint failed

UPDATE ... SET ... WHERE

Change one or more columns at once, separated by commas. WHERE picks which rows change.

Ctrl+Enter to run

✓ 1 row updated

ArtistIDArtistNameArtistEmail
101Evie Ceramics[email protected]
102Rojas Artwork[email protected]
103Heather Needlework[email protected]
104Vantage Prints[email protected]
105Alejandro Jewellery[email protected]
106Clodagh TraditionalNULL

6 rows

The new value can be a calculation using the old one:

Ctrl+Enter to run

✓ 3 rows updated

ItemDescNewCost
Bracelet fine495
Necklace with rose257.4
Shell necklace132

3 rows

DELETE FROM ... WHERE

Ctrl+Enter to run

✓ 1 row deleted

CustomerIDCustomerEmailCustomerCityCustomerCountry
1[email protected]MidtownUK
2[email protected]MidtownUK
3[email protected]LakeviewFrance
4[email protected]WinderviewItaly
5[email protected]LakeviewFrance
6[email protected]MidtownItaly
7[email protected]ParisFrance
8[email protected]VassoItaly
9[email protected]LyonFrance

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).

Ctrl+Enter to run

✗ Error: FOREIGN KEY constraint failed

⚠ Never forget WHERE on UPDATE or DELETE. 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 indexWith 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:

Ctrl+Enter to run

✓ Index created

ItemDescItemCost
Necklace with rose234
Sculpture featuring two cats239
Abstract wall art384
Bracelet fine450

4 rows

Model answer. Explain how SQL can be used to update data in a database. [4]
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.

FunctionReturnsExample
COUNT(*)number of rowshow many items
COUNT(column)number of rows where that column is not NULLhow many artists have an email
SUM(column)totaltotal quantity sold
AVG(column)mean averageaverage price
MIN(column)smallest valuecheapest item
MAX(column)largest valuemost expensive item
Ctrl+Enter to run
NumberOfItemsCheapestDearestAveragePriceTotalIfOneOfEach
1715450114.251942.28

1 row

COUNT(*) counts rows. COUNT(column) skips NULLs. Clodagh has no email:

Ctrl+Enter to run
AllArtistsArtistsWithEmail
65

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.

Ctrl+Enter to run
PurchaseDateItemDescQuantityItemCostLineTotal
2025-06-06Bracelet fine2450900
2025-07-30Sculpture featuring two cats1239239
2025-07-15Necklace with rose1234234
2025-06-20Self portrait1192192
2025-06-24Shell necklace1120120
2025-07-29The bridges print32575
2025-07-31Lighthouse print51575
2025-07-01Priory print41768
2025-07-03The waves229.7559.5
2025-06-02Floor vase with leaves145.4845.48
2025-07-28Vase featuring face145.0545.05
2025-06-11Lighthouse print31545
2025-07-28Birds on the beach14545
2025-07-31Harbour print21938
2025-07-28Lighthouse print21530
2025-07-09Decorative drum11515

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.

Ctrl+Enter to run
CustomerCountryCustomers
France4
Italy3
UK2
Portugal1

4 rows

Ctrl+Enter to run
ArtistNameItemsListedAvgPrice
Alejandro Jewellery3268
Rojas Artwork3197
Evie Ceramics491.88
Heather Needlework334.58
Queenies Prints419

5 rows

The GROUP BY rule: every column in the SELECT must either be in the GROUP BY or be inside an aggregate function. 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.

FROM / JOIN→ WHERE→ GROUP BY→ HAVING→ SELECT→ ORDER BY

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.

Ctrl+Enter to run
CustomerCountryCustomers
France4
Italy3

2 rows

Both together: WHERE keeps only items under 50, then HAVING keeps only artists with at least two of them.

Ctrl+Enter to run
ArtistNameCheapItems
Evie Ceramics3
Heather Needlework3
Queenies Prints4

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.

Ctrl+Enter to run

✓ View created

ItemDescItemCostArtistName
Decorative drum15Rojas Artwork
Mountain scene29Heather Needlework
Priory print17Queenies Prints
The waves29.75Heather Needlework
The bridges print25Queenies Prints
Lighthouse print15Queenies Prints
Harbour print19Queenies Prints

7 rows

TypeWhat it is
Simple viewBased on one table, no aggregates. Usually updatable: changes through the view reach the real table.
Complex viewUses joins, aggregates or GROUP BY. Usually read only.
Materialized viewThe 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 propertyMeaning
AtomicityAll or nothing. The transaction is never left half done.
ConsistencyThe database moves from one valid state to another. All constraints still hold.
IsolationTransactions running at the same time don't see each other's unfinished changes.
DurabilityOnce committed, changes are permanent, even after a crash or power cut.

Watch ROLLBACK undo a disaster:

Ctrl+Enter to run

✓ Transaction started

✓ 17 rows updated

StageItemDescItemCost
inside transactionBracelet fine0
inside transactionVase featuring face0
inside transactionSelf portrait0

3 rows

✓ Done

StageItemDescItemCost
after ROLLBACKBracelet fine450
after ROLLBACKVase featuring face45.05
after ROLLBACKSelf portrait192

3 rows

The Mistakes That Cost Marks

WrongRightWhy
WHERE Country = FranceWHERE Country = 'France'Text needs single quotes.
WHERE Email = NULLWHERE Email IS NULLNothing equals NULL.
WHERE COUNT(*) > 2HAVING COUNT(*) > 2Aggregates need HAVING.
WHERE A = 1 OR A = 2 AND B = 3WHERE (A = 1 OR A = 2) AND B = 3AND 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.ArtistIDNo 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 ItemCostClauses have a fixed order.
SELECT *, COUNT(*) ... GROUP BY CountrySELECT Country, COUNT(*) ... GROUP BY CountryThe 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.PurchaseDate correct columns [1]
  • FROM Purchase INNER JOIN Customer ON Purchase.CustomerID = Customer.CustomerID both tables with the correct join condition [1]
  • WHERE Customer.CustomerCountry = 'Italy' correct condition with quotes [1]
  • ORDER BY Purchase.PurchaseDate DESC correct order [1]
Ctrl+Enter to run
CustomerEmailPurchaseDate
[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.

Ctrl+Enter to run

Press Run to see your result.

SELECT ItemDesc, ItemCost
FROM Item
ORDER BY ItemCost ASC;
ItemDescItemCost
Decorative drum15
Lighthouse print15
Priory print17
Harbour print19
The bridges print25
Mountain scene29
The waves29.75
Blue bowl38
Birds on the beach45
Vase featuring face45.05
Floor vase with leaves45.48
Shell necklace120
Self portrait192
Necklace with rose234
Sculpture featuring two cats239
Abstract wall art384
Bracelet fine450

17 rows

Show the email and city of every customer in the UK.

Ctrl+Enter to run

Press Run to see your result.

SELECT CustomerEmail, CustomerCity
FROM Customer
WHERE CustomerCountry = 'UK';
CustomerEmailCustomerCity
[email protected]Midtown
[email protected]Midtown

2 rows

Find items that cost from 15 up to and including 30. Show description and cost.

Ctrl+Enter to run

Press Run to see your result.

SELECT ItemDesc, ItemCost
FROM Item
WHERE ItemCost BETWEEN 15 AND 30;
ItemDescItemCost
Decorative drum15
Mountain scene29
Priory print17
The waves29.75
The bridges print25
Lighthouse print15
Harbour print19

7 rows

Find every item whose description starts with the letter S.

Ctrl+Enter to run

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.

Ctrl+Enter to run

Press Run to see your result.

SELECT ArtistID, COUNT(*) AS Items
FROM Item
GROUP BY ArtistID
ORDER BY Items DESC;
ArtistIDItems
1044
1014
1053
1033
1023

5 rows

Show the artist IDs whose average item cost is more than 100.

Ctrl+Enter to run

Press Run to see your result.

SELECT ArtistID, ROUND(AVG(ItemCost), 2) AS AvgCost
FROM Item
GROUP BY ArtistID
HAVING AVG(ItemCost) > 100;
ArtistIDAvgCost
102197
105268

2 rows

List every item with the name of the artist who made it, ordered by artist name.

Ctrl+Enter to run

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;
ArtistNameItemDesc
Alejandro JewelleryBracelet fine
Alejandro JewelleryNecklace with rose
Alejandro JewelleryShell necklace
Evie CeramicsVase featuring face
Evie CeramicsFloor vase with leaves
Evie CeramicsSculpture featuring two cats
Evie CeramicsBlue bowl
Heather NeedleworkBirds on the beach
Heather NeedleworkMountain scene
Heather NeedleworkThe waves
Queenies PrintsPriory print
Queenies PrintsThe bridges print
Queenies PrintsLighthouse print
Queenies PrintsHarbour print
Rojas ArtworkSelf portrait
Rojas ArtworkDecorative drum
Rojas ArtworkAbstract wall art

17 rows

Show the date, email and quantity of every purchase made in July 2025 by customers in France.

Ctrl+Enter to run

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';
PurchaseDateCustomerEmailQuantity
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.

Ctrl+Enter to run

Press Run to see your result.

UPDATE Customer
SET CustomerEmail = '[email protected]'
WHERE CustomerID = 9;

SELECT * FROM Customer WHERE CustomerID = 9;

✓ 1 row updated

CustomerIDCustomerEmailCustomerCityCustomerCountry
9[email protected]LyonFrance

1 row

Challenge: find the items that have never been bought.

Ctrl+Enter to run

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;
ItemIDItemDesc
4944Abstract wall art
5839Mountain scene
6230Blue bowl

3 rows

Challenge: total money earned by each artist (quantity times cost), highest first.

Ctrl+Enter to run

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;
ArtistNameRevenue
Alejandro Jewellery1254
Queenies Prints331
Evie Ceramics329.53
Rojas Artwork207
Heather Needlework104.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.

Ctrl+Enter to run

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

Ninja Notes:

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!