MOO WebTech
Final Assessmentassignmentadvanced

Final SQL Assignment (Task Release)

Final assignment release: comprehensive SQL task covering all course topics.

80 minL51

Assignment

Final SQL Assignment — Comprehensive Relational Database Implementation

You will design and implement a complete relational database for an assigned subject area. The assignment covers every major topic from the PostgreSQL track: schema design, data manipulation, querying, access control, and transaction management.

The assignment is divided into 5 sections totalling 40 points. Each section is submitted as a separate .sql file inside a single submission folder.

Your instructor will assign each student a domain (e.g., library, hospital, online store, university, airline). All work must be based on that domain — do not change it after receiving it.

Requirements

Section 1 — Schema Design (DDL) — 10 points

Design a relational schema with at least 6 tables for your assigned domain.

  1. Use CREATE TABLE IF NOT EXISTS for every table
  2. Apply PRIMARY KEY, FOREIGN KEY, NOT NULL, and UNIQUE constraints where appropriate
  3. Include at least one GENERATED ALWAYS AS computed column
  4. Include at least one column with a DEFAULT value (other than a serial PK)
  5. Add at least 3 CHECK constraints across your tables
  6. Write at least 3 ALTER TABLE operations — add a column, add a constraint, rename or modify something. Each ALTER must have a comment explaining what change you are making and why.
