The example problems in this set give practice toward the following module outcome:
Each problem asks you to examine a realistic table schema, identify which warning signs are present, and explain why each sign indicates a design problem. This targets the Analyze level of Bloom's Taxonomy — you must break the schema apart, distinguish its structural properties, and draw reasoned conclusions rather than simply recall definitions.
Outcome practised: MO3 [Analyze]
Problem Statement
A developer has created the following table to store customer orders for an online store:
| Column Name | Data Type | Notes |
|---|---|---|
| order_id | INT | Primary Key |
| customer_name | VARCHAR(100) | |
| item1_name | VARCHAR(100) | |
| item1_qty | INT | |
| item1_price | DECIMAL(8,2) | |
| item2_name | VARCHAR(100) | |
| item2_qty | INT | |
| item2_price | DECIMAL(8,2) | |
| item3_name | VARCHAR(100) | |
| item3_qty | INT | |
| item3_price | DECIMAL(8,2) |
Diagnose the warning sign(s) of poor table design present in this schema and explain the practical consequences.
Solution
The primary key is order_id. Examine the remaining columns: item1_name, item1_qty, item1_price, item2_name, item2_qty, item2_price, item3_name, item3_qty, item3_price. Notice that the same three attributes — name, qty, and price — appear three times, each time with a numeric suffix (1, 2, 3). This is the definition of a repeating group: a set of logically identical columns distinguished only by a positional number.
Repeating groups — the schema encodes a one-to-many relationship (one order can have many items) by adding numbered column sets instead of using a separate table. This violates First Normal Form (1NF), which requires that every column hold a single, atomic value and that there be no repeating column groups.
item1_name, item2_name, and item3_name with OR conditions instead of a single column scan.SUM() aggregate on one column.The repeating group should be extracted into a child table, for example order_items(order_item_id, order_id, item_name, qty, price), with order_id as a foreign key back to the orders table. Each item becomes its own row, eliminating the hard limit and simplifying all queries.
Outcome practised: MO3 [Analyze]
Problem Statement
A school database stores the following information in a single table:
| Column Name | Data Type | Notes |
|---|---|---|
| enrollment_id | INT | Primary Key |
| student_id | INT | |
| student_first_name | VARCHAR(50) | |
| student_last_name | VARCHAR(50) | |
| student_email | VARCHAR(100) | |
| course_id | INT | |
| course_title | VARCHAR(100) | |
| course_credits | INT | |
| instructor_name | VARCHAR(100) | |
| enrollment_date | DATE | |
| grade | CHAR(2) |
Diagnose the warning sign(s) of poor design and explain the anomalies that result.
Solution
Ask: "What things is this table actually describing?" Careful inspection reveals three separate real-world entities bundled together:
student_id, student_first_name, student_last_name, student_email.course_id, course_title, course_credits, instructor_name.enrollment_id, enrollment_date, grade.Mixed entity types — the table conflates three logically distinct entities (Student, Course, Enrollment) into one structure. A well-designed schema should have one table per entity, with relationship tables (like Enrollment) holding only the foreign keys and attributes that truly belong to the relationship itself.
course_id appears must be updated. Miss even one row and the database contains contradictory course titles for the same course.
Separate the three entities into three tables: students(student_id, first_name, last_name, email), courses(course_id, title, credits, instructor_name), and enrollments(enrollment_id, student_id, course_id, enrollment_date, grade). The enrollments table holds foreign keys to both students and courses, plus only the attributes that genuinely belong to the enrollment event.
Outcome practised: MO3 [Analyze]
Problem Statement
A retail database uses the following table to track which products appear on which purchase orders. The composite primary key is (po_id, product_id).
| Column Name | Data Type | Notes |
|---|---|---|
| po_id | INT | Part of Primary Key |
| product_id | INT | Part of Primary Key |
| quantity_ordered | INT | |
| unit_price_at_order | DECIMAL(8,2) | |
| product_name | VARCHAR(100) | |
| product_category | VARCHAR(50) | |
| supplier_name | VARCHAR(100) | |
| po_date | DATE | |
| po_status | VARCHAR(20) |
Diagnose any partial-key dependencies and explain why they indicate poor design.
Solution
The primary key is (po_id, product_id) — both columns together uniquely identify a row. A partial-key dependency exists when a non-key column can be determined by only part of the composite key, rather than requiring all parts. This violates Second Normal Form (2NF).
Ask: "To know this column's value, do I need both po_id and product_id, or just one of them?"
| Column | Depends on po_id alone? | Depends on product_id alone? | Depends on both? | Verdict |
|---|---|---|---|---|
| quantity_ordered | No | No | Yes — how many of this product on this PO | Full dependency ✓ |
| unit_price_at_order | No | No | Yes — negotiated price for this product on this PO | Full dependency ✓ |
| product_name | No | Yes — a product has one name regardless of PO | No | Partial dependency ✗ |
| product_category | No | Yes — category belongs to the product, not the PO | No | Partial dependency ✗ |
| supplier_name | No | Yes — supplier is a property of the product | No | Partial dependency ✗ |
| po_date | Yes — a PO has one date regardless of which product | No | No | Partial dependency ✗ |
| po_status | Yes — status belongs to the PO, not a specific product line | No | No | Partial dependency ✗ |
product_name, product_category, supplier_name) depend only on product_id: If product 42 appears on 500 purchase orders, its name and category are stored 500 times. Changing the product name requires updating 500 rows; missing any one row creates an inconsistency. A new product also cannot be stored until it appears on a PO (insert anomaly), and deleting all POs for a product erases the product's attributes (delete anomaly).
po_date, po_status) depend only on po_id: A purchase order with 20 line items stores the same date and status 20 times. Updating the status (e.g., from "Pending" to "Shipped") requires updating 20 rows atomically or risk inconsistent status values within the same PO.
Decompose the table to remove partial dependencies:
products(product_id, product_name, product_category, supplier_name) — product attributes move here.purchase_orders(po_id, po_date, po_status) — PO-level attributes move here.po_line_items(po_id, product_id, quantity_ordered, unit_price_at_order) — only fully dependent attributes remain in the junction table.Now every non-key attribute in every table depends on the whole key, satisfying 2NF.
Outcome practised: MO3 [Analyze]
Problem Statement
A small business uses a single spreadsheet-turned-database table to manage its operations. Examine the schema below and diagnose all warning signs of poor design that are present, citing evidence from the schema for each one.
| Column Name | Data Type | Notes |
|---|---|---|
| invoice_id | INT | Primary Key |
| client_id | INT | |
| client_name | VARCHAR(100) | |
| client_email | VARCHAR(100) | |
| client_billing_address | VARCHAR(200) | |
| service1_description | VARCHAR(200) | |
| service1_hours | DECIMAL(5,2) | |
| service1_rate | DECIMAL(8,2) | |
| service2_description | VARCHAR(200) | |
| service2_hours | DECIMAL(5,2) | |
| service2_rate | DECIMAL(8,2) | |
| service3_description | VARCHAR(200) | |
| service3_hours | DECIMAL(5,2) | |
| service3_rate | DECIMAL(8,2) | |
| invoice_date | DATE | |
| payment_status | VARCHAR(20) | |
| tax_rate | DECIMAL(4,3) |
Solution
Look for columns with numbered suffixes that encode the same logical concept multiple times:
service1_description, service2_description, service3_descriptionservice1_hours, service2_hours, service3_hoursservice1_rate, service2_rate, service3_rateWarning sign confirmed: Repeating groups. The three attributes description, hours, and rate repeat three times with numeric suffixes. This caps an invoice at exactly 3 line items, wastes space when fewer than 3 services are billed, and forces multi-column OR searches to find all invoices for a given service type.
Identify the distinct real-world things described by the columns:
client_id, client_name, client_email, client_billing_address — these describe a client, not an invoice.invoice_id, invoice_date, payment_status, tax_rate — these describe the invoice transaction.serviceN_description, serviceN_hours, serviceN_rate — these describe individual service charges.Warning sign confirmed: Mixed entity types. Three distinct entities (Client, Invoice, Invoice Line Item) are collapsed into one table. This causes update anomalies (changing a client's email requires finding and updating every invoice row for that client), insert anomalies (a new client cannot be stored without an invoice), and delete anomalies (deleting the last invoice for a client erases all client contact information).
The primary key is the single column invoice_id. Partial-key dependencies require a composite primary key, so in their classic form they do not technically apply here. However, note that client_name, client_email, and client_billing_address are functionally determined by client_id alone — not by invoice_id. This is a transitive dependency (a closely related design flaw), where a non-key column (client_name) depends on another non-key column (client_id) rather than directly on the primary key. This is the root structural cause of the mixed-entity-type anomalies identified in Step 2.
| Warning Sign | Evidence in Schema | Key Consequence |
|---|---|---|
| Repeating groups | service1_*, service2_*, service3_* column sets |
Hard cap on line items; complex queries; NULL waste |
| Mixed entity types | Client attributes and Invoice attributes in same table | Update, insert, and delete anomalies; data redundancy |
| Transitive dependency | client_name/email/address determined by client_id, not invoice_id |
Client data duplicated across every invoice row |
Decompose into three tables:
clients(client_id, client_name, client_email, client_billing_address)invoices(invoice_id, client_id, invoice_date, payment_status, tax_rate)invoice_line_items(line_item_id, invoice_id, service_description, hours, rate)This eliminates all three warning signs: repeating groups become rows in invoice_line_items; entity types are separated; and client attributes live only once in the clients table.