DDL and DML
SQL splits into two parts with different jobs. Data Definition Language creates and alters the structure — tables, columns, data types, keys. Data Manipulation Language works with the contents — inserting, updating, deleting and above all querying rows.
A useful way to hold the distinction: DDL is used rarely, when the database is designed or changed. DML runs constantly, every time the application does anything at all.
| Sublanguage | Commands | Acts on |
|---|---|---|
| DDL | CREATE TABLE, ALTER TABLE, DROP TABLE | the structure |
| DDL | PRIMARY KEY, FOREIGN KEY, NOT NULL | constraints on the structure |
| DML | SELECT, INSERT, UPDATE, DELETE | the data in the rows |
- PRIMARY KEY
- uniquely identifies a rowcannot be null or duplicated
- FOREIGN KEY
- points at another table's keyprevents references to rows that do not exist
- NOT NULL
- a constraintthe column must always hold a value
The shape of a query
Almost every question you will be asked to answer takes the same six-clause shape, and the clauses must appear in this order even though the database does not evaluate them in it.
What surprises people is that WHERE filters individual rows before any grouping, while HAVING filters groups after aggregation. So a condition on a raw column belongs in WHERE, and a condition on a COUNT or SUM belongs in HAVING. Putting an aggregate in a WHERE clause is an error.
- WHERE
- filters rowsbefore grouping — no aggregates allowed
- GROUP BY
- collapses rows into groupsone output row per distinct value
- HAVING
- filters groupsafter aggregation — aggregates allowed here
Using tables Student(StudentID, Surname, ClassID) and Class(ClassID, ClassName), write a query listing each class name with the number of students in it, for classes with more than 20 students, largest first.
- The two tables must be joined on ClassID: FROM Student JOIN Class ON Student.ClassID = Class.ClassIDThe class name lives in one table and the students in the other, so a join is required.
- Group by class name: GROUP BY Class.ClassNameOne output row per class, which is what "each class" asks for.
- Count the students: SELECT Class.ClassName, COUNT(*) AS TotalCOUNT(*) counts the rows in each group. Naming it with AS makes the output readable.
- Filter the groups: HAVING COUNT(*) > 20This is a condition on an aggregate, so it must be HAVING, not WHERE.
- Order the results: ORDER BY Total DESC;DESC gives largest first. The alias defined in SELECT can be used here.
SELECT Class.ClassName, COUNT(*) AS Total FROM Student JOIN Class ON Student.ClassID = Class.ClassID GROUP BY Class.ClassName HAVING COUNT(*) > 20 ORDER BY Total DESC;
Follow the rows through each clause. WHERE removes individual rows before grouping; HAVING removes whole groups afterwards — which is exactly why an aggregate condition cannot go in WHERE.
WHERE cannot contain an aggregate
WHERE COUNT(*) > 20 is invalid, because WHERE is applied to individual rows before any counting has happened — there is nothing to count yet. The condition belongs in HAVING. Conversely, filtering on a plain column belongs in WHERE, where it removes rows early and makes the query faster.
Changing the data
Three DML commands modify rows rather than reading them, and two of them are dangerous in the same way: they act on every row that matches, and if no condition is given, that means every row in the table.
- INSERT INTO
- adds a rowthe column list and value list must correspond
- UPDATE … SET
- changes existing rowsalways needs a WHERE unless you mean all of them
- DELETE FROM
- removes rowsDROP TABLE removes the table itself, which is different
Points examiners test
DELETEremoves rows;DROPremoves the whole table structure.- A missing
WHEREon UPDATE or DELETE affects every row. - Aggregate functions:
COUNT,SUM,AVG,MAX,MIN. - Strings go in single quotes; numbers do not.
LIKE 'A%'matches anything starting with A —%is any sequence of characters.- Qualify column names as
Table.Columnwhenever a join makes a name ambiguous.
Joins: bringing two tables together
Normalisation deliberately splits data across tables so nothing is stored twice. The cost is that answering a real question usually needs data from more than one of them, and a join is how they are recombined.
A join matches rows from two tables wherever a specified condition holds — almost always a foreign key in one table equalling a primary key in the other. The result behaves like a single wide table for the rest of the query.
The one that appears in nearly every exam question is the INNER JOIN, which keeps only rows that match on both sides. A student with no class, or a class with no students, simply does not appear in the output — which is usually what is wanted, and occasionally a trap.
- INNER JOIN
- the default kindkeeps only matching rows
- ON
- the matching conditionusually foreign key = primary key
- Table.Column
- qualified namesrequired when a name appears in both tables
Forgetting the ON condition
A join written without its ON clause pairs every row of one table with every row of the other — a cross join. Two tables of a thousand rows produce a million meaningless rows. If a query returns far more than expected, a missing or wrong join condition is the first thing to check.
Reading a query the way the database does
SQL is written in one order and evaluated in another, and knowing the evaluation order explains several rules that otherwise look arbitrary.
The database starts with FROM and any joins, assembling the working set of rows. Then WHERE discards rows. Then GROUP BY collapses what remains into groups, at which point aggregates are computed. Then HAVING discards groups. Only then is SELECT applied, choosing the columns, and finally ORDER BY sorts the output.
That order explains why an aggregate cannot appear in WHERE — the grouping has not happened yet. It also explains why an alias defined in SELECT can be used in ORDER BY but not in WHERE: by the time the sort runs the alias exists, but when the filter ran it did not.
| Order run | Clause | What it does |
|---|---|---|
| 1 | FROM / JOIN | assemble the rows |
| 2 | WHERE | discard individual rows |
| 3 | GROUP BY | collapse rows into groups |
| 4 | HAVING | discard whole groups |
| 5 | SELECT | choose the columns |
| 6 | ORDER BY | sort the result |