Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions calculate_largest_expensors.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
-- ============================================================================
-- EXPENSE ACCOUNTABILITY REPORT: Employees exceeding spend limits
-- ============================================================================
-- Purpose: Identify who spent >1000 and hold their managers accountable
-- Business context: After last night's Christmas party disaster, the Chief of Staff
-- needs to know which employees exceeded limits and who approved the spending
--
-- Output: employee_id, employee_name, manager_id, manager_name, total_expensed_amount
-- Filtered: Only employees with total > 1000
-- Sorted: Highest spenders first (descending)
--
-- Business logic:
-- 1. Calculate expense per line item: unit_price * quantity
-- 2. Sum all expenses PER EMPLOYEE (GROUP BY employee_id)
-- 3. Filter: Only show employees with sum > 1000 (HAVING clause)
-- 4. Include manager info for accountability (LEFT JOIN to employee table)
-- 5. If no manager, show "No Manager" (for top-level staff)
-- 6. Sort by total descending (biggest overspenders first)
--
-- SQL techniques:
-- - CONCAT() to format "FirstName LastName" from two columns
-- - COALESCE() to handle NULL manager_name (default to "No Manager")
-- - LEFT JOIN on both expense and manager to handle missing data gracefully
-- - GROUP BY all non-aggregated columns to avoid ambiguity
-- - HAVING (not WHERE) to filter post-aggregation
-- - CAST to DECIMAL(10,2) to avoid floating-point rounding errors on currency
--
-- Edge cases handled:
-- - Employee with NO manager (manager_id IS NULL): Shows "No Manager"
-- - Employee with NO expenses: Not shown (no expense rows to JOIN)
-- - Employee with expenses totaling EXACTLY 1000: Not shown (threshold is > 1000, not >=)
-- - Manager deleted but still referenced: Handled by LEFT JOIN (shows "No Manager")
-- - NULL unit_price or quantity: SUM() ignores NULLs, so totals remain accurate
--
-- Performance notes:
-- - Indexes on expense(employee_id) and employee(employee_id, manager_id) would speed joins
-- - LEFT JOINs are more expensive than INNER JOINs; use if missing data is possible
-- - GROUP BY cost scales O(n log n) in typical SQL engines; acceptable for <100k rows
--
-- Data lineage:
-- Source: memory.default.employee, memory.default.expense (created by earlier scripts)
-- Output: Result set (report for Chief of Staff)

SELECT
e.employee_id,
CONCAT(e.first_name, ' ', e.last_name) AS employee_name,
e.manager_id,
COALESCE(CONCAT(m.first_name, ' ', m.last_name), 'No Manager') AS manager_name,
CAST(SUM(ex.unit_price * ex.quantity) AS DECIMAL(10, 2)) AS total_expensed_amount
FROM employee e
LEFT JOIN expense ex ON e.employee_id = ex.employee_id
LEFT JOIN employee m ON e.manager_id = m.employee_id
GROUP BY
e.employee_id,
e.first_name,
e.last_name,
e.manager_id,
m.first_name,
m.last_name
HAVING SUM(ex.unit_price * ex.quantity) > 1000
ORDER BY total_expensed_amount DESC;

-- Interpretation:
-- Rows show employees who spent >1000 with their manager
-- Use this to:
-- 1. Identify accountability (who approved? manager_id)
-- 2. Set corrective action (review process with this manager)
-- 3. Prevent recurrence (policy training or pre-approval workflows)
--
-- Sample output:
-- | employee_id | employee_name | manager_id | manager_name | total_expensed_amount |
-- | 1 | Alice Smith | NULL | No Manager | 1250.00 |
-- | 2 | Bob Jones | 1 | Alice Smith | 1100.00 |
33 changes: 33 additions & 0 deletions create_employees.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-- ============================================================================
-- EMPLOYEE TABLE: Core staff directory with manager relationships
-- ============================================================================
-- Purpose: Load employee roster from source data
-- Columns: employee_id (PK), first_name, last_name, manager_id (FK)
--
-- Business logic:
-- - Every employee has a unique ID (primary key)
-- - manager_id links to another employee (manager relationship)
-- - TOP-LEVEL employees have NULL manager_id
-- - Used to build org hierarchy for approval chains and cycle detection
--
-- Edge cases handled:
-- - NULL manager_id for C-suite/top-level staff (valid)
-- - TINYINT for IDs ensures deterministic casting from source CSV
--
-- Data lineage:
-- Source: hr/employee_index.csv
-- Target: memory.default.employee (in-memory Presto table)

CREATE TABLE employee AS
SELECT
CAST(employee_id AS TINYINT) AS employee_id,
first_name,
last_name,
CAST(manager_id AS TINYINT) AS manager_id
FROM memory.default.employee_source;