SQL
-- Example structure (adapt to your own domain)
CREATE TABLE IF NOT EXISTS departments (
    dept_id     SERIAL PRIMARY KEY,
    dept_name   VARCHAR(100) NOT NULL UNIQUE,
    location    VARCHAR(200),
    created_at  TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS employees (
    emp_id        SERIAL PRIMARY KEY,
    full_name     VARCHAR(200) NOT NULL,
    dept_id       INT NOT NULL REFERENCES departments(dept_id),
    hire_date     DATE NOT NULL CHECK (hire_date >= '2000-01-01'),
    salary        NUMERIC(10,2) CHECK (salary >= 0),
    status        VARCHAR(20) DEFAULT 'active'
                  CHECK (status IN ('active', 'on_leave', 'terminated')),
    full_name_len INT GENERATED ALWAYS AS (LENGTH(full_name)) STORED
);

-- ALTER TABLE examples with explanatory comments
-- Adding an email column that was missed in the initial design
ALTER TABLE employees ADD COLUMN IF NOT EXISTS email VARCHAR(255);

-- Enforcing email uniqueness once the column exists
ALTER TABLE employees ADD CONSTRAINT uq_employee_email UNIQUE (email);

-- Adding a non-empty location constraint to departments
ALTER TABLE departments ADD CONSTRAINT chk_location_not_empty
    CHECK (location IS NULL OR LENGTH(TRIM(location)) > 0);

Section 2 — Data Population (DML) — 10 points

Write a re-runnable INSERT script. Re-runnable means: run it once, run it again, the row counts are identical both times.

  1. Start with a TRUNCATE block — truncate all tables in the correct order (child tables first, then parent tables) or use CASCADE with care
  2. Insert at least 5 rows into every table
  3. Do not hardcode FK IDs — resolve foreign keys using subqueries
  4. Include at least one INSERT INTO ... SELECT statement
  5. Include at least one CTE + INSERT pattern
  6. Use ON CONFLICT DO NOTHING where a UNIQUE constraint could cause duplicate errors
SQL
-- Re-runnable: truncate in child-first order
TRUNCATE employees, departments RESTART IDENTITY CASCADE;

-- Parent table insert
INSERT INTO departments (dept_name, location) VALUES
    ('Engineering',   'Building A'),
    ('Marketing',     'Building B'),
    ('Finance',       'Building C'),
    ('HR',            'Building A'),
    ('Operations',    'Building D')
ON CONFLICT DO NOTHING;

-- Child table insert: FK resolved via subquery, no hardcoded IDs
INSERT INTO employees (full_name, dept_id, hire_date, salary, status, email)
VALUES
    ('Alice Johnson',
     (SELECT dept_id FROM departments WHERE dept_name = 'Engineering'),
     '2022-03-15', 85000.00, 'active', 'alice@example.com'),
    ('Bob Smith',
     (SELECT dept_id FROM departments WHERE dept_name = 'Marketing'),
     '2021-07-01', 72000.00, 'active', 'bob@example.com')
ON CONFLICT DO NOTHING;

-- INSERT INTO ... SELECT example
INSERT INTO employees (full_name, dept_id, hire_date, salary, status, email)
SELECT
    'Contractor: ' || dept_name,
    dept_id,
    CURRENT_DATE,
    55000.00,
    'active',
    LOWER(dept_name) || '.contractor@example.com'
FROM departments
WHERE location = 'Building A'
ON CONFLICT DO NOTHING;

-- CTE + INSERT example
WITH new_staff AS (
    SELECT
        'Intern ' || dept_name AS full_name,
        dept_id,
        CURRENT_DATE AS hire_date,
        25000.00     AS salary,
        'active'     AS status,
        LOWER(dept_name) || '.intern@example.com' AS email
    FROM departments
    WHERE dept_name IN ('HR', 'Finance')
)
INSERT INTO employees (full_name, dept_id, hire_date, salary, status, email)
SELECT full_name, dept_id, hire_date, salary, status, email
FROM new_staff
ON CONFLICT DO NOTHING;

Section 3 — SELECT Queries — 10 points

Write exactly 6 queries, numbered and commented. Each query must be tested — it must return at least one row.

  1. Basic — SELECT with WHERE, ORDER BY, and LIMIT
  2. Multi-table JOIN — join at least 3 tables
  3. Aggregate — GROUP BY with HAVING
  4. Subquery — a subquery in WHERE or FROM
  5. CTE — a Common Table Expression
  6. Combined — JOIN + aggregate + HAVING in one query
SQL
-- Query 1: Basic — active employees hired after 2021, newest first
SELECT emp_id, full_name, hire_date, salary
FROM employees
WHERE status = 'active'
  AND hire_date > '2021-01-01'
ORDER BY hire_date DESC
LIMIT 10;

-- Query 2: Multi-table JOIN (adapt to your own 3+ tables)
SELECT
    e.full_name,
    d.dept_name,
    d.location
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE e.status = 'active'
ORDER BY d.dept_name, e.full_name;

-- Query 3: Aggregate — headcount and avg salary per department
SELECT
    d.dept_name,
    COUNT(e.emp_id)           AS headcount,
    ROUND(AVG(e.salary), 2)   AS avg_salary,
    MAX(e.salary)             AS max_salary
FROM departments d
JOIN employees e ON d.dept_id = e.dept_id
GROUP BY d.dept_name
HAVING COUNT(e.emp_id) > 1
ORDER BY avg_salary DESC;

-- Query 4: Subquery — employees earning above company average
SELECT full_name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
ORDER BY salary DESC;

-- Query 5: CTE — rank employees by salary within each department
WITH ranked AS (
    SELECT
        full_name,
        dept_id,
        salary,
        RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS salary_rank
    FROM employees
    WHERE status = 'active'
)
SELECT
    r.full_name,
    d.dept_name,
    r.salary,
    r.salary_rank
FROM ranked r
JOIN departments d ON r.dept_id = d.dept_id
WHERE r.salary_rank = 1
ORDER BY d.dept_name;

-- Query 6: Combined JOIN + aggregate + HAVING
SELECT
    d.dept_name,
    d.location,
    COUNT(e.emp_id)     AS total_employees,
    SUM(e.salary)       AS total_payroll
FROM departments d
JOIN employees e ON d.dept_id = e.dept_id
WHERE e.status = 'active'
GROUP BY d.dept_name, d.location
HAVING SUM(e.salary) > 100000
ORDER BY total_payroll DESC;

Section 4 — DCL — 5 points

Set up a two-role permission model for your schema:

  1. Create role schema_reader with SELECT on all your tables
  2. Create role schema_writer with SELECT, INSERT, UPDATE on all your tables
  3. Create two users: one for each role
  4. Grant USAGE on the schema to both roles
  5. Assign each user the appropriate role
SQL
-- Create roles
CREATE ROLE schema_reader;
CREATE ROLE schema_writer;

-- Grant schema access (required before table-level grants take effect)
GRANT USAGE ON SCHEMA public TO schema_reader;
GRANT USAGE ON SCHEMA public TO schema_writer;

-- Reader: SELECT only on all tables
GRANT SELECT ON ALL TABLES IN SCHEMA public TO schema_reader;

-- Writer: SELECT + INSERT + UPDATE on all tables
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO schema_writer;

-- Create users and assign roles
CREATE USER readonly_user WITH PASSWORD 'read_pass_123';
GRANT schema_reader TO readonly_user;

CREATE USER app_user WITH PASSWORD 'app_pass_456';
GRANT schema_writer TO app_user;

Section 5 — Transactions — 5 points

Write a single transaction that performs a multi-step operation which must succeed or fail as a unit. The transaction must:

  1. Use BEGIN and COMMIT
  2. Include at least one SAVEPOINT
  3. Show the ROLLBACK TO SAVEPOINT path (even as a comment, to demonstrate understanding)
  4. Have a comment on every significant step explaining what it does and why atomicity matters here
SQL
-- Transaction: transfer an employee to a new department
-- Both the update and the audit log entry must succeed together.
-- If the audit insert fails, the transfer is also rolled back.
BEGIN;

-- Step 1: Snapshot state before any changes
SAVEPOINT before_transfer;

-- Step 2: Move the employee to the Finance department
-- FK is resolved by subquery — no hardcoded ID
UPDATE employees
SET dept_id = (SELECT dept_id FROM departments WHERE dept_name = 'Finance'),
    status  = 'active'
WHERE full_name = 'Alice Johnson';

-- If the UPDATE affected 0 rows (employee not found), roll back:
-- ROLLBACK TO SAVEPOINT before_transfer;

-- Step 3: Record the transfer in an audit log
-- If this fails, the entire transaction including Step 2 is undone
INSERT INTO audit_log (action, performed_at, description)
VALUES (
    'DEPT_TRANSFER',
    NOW(),
    'Alice Johnson transferred to Finance department'
);

-- Both steps committed as one atomic unit
COMMIT;

Resources

  • PostgreSQL 16 documentation: https://www.postgresql.org/docs/16/
  • Course lesson index: /tracks/web-tech/postgresql/
  • DDL reference: lessons 09–10 (CREATE TABLE, constraints), lesson 14 (ALTER TABLE)
  • DML reference: lessons 11–13 (INSERT patterns, re-runnable scripts)
  • SELECT reference: lessons 26–40 (filtering, joins, aggregates, CTEs)
  • DCL reference: lesson 54 (GRANT, REVOKE, roles)
  • Transactions reference: lesson 55 (BEGIN, SAVEPOINT, ROLLBACK)

Notes for Students

Submission folder name: FINAL_Yourname_Group — replace Yourname with your actual name and Group with your group number. Example: FINAL_Johnson_IT23.

File structure inside the folder:

FileContents
01_schema.sqlCREATE TABLE + ALTER TABLE statements
02_data.sqlTRUNCATE + all INSERT statements
03_queries.sql6 SELECT queries, numbered and commented
04_dcl.sqlRole and user creation + GRANT statements
05_transactions.sqlTransaction with SAVEPOINT

Common pitfalls to avoid:

  • Running 02_data.sql twice must give the same row counts — test this before submitting
  • Foreign key subqueries must match existing names exactly — if a name changes in your data, the subquery silently returns NULL and the INSERT fails
  • The schema_reader role needs USAGE on the schema before it can see any tables — do not skip this line
  • A SAVEPOINT only makes sense inside a BEGIN / COMMIT block — do not use it outside a transaction
  • Comments are part of the grade — each query and each ALTER TABLE must have an explanatory comment above it

Submission deadline: announced by your instructor. Submit before the start of Work Session 5 (lesson 56).

During work sessions: the instructor circulates and answers questions. Use this time actively — do not save questions for the night before the defense.

Grading Criteria

#SectionPoints
1Schema Design (DDL) — correct structure, constraints, ALTERs10
2Data Population (DML) — re-runnable, dynamic FKs, CTE + INSERT10
3SELECT Queries — 6 queries of increasing complexity10
4DCL — roles, grants, user assignment5
5Transactions — multi-step transaction with SAVEPOINT5
Total40

Session Structure

TimeActivity
0–20 minAssignment presentation and requirements walkthrough
20–35 minQ&A — clarify requirements and schema expectations
35–70 minStudents begin working: choose domain, sketch schema
70–80 minWrap-up: next work sessions announced, defence schedule preview