TEC-DATA85-05 · Lesson 8 · 11 clock hours · lab
Cleaning Data in SQL and Integrating Sources
Objective. Students will write SQL queries that perform data-type casting, TRIM, COALESCE, CASE-WHEN recoding, and JOIN-based integration to prepare a multi-table dataset in PostgreSQL for analysis.
⚠ Pending professional review
This lesson, its worked examples and its practice questions has not yet been verified by an industry-certified professional or by a certification or approval body. It is a study draft and may contain errors. Do not use it as instructions for work on a job site or on live equipment. Always follow your instructor, your supervising professional, and current official references and regulations.
VoTechWorks program approval and accreditation are still in progress. Credentials are issued only by the certifying bodies named in each program, after their own exams.
Why SQL Cleaning Matters
Raw operational data is almost never analysis-ready. Work-order systems, customer databases, and sensor logs are designed for transaction speed, not analytical accuracy. Before any chart or model can be trusted, you must resolve mismatched data types, inconsistent strings, unexpected NULLs, and records scattered across multiple tables. SQL is the most direct tool for this work because the cleaning happens inside the database, close to the data, without exporting files or risking version confusion.
This lab uses PostgreSQL, an open-source relational database used widely in government, healthcare, and technology. You will work in DBeaver or the psql command-line client.
Data Integrity and Workspace Safety
Before writing a single query, protect the source data. Always work on a copy of the dump, never the original file. In a professional environment, raw data is treated like evidence — altering it without an audit trail can invalidate analysis and, in regulated industries, create legal exposure. In this sandbox, your first action after loading the dump is to confirm you are connected to the correct schema and that you have only SELECT and CREATE VIEW privileges, not DROP or DELETE. If you are uncertain, ask your instructor before proceeding. This is the data-world equivalent of verifying a machine is de-energized before you touch it.
Understanding the Four Tables
The city work-order dataset arrives as four un-normalized tables:
- work_orders — one row per work order, with a numeric priority code and a status string
- locations — addresses and district codes for each site
- technicians — employee records including name fields entered by many different staff
- categories — lookup table mapping category codes to descriptions
Each table has its own quality problems. You will fix them individually using views before joining.
TRIM and LOWER: Standardizing Strings
`TRIM` removes leading and trailing whitespace from a string. Without it, the value `' Electrical '` will never match `'Electrical'` in a JOIN or a WHERE clause — they look the same on screen but differ in the database. `LOWER` converts all characters to lowercase so that `'SMITH'`, `'Smith'`, and `'smith'` collapse to one value.
Always apply both together on any string column used in a JOIN or GROUP BY:
```sql
SELECT TRIM(LOWER(technician_name)) AS technician_name
FROM technicians;
```
CAST: Fixing Data Types
When a numeric column is stored as `VARCHAR` — which happens when data is imported from CSV without a schema — arithmetic and comparisons break silently. `CAST` converts the stored value to the correct type:
```sql
SELECT CAST(estimated_hours AS NUMERIC(6,2)) AS estimated_hours
FROM work_orders;
```
If a value cannot be cast (for example, the string `'N/A'` cannot become a number), PostgreSQL raises an error. Inspect problem rows with a `WHERE` filter before casting the whole column.
COALESCE: Replacing NULLs
`COALESCE` returns the first non-NULL value from its argument list. It is the standard tool for filling gaps:
```sql
SELECT COALESCE(district_code, 'UNKNOWN') AS district_code
FROM locations;
```
In a LEFT JOIN, rows in the left table that have no match in the right table produce NULLs in every right-table column. Wrapping those columns in `COALESCE` prevents NULL propagation into your final dataset:
```sql
SELECT w.order_id,
COALESCE(c.category_desc, 'Uncategorized') AS category
FROM work_orders w
LEFT JOIN categories c ON w.category_code = c.category_code;
```
CASE-WHEN: Recoding Values
`CASE-WHEN` is SQL's conditional expression. Use it to translate numeric codes into meaningful labels that non-technical readers can understand:
```sql
CASE
WHEN priority_code = 1 THEN 'Critical'
WHEN priority_code = 2 THEN 'High'
WHEN priority_code = 3 THEN 'Medium'
ELSE 'Low'
END AS priority_label
```
Always include an `ELSE` clause. Without it, any value not matched by a `WHEN` branch returns NULL silently.
Saving Work as Views
A view stores a SELECT query under a name without copying data. After cleaning each table, save the result as a view so that your JOIN query references clean, consistent data:
```sql
CREATE OR REPLACE VIEW v_technicians AS
SELECT
tech_id,
TRIM(LOWER(first_name)) AS first_name,
TRIM(LOWER(last_name)) AS last_name
FROM technicians;
```
Validation: Checking Your Own Work
Cleaning without validation is incomplete. Run a final query that confirms three things: total row count, NULL counts in required columns, and referential consistency (every work order has a valid location). A simple approach:
```sql
SELECT
COUNT(*) AS total_rows,
COUNT(*) FILTER (WHERE district_code IS NULL) AS null_district,
COUNT(*) FILTER (WHERE technician_name IS NULL) AS null_tech
FROM v_analysis_ready;
```
Record every figure in the validation checklist. Zero NULLs in required columns and a row count matching the answer key are the acceptance criteria for this lab.
Key terms8
TRIMA SQL function that removes leading and trailing whitespace characters from a string value, preventing mismatches in joins and comparisons.
CASTA SQL expression that converts a value from one data type to another, such as converting a VARCHAR column to NUMERIC so arithmetic can be performed.
COALESCEA SQL function that evaluates its arguments in order and returns the first non-NULL value, used to replace NULLs with meaningful defaults.
CASE-WHENA SQL conditional expression that evaluates conditions in sequence and returns a specified value for the first condition that is true, similar to an if-else statement.
LEFT JOINA JOIN that returns all rows from the left table and matching rows from the right table; columns from the right table are NULL when no match exists.
ViewA named, stored SELECT query in a database that behaves like a table but does not physically copy the data; used here to encapsulate cleaned versions of each source table.
NULLA special marker in SQL indicating that a value is absent or unknown; NULLs are not the same as zero or an empty string and require specific functions to handle.
Referential consistencyThe condition in which every foreign key value in one table has a matching primary key value in the referenced table, confirming that related records exist and are correctly linked.
Worked examples4
1. The 'work_orders' table has a column 'priority_code' stored as VARCHAR. Write a SELECT that casts it to INTEGER and adds a CASE-WHEN column named 'priority_label' with labels Critical, High, Medium, and Low for codes 1–4.
- Start the SELECT and reference the table: SELECT order_id, status
- Add the CAST expression: CAST(priority_code AS INTEGER) AS priority_code
- Add the CASE-WHEN block: CASE WHEN CAST(priority_code AS INTEGER) = 1 THEN 'Critical' WHEN CAST(priority_code AS INTEGER) = 2 THEN 'High' WHEN CAST(priority_code AS INTEGER) = 3 THEN 'Medium' WHEN CAST(priority_code AS INTEGER) = 4 THEN 'Low' ELSE 'Unknown' END AS priority_label
- Close with FROM work_orders; and run the query to confirm all rows return a label and no NULLs appear in priority_label.
Answer: SELECT order_id, status, CAST(priority_code AS INTEGER) AS priority_code, CASE WHEN CAST(priority_code AS INTEGER) = 1 THEN 'Critical' WHEN CAST(priority_code AS INTEGER) = 2 THEN 'High' WHEN CAST(priority_code AS INTEGER) = 3 THEN 'Medium' WHEN CAST(priority_code AS INTEGER) = 4 THEN 'Low' ELSE 'Unknown' END AS priority_label FROM work_orders;
2. The 'technicians' table has columns 'first_name' and 'last_name' with inconsistent casing and extra spaces. Write the CREATE VIEW statement for 'v_technicians' that standardizes both columns.
- Begin with CREATE OR REPLACE VIEW v_technicians AS
- Write SELECT tech_id to retain the primary key.
- Apply TRIM and LOWER to first_name: TRIM(LOWER(first_name)) AS first_name
- Apply TRIM and LOWER to last_name: TRIM(LOWER(last_name)) AS last_name
- Close with FROM technicians; — run SELECT * FROM v_technicians; to confirm the output shows lowercase, trimmed names.
Answer: CREATE OR REPLACE VIEW v_technicians AS SELECT tech_id, TRIM(LOWER(first_name)) AS first_name, TRIM(LOWER(last_name)) AS last_name FROM technicians;
3. Using the four cleaned views (v_work_orders, v_locations, v_technicians, v_categories), write a LEFT JOIN query that produces a single analysis-ready result. The 'district_code' column from v_locations must never be NULL in output — replace any NULL with the string 'UNKNOWN'.
- Start with SELECT w.order_id, w.priority_label, w.status from v_work_orders w as the driving table.
- LEFT JOIN v_locations l ON w.location_id = l.location_id and select COALESCE(l.district_code, 'UNKNOWN') AS district_code.
- LEFT JOIN v_technicians t ON w.tech_id = t.tech_id and select t.first_name, t.last_name.
- LEFT JOIN v_categories c ON w.category_code = c.category_code and select COALESCE(c.category_desc, 'Uncategorized') AS category_desc.
- Run the query and verify in a follow-up COUNT(*) FILTER (WHERE district_code IS NULL) that zero NULLs remain.
Answer: SELECT w.order_id, w.priority_label, w.status, COALESCE(l.district_code, 'UNKNOWN') AS district_code, t.first_name, t.last_name, COALESCE(c.category_desc, 'Uncategorized') AS category_desc FROM v_work_orders w LEFT JOIN v_locations l ON w.location_id = l.location_id LEFT JOIN v_technicians t ON w.tech_id = t.tech_id LEFT JOIN v_categories c ON w.category_code = c.category_code;
4. Write a validation query against a view named 'v_analysis_ready' that returns: total row count, count of NULLs in 'district_code', and count of NULLs in 'priority_label'.
- Open SELECT and alias three aggregates.
- First aggregate: COUNT(*) AS total_rows
- Second aggregate: COUNT(*) FILTER (WHERE district_code IS NULL) AS null_district
- Third aggregate: COUNT(*) FILTER (WHERE priority_label IS NULL) AS null_priority_label
- Close with FROM v_analysis_ready; — expected result for a passing submission is null_district = 0 and null_priority_label = 0.
Answer: SELECT COUNT(*) AS total_rows, COUNT(*) FILTER (WHERE district_code IS NULL) AS null_district, COUNT(*) FILTER (WHERE priority_label IS NULL) AS null_priority_label FROM v_analysis_ready;
In class: Students receive four un-normalized PostgreSQL tables representing a city's work-order system; they write SELECT queries using TRIM, LOWER, CAST, and COALESCE to standardize each table individually, then save results as views. · Students write a multi-table JOIN that integrates the four views into a single analysis-ready table; they must handle NULLs from LEFT JOINs explicitly using COALESCE and add a CASE-WHEN column that recodes numeric priority codes into readable labels. · Students run a final validation query that counts rows, checks for NULLs in required columns, and verifies referential consistency; they paste results into a validation checklist and submit both the SQL script and the completed checklist.
Materials: PostgreSQL sandbox, DBeaver or psql, work-order SQL dump file, validation checklist template
Check: Validation checklist must show zero NULLs in required columns and row counts matching instructor answer key (±0 rows); SQL script reviewed for correct use of TRIM, COALESCE, CAST, CASE-WHEN, and JOIN; pass at 80 % on a correctness rubric.
Practice check8
Answer each question, then check it. Nothing is recorded; this is your own practice.
Record this lesson on my transcript. Sign in with your key and enrollment email to record lessons (enroll free if you have no key). This page stays open to read and practise.