-- Verification query (optional, run separately if needed):
-- SELECT COUNT(*) AS total_employees,
-- COUNT(DISTINCT manager_id) AS unique_managers,
-- COUNT(CASE WHEN manager_id IS NULL THEN 1 END) AS top_level_staff
-- FROM employee;
39 changes: 39 additions & 0 deletions create_expenses.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
-- ============================================================================
-- EXPENSE TABLE: Itemized receipts from last night's Christmas party disaster
-- ============================================================================
-- Purpose: Load expense line items for reconciliation and accountability
-- Columns: employee_id (FK), unit_price (DECIMAL), quantity (TINYINT)
--
-- Business logic:
-- - Each row represents ONE PURCHASED ITEM (not a transaction total)
-- - Expense amount per line = unit_price * quantity
-- - Total employee spending = SUM(unit_price * quantity) grouped by employee_id
-- - Used to calculate who exceeded spending limits (>1000)
--
-- Type safety:
-- - unit_price as DECIMAL(8,2) to handle currency without floating-point errors
-- - quantity as TINYINT (max 255 units per item, sufficient for expense tracking)
-- - employee_id as TINYINT to match EMPLOYEE table
--
-- Edge cases handled:
-- - Quantity of 0 (cancelled items) will be included but contribute $0 to totals
-- - Unit price can be fractional (e.g., 15.99) — DECIMAL preserves precision
-- - Missing employee_id in EXPENSE (orphaned lines) will be handled by LEFT JOIN in queries
--
-- Data lineage:
-- Source: finance/receipts_from_last_night/ directory (CSV files)
-- Target: memory.default.expense (in-memory Presto table)

CREATE TABLE expense AS
SELECT
CAST(employee_id AS TINYINT) AS employee_id,
CAST(unit_price AS DECIMAL(8, 2)) AS unit_price,
CAST(quantity AS TINYINT) AS quantity
FROM memory.default.expense_source;

-- Verification query (optional, run separately if needed):
-- SELECT COUNT(*) AS total_line_items,
-- COUNT(DISTINCT employee_id) AS employees_with_expenses,
-- SUM(unit_price * quantity) AS total_spending,
-- MAX(unit_price * quantity) AS largest_single_item
-- FROM expense;
71 changes: 71 additions & 0 deletions create_invoices.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
-- ============================================================================
-- SUPPLIER & INVOICE TABLES: Vendor directory and payment obligations
-- ============================================================================
-- Two-table design for normalized vendor payment tracking
--
-- SUPPLIER TABLE
-- Purpose: Master list of vendors with deterministic IDs
-- Columns: supplier_id (PK), name (VARCHAR)
--
-- Business logic:
-- - Supplier IDs are generated ALPHABETICALLY by company name
-- - ROW_NUMBER() OVER (ORDER BY supplier_name) ensures deterministic assignment
-- - This makes supplier_id = 1 always "Catering Plus", supplier_id = 2 always "Office Supplies", etc.
-- - Deterministic IDs critical for data reproducibility and joins
--
-- Edge case: Duplicate company names (deduplicated via SELECT DISTINCT before numbering)
--
-- INVOICE TABLE
-- Purpose: Track bills from suppliers with payment schedules
-- Columns: supplier_id (FK), invoice_amount (DECIMAL), due_date (DATE)
--
-- Business logic:
-- - invoice_amount is the total bill owed to this supplier
-- - due_date is NORMALIZED to LAST DAY OF MONTH (company policy)
-- Example: 2026-07-15 → 2026-07-31, 2026-08-05 → 2026-08-31
-- - Normalization ensures consistent payment schedules across all invoices
-- - Joins to SUPPLIER via supplier_id to enable supplier name lookups
--
-- Date normalization approach:
-- DATE_TRUNC('month', date) gives first day of month
-- + INTERVAL '1' month advances to first day of NEXT month
-- - INTERVAL '1' day steps back one day to last day of current month
-- Result: Any date in July becomes 2026-07-31
--
-- Type safety:
-- - supplier_id as TINYINT (max 255 suppliers sufficient)
-- - invoice_amount as DECIMAL(8,2) for accurate currency (no floating-point errors)
-- - due_date as DATE for precise date comparison and interval math
--
-- Edge cases handled:
-- - Invoices already on last day of month (normalization is idempotent)
-- - Multiple invoices from same supplier (each gets own row; balance_outstanding sums them)
-- - Missing supplier_id in INVOICE (will fail JOIN — data integrity check)
--
-- Data lineage:
-- Source: finance/invoices_due/ directory (CSV files)
-- Target: memory.default.supplier, memory.default.invoice (in-memory Presto tables)

-- STEP 1: Create SUPPLIER table with alphabetically-assigned IDs
CREATE TABLE supplier AS
SELECT
ROW_NUMBER() OVER (ORDER BY supplier_name) AS supplier_id,
supplier_name AS name
FROM (
SELECT DISTINCT supplier_name
FROM memory.default.invoice_source
) s;

