Which Inspections Took Longer Than the Usual Visit?
Use one query’s result inside another without losing track of whether that result is one value, several rows, or a value belonging to the current asset.
13 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
A Query Could Supply Another Query’s Input
BOTIE
Our inspection average is a small example of that composition. It keeps the calculation with the comparison, while leaving us responsible for the shape of the inner result and the meaning of missing values.
Sources for this history
The Comparison Value Is Still in the Records
Botie needs the inspections that took longer than the average recorded visit. We could calculate the average, copy it into another query, and remember to repeat both steps when a new visit arrives. That gives us a value that can become stale as soon as the records change.
Instead, let the comparison obtain its value from another query. We will first inspect that small result on its own, then connect it to the inspection rows.
The records are invented maintenance examples. The workshop exposes record operations; the SQL worksheet executes a real query in an isolated SQLite database.
Step 1 of 2
Inspect the inner result first
The known durations are 20, 40 and 60 minutes. The missing duration remains unknown.
| Calculation | Result |
|---|---|
| AVG(minutes) | 40 minutes |
AVG(minutes)
- Result
- 40 minutes
Step 2 of 2
Use the same value for each outer row
The predicate is strictly greater than that recorded mean.
| Inspection | Minutes | Greater than 40? |
|---|---|---|
| 1 | 20 | No |
| 2 | 40 | No |
| 3 | 60 | Yes |
| 4 | NULL | Unknown; not selected |
1
- Minutes
- 20
- Greater than 40?
- No
2
- Minutes
- 40
- Greater than 40?
- No
3
- Minutes
- 60
- Greater than 40?
- Yes
4
- Minutes
- NULL
- Greater than 40?
- Unknown; not selected
A scalar result becomes a comparison input
The aggregate and the outer rows depend on the same changing data.
Connect the recorded mean to the comparison
Calculate a mean from all known durations and feed it into the row comparison.
BOTIE
Inspection IDs and minutes
[
{
"inspection_id": 3,
"minutes": 60
}
]Follow the operations
1. Inspection records
[ { "inspection_id": 1, "minutes": 20 }, { "inspection_id": 2, "minutes": 40 }, { "inspection_id": 3, "minutes": 60 }, { "inspection_id": 4, "minutes": null } ]2. Summarise all rows
[ { "mean_minutes": 40 } ]3. Read the one-row result
40
4. Keep rows passing a comparison
[ { "inspection_id": 3, "minutes": 60 } ]5. Choose table columns
[ { "inspection_id": 3, "minutes": 60 } ]6. Order rows by named fields
[ { "inspection_id": 3, "minutes": 60 } ]7. Result
[ { "inspection_id": 3, "minutes": 60 } ]
The visible graph models this calculation. The SQL editor uses SQLite’s actual subquery implementation.
1. Run the quantity we need on its own
Begin with AVG(minutes) over the inspection table. AVG ignores NULL values, so this is the average of recorded durations. If every duration is missing, that result is NULL.
The inner query still has a shape: one column and one row when an aggregate without GROUP BY produces this result. Knowing that shape matters before placing it where a single value belongs.
A scalar means one value. It does not mean that the database forgot where the value came from.
Inspect the average before using it
The setup contains four inspections, one with an unknown duration.
| inspection_id | asset_id | minutes |
|---|---|---|
| 1 | P7 | 20 |
| 2 | P7 | 40 |
| 3 | P8 | 60 |
| 4 | P9 | NULL |
Database setup for this example
CREATE TABLE inspections(inspection_id INTEGER PRIMARY KEY, asset_id TEXT NOT NULL, minutes INTEGER);
INSERT INTO inspections VALUES (1,'P7',20),(2,'P7',40),(3,'P8',60),(4,'P9',NULL);SELECT AVG(minutes) AS mean_minutes FROM inspections;| mean_minutes |
|---|
| 40 |
The result has one value. The later filter will use that value rather than a typed-in threshold.
2. Put the result beside each inspection
A scalar subquery can appear on the right of the greater-than comparison. The outer query keeps each inspection whose own minutes exceed the supplied mean.
The independent inner query does not refer to the current outer row, so its meaning is the same for every inspection. This is a useful way to read the expression; the optimiser decides the physical execution plan.
Describe the dependency rather than claiming that the engine must literally execute the inner query once for every row.
3. Write the nested comparison
Parentheses identify the inner SELECT. Qualify the outer columns with an alias when another occurrence of the same table appears nearby. The final ORDER BY gives the report a reproducible inspection order.
The strict greater-than comparison excludes a visit exactly at the mean. A NULL duration also does not pass it, because that comparison is unknown.
Choose the comparison boundary from the question. Greater than and greater than or equal to select different visits.
Keep the visits above the recorded mean
The average is computed by the query, so another inserted visit changes both the benchmark and the candidates.
| inspection_id | asset_id | minutes |
|---|---|---|
| 1 | P7 | 20 |
| 2 | P7 | 40 |
| 3 | P8 | 60 |
| 4 | P9 | NULL |
Database setup for this example
CREATE TABLE inspections(inspection_id INTEGER PRIMARY KEY, asset_id TEXT NOT NULL, minutes INTEGER);
INSERT INTO inspections VALUES (1,'P7',20),(2,'P7',40),(3,'P8',60),(4,'P9',NULL);SELECT i.inspection_id, i.minutes
FROM inspections AS i
WHERE i.minutes > (SELECT AVG(minutes) FROM inspections)
ORDER BY i.inspection_id;| inspection_id | minutes |
|---|---|
| 3 | 60 |
Inspection 3 exceeds the mean on these records. That is a descriptive comparison, not a diagnosis of why its visit took longer.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
Which visit is selected?
Use the information below.
Known durations are 20, 40 and 60; a fourth duration is NULL. The query asks for minutes > AVG(minutes).4. Correlate the benchmark with the current asset
Suppose pumps have different routine workloads. Comparing each visit with its own asset’s recorded mean asks a different question. The inner WHERE can connect its asset_id to the outer alias.
The same alias must not accidentally refer only to the inner table. Writing inner_visit.asset_id = i.asset_id makes the dependency explicit. Read it as “for this outer inspection, average the visits with this asset ID.”
A correlated subquery depends on an outer value. It does not itself guarantee an efficient implementation.
Compare each visit with its asset’s own mean
P7 has two known durations; P8 has only one. A single known visit equals its own asset mean.
| inspection_id | asset_id | minutes |
|---|---|---|
| 1 | P7 | 20 |
| 2 | P7 | 40 |
| 3 | P8 | 60 |
| 4 | P9 | NULL |
Database setup for this example
CREATE TABLE inspections(inspection_id INTEGER PRIMARY KEY, asset_id TEXT NOT NULL, minutes INTEGER);
INSERT INTO inspections VALUES (1,'P7',20),(2,'P7',40),(3,'P8',60),(4,'P9',NULL);SELECT i.inspection_id, i.asset_id
FROM inspections AS i
WHERE i.minutes > (
SELECT AVG(v.minutes) FROM inspections AS v
WHERE v.asset_id = i.asset_id
)
ORDER BY i.inspection_id;| inspection_id | asset_id |
|---|---|
| 2 | P7 |
The selected visit changes because the reference population changed. The query is not interchangeable with the global-average version.
5. Make one-row assumptions explicit
A plain SELECT of minutes can return several rows. SQLite uses the first returned row when such a query is used as a scalar; other engines can reject a multiple-row scalar result. Neither behaviour establishes that the chosen row is the one the task requires.
Use an aggregate when the task asks for an aggregate. Use a unique key for a unique lookup. If the task asks for the latest row, specify its ordering and tie rule before LIMIT 1. A limit added only to suppress a cardinality problem hides the question rather than answering it.
The workshop’s one-row reader deliberately rejects multiple rows. It teaches the shape requirement without relying on SQLite’s permissive first-row behaviour.
6. Decide whether the task actually needs a value
Sometimes the question is simply whether an inspection exists. Returning a duration is then unnecessary, and missing durations make the result harder to interpret. The next lesson will use EXISTS for that question.
For an average comparison, keep a test where all values are NULL and another where every known duration is equal. Both should produce no unusually long visits under the strict rule.
An empty report can be correct. Check its cause before replacing NULL or loosening the threshold.
Check yourself
Questions on Parts 1 to 6
Question 1 of 2
What makes it unsuitable as a reliable latest-duration lookup?
Use the information below.
A scalar lookup SELECT minutes FROM inspections returns three rows, with no ORDER BY.Try it with Botie
Build with Botie
Write and run SQL