How Much Work Belongs to Each Asset?
Form a separate group for each asset, then calculate summaries without mixing visits from different equipment.
12 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
One Summary Was Not Enough for a Comparative Report
BOTIE
The maintenance version is concrete: place every P1 visit in one collection and every P2 visit in another. Aggregate each collection independently. This is a query result grouping, not a change to how the records must be stored.
Sources for this history
The Grand Total Hides Where the Work Went
The inspection total tells Botie how much recorded work the team did. It does not tell the supervisor which asset consumed that time. We need to place each visit with the other visits for the same asset before calculating.
GROUP BY defines those collections. The result changes from one row per visit to one row per asset that appears in the selected log. That last qualification will matter when we later include assets with no visits.
These maintenance records are invented. The workshop exposes record operations; the SQL worksheet runs actual SQLite queries.
Step 1 of 1
Show the membership before the subtotal
The group key determines which records can contribute together.
| Group | Visit IDs | Known durations |
|---|---|---|
| P1 | 101, 102, 106 | 30, NULL, 15 |
| P2 | 103, 105 | 0, 10 |
| P3 | 104 | 20 |
P1
- Visit IDs
- 101, 102, 106
- Known durations
- 30, NULL, 15
P2
- Visit IDs
- 103, 105
- Known durations
- 0, 10
P3
- Visit IDs
- 104
- Known durations
- 20
Follow the same records into the query
Every matching key contributes to the same collection regardless of source order.
Keep visits with the same asset together
Group by asset_id, sum minutes within each group, then order the resulting assets.
BOTIE
Asset-level duration summaries
[
{
"asset_id": "P1",
"total_minutes": 45
},
{
"asset_id": "P2",
"total_minutes": 10
},
{
"asset_id": "P3",
"total_minutes": 20
}
]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. Summarise each group
[ { "asset_id": "P1", "total_minutes": 45 }, { "asset_id": "P2", "total_minutes": 10 }, { "asset_id": "P3", "total_minutes": 20 } ]3. Order rows by named fields
[ { "asset_id": "P1", "total_minutes": 45 }, { "asset_id": "P2", "total_minutes": 10 }, { "asset_id": "P3", "total_minutes": 20 } ]4. Result
[ { "asset_id": "P1", "total_minutes": 45 }, { "asset_id": "P2", "total_minutes": 10 }, { "asset_id": "P3", "total_minutes": 20 } ]
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. Choose the grouping key from the requested grain
One row per asset means asset_id is the grouping key. Grouping by finding would answer a different question, even though it uses the same source visits. Multiple grouping columns define a combination, such as one row per asset and finding.
Predict how many groups the visible data should create before running the query. If the output contains more rows than expected, inspect which extra fields were included in GROUP BY.
The grouping key describes the new result grain.
2. Calculate inside each collection
GROUP BY partitions rows according to the selected key values. Aggregate functions then operate separately within each group. COUNT(*) counts the visits in that group; SUM(minutes) combines its known durations.
The select list should describe those group keys and their aggregates. Do not add inspection_id merely because it exists on the source rows. There is no single visit ID for an asset with several visits.
A grouped row contains facts about a group, not an arbitrary representative visit.
Compare the number of known durations by asset
The two counts reveal incomplete measurement coverage inside each group.
| 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 asset_id, COUNT(*) AS visits, COUNT(minutes) AS measured
FROM inspections GROUP BY asset_id ORDER BY asset_id;| asset_id | visits | measured |
|---|---|---|
| P1 | 3 | 2 |
| P2 | 2 | 2 |
| P3 | 1 | 1 |
P1 has three visits but only two recorded durations. A grand count would hide where the missing value occurs.
3. Keep all grouping fields in view
If the question becomes one subtotal per asset and finding, include both fields in GROUP BY and in the report. Otherwise two result rows can look identical while actually describing different groups.
NULL grouping values form a group rather than disappearing. This grouping rule differs from an equality predicate involving NULL. A missing finding can therefore appear as its own category in a findings summary.
The visible labels should be sufficient to identify which group a result describes.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
Which visits share a group?
Use the information below.
Visits for P1 have durations 30 and 15; P2 has duration 10. GROUP BY asset_id is requested.4. Recognise the missing group
GROUP BY over inspections can only produce groups from visits that survived the input filter. It cannot invent a P4 group when P4 has no inspection row. An asset-register report that must retain P4 needs a different source arrangement, which the left-join lesson will build.
An empty input produces no grouped rows. This differs from the single summary row of an aggregate query without GROUP BY.
The table chosen in FROM determines which entities are available to group.
5. Request the output order separately
A grouping implementation may sort keys internally or use another strategy. ORDER BY asset_id explicitly requests the report’s display order. Do not depend on a grouping plan to provide a stable presentation.
The workshop’s changed input interleaves visits for two assets. Group membership depends on key equality, not on adjacent rows or a change in the visible sequence.
A group can contain records that were far apart in the source list.
Check yourself
Questions on Parts 1 to 5
Question 1 of 2
Will P4 automatically appear?
Use the information below.
The asset register contains P4, but inspections has no P4 row. The query groups inspections by asset_id.Try it with Botie
Build with Botie
Write and run SQL