-- STEP 2: Create INVOICE table with normalized due dates
CREATE TABLE invoice AS
SELECT
s.supplier_id,
CAST(i.invoice_amount AS DECIMAL(8, 2)) AS invoice_amount,
DATE_TRUNC('month', CAST(i.due_date AS DATE)) + INTERVAL '1' month - INTERVAL '1' day AS due_date
FROM memory.default.invoice_source i
JOIN supplier s ON UPPER(i.supplier_name) = UPPER(s.name);

-- Verification queries (optional, run separately if needed):
-- SELECT * FROM supplier ORDER BY supplier_id;
-- SELECT *, DAY(due_date) AS day_of_month FROM invoice ORDER BY supplier_id, due_date;
-- SELECT COUNT(*) FROM invoice GROUP BY supplier_id;
92 changes: 92 additions & 0 deletions find_manager_cycles.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
-- ============================================================================
-- MANAGER CYCLE DETECTION: Identify circular approval chains
-- ============================================================================
-- Purpose: Find employees trapped in approval loops
-- Problem: If A manages B, B manages C, and C manages A → circular chain blocks approvals
-- Solution: Recursive CTE traverses manager hierarchy and detects loops
--
-- Output: employee_id and the complete cycle path (comma-separated)
-- Example: Employee 1 in cycle [1,2,3,1] appears as: (1, "1,2,3,1")
--
-- Algorithm:
-- 1. BASE CASE: Start with each employee + their direct manager
-- 2. RECURSIVE CASE: Follow manager's manager, building path as we go
-- 3. STOP CONDITIONS:
-- a) We revisit an employee already in path (indicates loop)
-- b) We hit max depth of 10 levels (prevent runaway recursion)
-- c) Manager is NULL (reached top of hierarchy)
-- 4. DETECT CYCLE: Manager_id closes the loop (is in our path)
--
-- Example trace:
-- Employee 1: manager_id = 2
-- Employee 2: manager_id = 3
-- Employee 3: manager_id = 1 ← Points back to 1, so [1,2,3,1] is a cycle
--
-- Edge cases handled:
-- - Self-loop: employee_id = manager_id (caught immediately)
-- - Two-person loop: A manages B, B manages A (depth 2)
-- - Deep hierarchies with no cycle: A→B→C→D→NULL (returns no cycles)
-- - NULL manager (top-level staff): skipped in base case (WHERE manager_id IS NOT NULL)
--
-- Performance notes:
-- - Depth limit of 10 is conservative (most orgs < 8 levels)
-- - CONTAINS(array, value) is O(n) per check; alternatives: ARRAY_OVERLAPS, nested LIKE
-- - Recursive CTE suitable for <100 employees; larger datasets may need materialized path approach
--
-- Data lineage:
-- Source: memory.default.employee (created by create_employees.sql)
-- Output: Result set (not persisted; view-only)

WITH RECURSIVE manager_chain AS (
-- BASE CASE: Each employee with their direct manager
-- Path starts with just the employee; we'll append managers as we traverse
SELECT
employee_id,
manager_id,
CAST(ARRAY[employee_id] AS ARRAY(TINYINT)) AS path,
1 AS depth
FROM employee
WHERE manager_id IS NOT NULL -- Skip top-level (no manager)

UNION ALL

-- RECURSIVE CASE: Follow manager's manager, extending the path
-- e = current manager's employee record (to get their manager_id)
-- mc = previous row in the recursion (current path and depth)
SELECT
mc.employee_id,
e.manager_id,
mc.path || ARRAY[e.employee_id], -- Append manager to path
mc.depth + 1
FROM manager_chain mc
JOIN employee e ON mc.manager_id = e.employee_id
WHERE
-- Stop if we've already seen this employee in our path (loop detected)
NOT CONTAINS(mc.path, e.employee_id)
-- Prevent infinite recursion; org hierarchies are typically <10 levels
AND mc.depth < 10
),
cycles AS (
-- Find rows where manager_id is already in the path (closes the loop)
SELECT
mc.employee_id,
mc.path || ARRAY[mc.manager_id] AS cycle_path -- Append final manager to complete cycle
FROM manager_chain mc
WHERE
mc.manager_id IS NOT NULL
AND CONTAINS(mc.path, mc.manager_id) -- Manager is already in path → cycle!
)
SELECT
employee_id,
ARRAY_JOIN(cycle_path, ',') AS loop -- Format as "1,2,3,1" string
FROM cycles
ORDER BY employee_id;

-- Interpretation:
-- If query returns 0 rows: No cycles found (org structure is healthy)
-- If query returns rows: Each row is an employee in a cycle; loop column shows the full cycle
--
-- Remediation example:
-- If you see (2, "1,2,3,1"), fix by changing one person's manager:
-- UPDATE employee SET manager_id = NULL WHERE employee_id = 3;
-- Then re-run to confirm cycle is broken
Loading