Which Columns Answer the Question?
A report needs the kinds of equipment at each site. Choose the relevant fields and decide exactly which repeated rows should disappear.
12 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
Choose a Result Without Writing a Storage Walk
BOTIE
Selecting fields and eliminating repeated result rows remain separate choices in SQL. That distinction lets the maintenance register support both an asset list and a site-category report.
Sources for this history
The Stores Team Needs Categories, Not Every Asset
Botie’s asset register is useful to technicians, but the stores team has a different question: which kinds of equipment are present at each site? They do not need a separate line for every pump.
We can first keep site and kind, then remove repeated pairs. That order matters. Two pumps at the same site give the stores team one category, while pumps at different sites still need separate lines.
These maintenance records are invented. The workshop exposes record operations; the SQL worksheet runs actual SQLite queries.
Step 1 of 1
Different assets can become the same projected row
The source IDs distinguish assets. The requested report deliberately drops those IDs.
| Source asset | Projected site | Projected kind |
|---|---|---|
| P3 | south | pump |
| P4 | south | pump |
P3
- Projected site
- south
- Projected kind
- pump
P4
- Projected site
- south
- Projected kind
- pump
Follow the same records into the query
Duplicate identity is defined by the selected result fields.
Which pairs are actually repeated?
Project site and kind before removing complete duplicate rows. Then order the pairs for the report.
BOTIE
Distinct site and kind pairs
[
{
"site": "north",
"kind": "fan"
},
{
"site": "north",
"kind": "pump"
},
{
"site": "south",
"kind": "pump"
}
]Follow the operations
1. Asset register
[ { "asset_id": "P1", "site": "north", "kind": "pump" }, { "asset_id": "P2", "site": "north", "kind": "fan" }, { "asset_id": "P3", "site": "south", "kind": "pump" }, { "asset_id": "P4", "site": "south", "kind": "pump" } ]2. Choose table columns
[ { "site": "north", "kind": "pump" }, { "site": "north", "kind": "fan" }, { "site": "south", "kind": "pump" }, { "site": "south", "kind": "pump" } ]3. Keep distinct rows
[ { "site": "north", "kind": "pump" }, { "site": "north", "kind": "fan" }, { "site": "south", "kind": "pump" } ]4. Order rows by named fields
[ { "site": "north", "kind": "fan" }, { "site": "north", "kind": "pump" }, { "site": "south", "kind": "pump" } ]5. Result
[ { "site": "north", "kind": "fan" }, { "site": "north", "kind": "pump" }, { "site": "south", "kind": "pump" } ]
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 fields before worrying about duplicate lines
The question asks for a site and an equipment kind. Keeping asset_id as well would make P3 and P4 different result rows, so DISTINCT would preserve both. Dropping site would collapse pumps from different locations into a single category.
Projection is the name for selecting fields. Think about what one row of the requested result should mean before writing the select list.
For this report the grain is one site-kind pair.
2. Name the output columns deliberately
A SELECT list can contain stored columns or expressions. AS gives an expression a useful output label. For example, active AS is_active labels the returned value without renaming the stored column. Aliases are part of the result interface another program sees.
SELECT * is convenient for a first inspection, but it makes a long-lived report depend on every source column. A new database field can unexpectedly appear in an export. Naming columns also makes the intended grain easier to review.
An alias changes a result label; it does not change the underlying values.
Give a report column a useful label
Read identifiers and the stored active flags. The alias explains what those flags describe.
| 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, active AS is_active
FROM assets
ORDER BY asset_id;| asset_id | is_active |
|---|---|
| P1 | 1 |
| P2 | 1 |
| P3 | 0 |
| P4 | 1 |
The output values still come from the active column. The table was not altered.
3. Apply DISTINCT to the whole selected row
SELECT DISTINCT site, kind removes rows that repeat both selected values. A repeated site alone is insufficient, because north has a fan and a pump. A repeated kind alone is also insufficient, because pump occurs at several sites.
SQL retains duplicates by default. This differs from the mathematical idea of a relation as a set of tuples. Use DISTINCT when the question genuinely asks for unique combinations, not as a general way to make an unexpectedly large answer look tidier.
Duplicate removal can hide evidence of a faulty join. We will return to that problem when tables are connected.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
Which row is repeated as a whole?
Use the information below.
Rows after selecting site and kind are (north,pump), (north,fan), (south,pump), (south,pump).4. Keep calculations and their labels together
An expression can calculate a value for each row, such as a duration in hours from stored minutes. Make its unit clear in the alias. In SQLite, dividing two integers can produce integer division, so use a real-valued operand when a fraction is needed.
Expressions do not automatically combine rows. A calculation in the SELECT list usually produces one value per surviving row; aggregation is a separate operation that comes later.
The decimal operand requests fractional arithmetic. A missing duration still produces a missing derived value.
Convert each recorded duration to hours
ROUND rounds the displayed fractional result to two decimal places. It does not change the stored minutes.
| 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 inspection_id, ROUND(minutes / 60.0, 2) AS hours FROM inspections ORDER BY inspection_id;| inspection_id | hours |
|---|---|
| 101 | 0.5 |
| 102 | NULL |
| 103 | 0 |
| 104 | 0.33 |
| 105 | 0.17 |
| 106 | 0.25 |
Each visit retains its own result. The unknown duration remains unknown.
5. Specify the order of the report as well
DISTINCT determines which combinations survive. ORDER BY determines how to present them. Those are different requests, so do not depend on the apparent order a database happens to use while eliminating duplicates.
Our challenge sorts by site and then kind. The workshop deliberately includes a duplicate pair under a new asset ID; a construction that keeps the ID through duplicate removal will fail.
Read the output as a set of site-category facts displayed in a chosen order.
Check yourself
Questions on Parts 1 to 5
Question 1 of 2
What does AS change?
Use the information below.
SELECT active AS is_active FROM assets;Try it with Botie
Build with Botie
Write and run SQL