How Much Inspection Time Did We Actually Record?
Turn visit rows into a summary without confusing the number of visits with the number of known durations.
13 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
A Report Needed Fewer Rows Than the Log
BOTIE
SQLite documents its aggregate treatment of missing and empty inputs. The maintenance example uses those rules to distinguish visits that exist from durations that were actually measured.
Sources for this history
Six Visits Do Not Give Us Six Known Durations
The supervisor wants a total and an average inspection duration. Botie has six visit records, but one has no minutes entered. Another records zero. Dividing a known total by every row would silently assign a zero contribution to the unknown visit.
Before writing an aggregate, decide what it is summarising. SQL provides separate operations for counting rows, counting nonmissing values and combining those values. We will keep those quantities visible together.
These maintenance records are invented. The workshop exposes record operations; the SQL worksheet runs actual SQLite queries.
Step 1 of 1
Count the records and known values separately
These three visits demonstrate why the denominator matters.
| Visit | minutes | Contribution |
|---|---|---|
| A | 30 | One visit; one known duration |
| B | 0 | One visit; one known duration |
| C | NULL | One visit; no known duration |
A
- minutes
- 30
- Contribution
- One visit; one known duration
B
- minutes
- 0
- Contribution
- One visit; one known duration
C
- minutes
- NULL
- Contribution
- One visit; no known duration
Follow the same records into the query
Filtering defines the input population; the aggregate reports only its known durations.
Total the time for loose-component findings
Filter to the requested finding before summing known durations. An entirely unknown selection must remain unknown.
BOTIE
Known minutes for the selected finding
[
{
"total_minutes": 55
}
]Follow the operations
1. Inspection visits
[ { "inspection_id": 101, "asset_id": "P1", "minutes": 30, "finding": "loose" }, { "inspection_id": 102, "asset_id": "P1", "minutes": null, "finding": null }, { "inspection_id": 103, "asset_id": "P2", "minutes": 0, "finding": "clear" }, { "inspection_id": 104, "asset_id": "P3", "minutes": 20, "finding": "clear" }, { "inspection_id": 105, "asset_id": "P2", "minutes": 10, "finding": "loose" }, { "inspection_id": 106, "asset_id": "P1", "minutes": 15, "finding": "loose" } ]2. Text comparison value
loose
3. Keep rows passing a comparison
[ { "inspection_id": 101, "asset_id": "P1", "minutes": 30, "finding": "loose" }, { "inspection_id": 105, "asset_id": "P2", "minutes": 10, "finding": "loose" }, { "inspection_id": 106, "asset_id": "P1", "minutes": 15, "finding": "loose" } ]4. Summarise all rows
[ { "total_minutes": 55 } ]5. Result
[ { "total_minutes": 55 } ]
The workshop calculates over small record lists to make the relational operations visible. It is not a SQL parser or query planner. The worksheet runs SQLite and is the authority for SQL behaviour, including types and NULL.
1. Decide what the summary row represents
The source grain is one inspection. An aggregate query without GROUP BY creates one summary over its input rows. The result no longer describes a particular visit.
That is why an inspection ID beside a total needs explanation. Which visit would it refer to? Portable aggregate queries select aggregates or explicit grouping keys rather than an unrelated row’s ID. SQLite permits some bare-column forms, but they can conceal an unintended result.
The summary’s grain is the selected collection of visits.
2. Count rows and values for different purposes
COUNT(*) counts input rows. COUNT(minutes) counts rows where minutes is not NULL. A recorded zero is a non-null value and is counted. COUNT(DISTINCT asset_id) instead asks how many different non-null asset IDs occur.
Keeping both visit count and measured-duration count exposes coverage. A total derived from a small measured subset can look authoritative if the missing observations are omitted from the report.
The name inside COUNT changes the question being answered.
3. Combine known values while preserving the missingness caveat
SUM and AVG operate on known numeric values in our schema. AVG’s denominator is the number of non-null values, not the number of source rows. MIN and MAX return the extremes of the known values.
These functions describe what is present. They do not estimate the missing duration or correct a biased collection process. If the longest visits are the ones technicians forget to enter, a known-only average can still mislead.
Report the population and coverage with the result.
Check the range and the number of assets visited
This report combines values from the visit log without pretending it contains all assets in the register.
| asset_id | site | kind | active |
|---|---|---|---|
| P1 | north | pump | 1 |
| P2 | north | fan | 1 |
| P3 | south | pump | 0 |
| P4 | south | pump | 1 |
| inspection_id | asset_id | minutes | finding |
|---|---|---|---|
| 101 | P1 | 30 | loose |
| 102 | P1 | NULL | NULL |
| 103 | P2 | 0 | clear |
| 104 | P3 | 20 | clear |
| 105 | P2 | 10 | loose |
| 106 | P1 | 15 | loose |
| reading_id | asset_id | temperature |
|---|---|---|
| 1 | P1 | 70 |
| 2 | P1 | 80 |
| 3 | P2 | NULL |
| 4 | P3 | 65 |
Database setup for this example
CREATE TABLE assets (
asset_id TEXT PRIMARY KEY NOT NULL,
site TEXT NOT NULL,
kind TEXT NOT NULL,
active INTEGER NOT NULL CHECK (active IN (0, 1))
);
CREATE TABLE inspections (
inspection_id INTEGER PRIMARY KEY,
asset_id TEXT NOT NULL REFERENCES assets(asset_id),
minutes INTEGER CHECK (minutes >= 0),
finding TEXT
);
CREATE TABLE readings (
reading_id INTEGER PRIMARY KEY,
asset_id TEXT NOT NULL REFERENCES assets(asset_id),
temperature REAL
);
INSERT INTO assets VALUES
('P1','north','pump',1), ('P2','north','fan',1),
('P3','south','pump',0), ('P4','south','pump',1);
INSERT INTO inspections VALUES
(101,'P1',30,'loose'), (102,'P1',NULL,NULL),
(103,'P2',0,'clear'), (104,'P3',20,'clear'),
(105,'P2',10,'loose'), (106,'P1',15,'loose');
INSERT INTO readings VALUES
(1,'P1',70.0), (2,'P1',80.0), (3,'P2',NULL), (4,'P3',65.0);SELECT COUNT(DISTINCT asset_id) AS visited_assets,
MIN(minutes) AS shortest, MAX(minutes) AS longest
FROM inspections;| visited_assets | shortest | longest |
|---|---|---|
| 3 | 0 | 30 |
The zero-minute record participates in the range. P4 has no visit and does not appear in this count.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
What does COUNT(minutes) return?
Use the information below.
Durations are 30, 0 and NULL.4. Understand an empty selection before adding a fallback
An aggregate query without GROUP BY still returns one row when no source rows qualify. COUNT returns zero; SUM and AVG return NULL when no non-null values exist. An all-null selection has the same missing numeric aggregate, although its row count can be positive.
SQLite also has TOTAL, which returns a floating-point zero for no non-null inputs. It is not a portable substitute with the same meaning as SUM. COALESCE(SUM(...),0) is an explicit policy choice: it may be suitable for no expenditure, but it can conceal unrecorded work.
Zero work and no known measurement should be distinguished when the report depends on that difference.
5. Filter the population before calculating
WHERE determines which visits reach the aggregate. Summing durations after finding = loose answers a different question from summing all visits and labelling the total loose afterwards.
The workshop makes this ordering visible. The comparison emits a smaller record list, and the sum component consumes that list. Try the all-missing loose visits case to see why a numeric zero is not an acceptable replacement under the stated rule.
The filter defines the report population; the aggregate describes that population.
6. Keep units and precision in the result contract
A duration total and an average both use minutes, while counts use records or assets. Name columns so a downstream reader can distinguish them. Rounding is a display decision; preserve sufficient precision for later calculations.
For money or quantities requiring exact decimal arithmetic, consider the database’s numeric types and units carefully. Our integer minutes avoid that issue for sums, while averages can be fractional.
A correct formula with the wrong unit still answers the wrong question.
Check yourself
Questions on Parts 1 to 6
Question 1 of 2
Which result shape is expected?
Use the information below.
No rows satisfy the WHERE condition. The query selects COUNT(*), SUM(minutes) without GROUP BY.Try it with Botie
Build with Botie
Write and run SQL