<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<game format-version="2" db-system="sqlite" db-system-min-version="3.37.0">
    <head>
        <title>Construction Company</title>
        <teaser>You run a construction company, and an investor has a few questions for you...

Difficulty: Very high

You need NULL, aggregate functions, JOINs, self-joins, GROUP BY, HAVING and subqueries.</teaser>
        <copyright>© 2024 Niklas J., Finn V., Lasse M., Bennet W.</copyright>
    </head>
    <scenes>
        <text-scene>
            <text>*You run a construction company*

“I'm an investor and have a few questions about this company because I'd like to buy it. Please answer these questions using your company's database.”</text>
        </text-scene>
        <select-scene is-row-order-relevant="false" is-col-order-relevant="true" are-col-names-relevant="true">
            <text>“Try the following query: `SELECT * FROM employee`. It shows all employees with all columns.”</text>
            <sql-solution>SELECT *
FROM employee</sql-solution>
            <sql-placeholder></sql-placeholder>
            <hints>
                <expected-result-hint />
            </hints>
        </select-scene>
        <select-scene is-row-order-relevant="false" is-col-order-relevant="false" are-col-names-relevant="false">
            <text>“How many construction projects are there in total? Return exactly one number.”</text>
            <sql-solution>SELECT COUNT(*) AS project_count
FROM project</sql-solution>
            <sql-placeholder></sql-placeholder>
            <hints>
                <expected-result-hint />
                <text-hint>Use `COUNT(*)` to count the rows in the `project` table.</text-hint>
            </hints>
        </select-scene>
        <select-scene is-row-order-relevant="false" is-col-order-relevant="false" are-col-names-relevant="false">
            <text>“How many employees are not working on any project? This means employees with no project assignment. Return exactly one number.”</text>
            <sql-solution>SELECT COUNT(*) AS employee_count
FROM employee
WHERE project_id IS NULL</sql-solution>
            <sql-placeholder></sql-placeholder>
            <hints>
                <expected-result-hint />
                <text-hint>The project assignment is stored in `employee.project_id`. If there is no assignment, this value is `NULL`.</text-hint>
                <text-hint>Filter with `IS NULL` and count the matching rows with `COUNT(*)`. A comparison using `= NULL` does not check for missing values.</text-hint>
            </hints>
        </select-scene>
        <select-scene is-row-order-relevant="false" is-col-order-relevant="false" are-col-names-relevant="false">
            <text>“What is the sum of the project budgets, broken down by project location and status? Return the location, status and total budget in euros.”</text>
            <sql-solution>SELECT location, status, SUM(budget) AS total_budget
FROM project
GROUP BY location, status</sql-solution>
            <sql-placeholder></sql-placeholder>
            <hints>
                <expected-result-hint />
                <text-hint>Group by both attributes, `location` and `status`, and use `SUM` to add up the `budget` within each group.</text-hint>
            </hints>
        </select-scene>
        <select-scene is-row-order-relevant="false" is-col-order-relevant="false" are-col-names-relevant="false">
            <text>“What is the name of the employee who earns the most? Return only the names; if several employees share the highest salary, include all of them. Compare gross monthly salaries.”</text>
            <sql-solution>SELECT name
FROM employee
WHERE salary = (SELECT MAX(salary) FROM employee)</sql-solution>
            <sql-placeholder></sql-placeholder>
            <hints>
                <expected-result-hint />
                <text-hint>Use `MAX(salary)` to find the highest salary. However, you need the names of the employees who earn that salary.</text-hint>
                <text-hint>Find the highest salary in a subquery and compare each salary with it in `WHERE`. This keeps all matching employees in the result when there is a tie.</text-hint>
            </hints>
        </select-scene>
        <select-scene is-row-order-relevant="false" is-col-order-relevant="false" are-col-names-relevant="false">
            <text>“How much does the material ordered for the projects in Berlin cost? Return the total value of all recorded material orders for these projects as a single amount in euros.”</text>
            <sql-solution>SELECT SUM(o.unit_price * o.quantity) AS order_value
FROM material_order AS o
JOIN project AS p ON o.project_id = p.project_id
WHERE p.location = 'Berlin'</sql-solution>
            <sql-placeholder></sql-placeholder>
            <hints>
                <expected-result-hint />
                <text-hint>The location is stored in `project`, and the orders are stored in `material_order`. Use `project_id` to match the orders to the projects in Berlin.</text-hint>
                <text-hint>For each order, multiply `unit_price` by `quantity` and add up the values with `SUM`. `quantity` is the amount ordered; `material.stock_quantity` is the stock on hand.</text-hint>
            </hints>
        </select-scene>
        <select-scene is-row-order-relevant="false" is-col-order-relevant="false" are-col-names-relevant="false">
            <text>“What is the average price per unit for each material ordered for the Bridge project in Hamburg? Each order counts once, regardless of its quantity. Return the material name and average price in euros, rounded to two decimal places.”</text>
            <sql-solution>SELECT m.name, ROUND(AVG(o.unit_price), 2) AS average_price
FROM material_order AS o
JOIN material AS m ON o.material_id = m.material_id
JOIN project AS p ON o.project_id = p.project_id
WHERE p.name = 'Bridge' AND p.location = 'Hamburg'
GROUP BY m.material_id, m.name</sql-solution>
            <sql-placeholder></sql-placeholder>
            <hints>
                <expected-result-hint />
                <text-hint>Join `material_order` to `material` using `material_id` and to `project` using `project_id`. Filter by project name and location.</text-hint>
                <text-hint>Group by material and use `AVG` on `material_order.unit_price`. Each order counts once, so its quantity is not part of the calculation.</text-hint>
                <text-hint>Apply `ROUND(..., 2)` to the calculated average so that only the final result is rounded.</text-hint>
            </hints>
        </select-scene>
        <select-scene is-row-order-relevant="false" is-col-order-relevant="false" are-col-names-relevant="false">
            <text>“How many employees earn more than their manager? `manager_id` refers to the `employee_id` of the direct manager; occupation and project do not matter. Do not count employees without a direct manager. Return exactly one number.”</text>
            <sql-solution>SELECT COUNT(*) AS employee_count
FROM employee AS e
JOIN employee AS m ON e.manager_id = m.employee_id
WHERE e.salary &gt; m.salary</sql-solution>
            <sql-placeholder></sql-placeholder>
            <hints>
                <expected-result-hint />
                <text-hint>Use `employee` twice with different aliases: once for the employee and once for their direct manager. Join the manager's `employee_id` to the employee's `manager_id`.</text-hint>
                <text-hint>Filter the joined rows to employees whose salary is higher than their manager's, and count the matches. An `INNER JOIN` leaves out employees without an assigned manager.</text-hint>
            </hints>
        </select-scene>
        <select-scene is-row-order-relevant="false" is-col-order-relevant="false" are-col-names-relevant="false">
            <text>“Which project has the highest percentage of its budget accounted for by materials? Compare the value of the recorded material orders with each project's budget. Return exactly the project name and the percentage, without rounding the percentage. If there is a tie, choose the project with the smallest `project_id`. Projects without orders have a percentage of 0.”</text>
            <sql-solution>SELECT p.name,
       100.0 * COALESCE(SUM(o.unit_price * o.quantity), 0) / p.budget AS percentage
FROM project AS p
LEFT JOIN material_order AS o ON p.project_id = o.project_id
GROUP BY p.project_id, p.name, p.budget
ORDER BY percentage DESC, p.project_id ASC
LIMIT 1</sql-solution>
            <sql-placeholder></sql-placeholder>
            <hints>
                <expected-result-hint />
                <text-hint>First, sum `unit_price * quantity` for each project. The percentage is this order value multiplied by `100.0`, divided by the budget. Multiply by `100.0` before dividing to avoid integer division.</text-hint>
                <text-hint>Start with `project` and use a `LEFT JOIN` to keep projects without orders. Use `COALESCE(..., 0)` to replace a missing sum with zero euros.</text-hint>
                <text-hint>Sort first by percentage in descending order, then by `project_id` in ascending order. Use `LIMIT 1` to select the required project.</text-hint>
            </hints>
        </select-scene>
        <select-scene is-row-order-relevant="false" is-col-order-relevant="false" are-col-names-relevant="false">
            <text>“How many projects have more than two employees with the same occupation? Count each project only once, even if several occupations meet the condition. Employees without a project assignment do not belong to any project. Return exactly one number, even if no project meets the condition.”</text>
            <sql-solution>SELECT COUNT(DISTINCT project_id) AS project_count
FROM (
    SELECT project_id, occupation
    FROM employee
    WHERE project_id IS NOT NULL
    GROUP BY project_id, occupation
    HAVING COUNT(*) &gt; 2
) AS matching_groups</sql-solution>
            <sql-placeholder></sql-placeholder>
            <hints>
                <expected-result-hint />
                <text-hint>Group employees who have a project assignment by `project_id` and `occupation`. Use `HAVING COUNT(*) &gt; 2` to find the matching groups.</text-hint>
                <text-hint>Use these groups as a subquery. An outer `COUNT(DISTINCT project_id)` counts each project only once and returns a number even if there are no matching groups.</text-hint>
            </hints>
        </select-scene>
        <text-scene>
            <text>“Thank you very much for your help!!! Your analysis gives me an initial overview of the company.”

Congratulations! You have won!</text>
        </text-scene>
    </scenes>
    <initial-sql-script>-- Fictional data extract: budgets and prices in euros, salaries gross per month.
-- Orders cover only part of the material procurement.
CREATE TABLE project (
    project_id INTEGER PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    location VARCHAR(50) NOT NULL,
    budget DECIMAL(10,2) NOT NULL CHECK (budget &gt; 0),
    status VARCHAR(20) NOT NULL
);

CREATE TABLE employee (
    employee_id INTEGER PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    salary DECIMAL(10,2) NOT NULL CHECK (salary &gt;= 0),
    occupation VARCHAR(30) NOT NULL,
    project_id INTEGER REFERENCES project (project_id),
    manager_id INTEGER REFERENCES employee (employee_id),
    CHECK (manager_id IS NULL OR manager_id &lt;&gt; employee_id)
);

-- stock_quantity is the stock on hand in the specified unit.
CREATE TABLE material (
    material_id INTEGER PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    unit VARCHAR(20) NOT NULL,
    stock_quantity INTEGER NOT NULL CHECK (stock_quantity &gt;= 0)
);

-- Prices are recorded for each order, not in the material table.
CREATE TABLE material_order (
    order_id INTEGER PRIMARY KEY,
    order_date DATE NOT NULL,
    project_id INTEGER NOT NULL REFERENCES project (project_id),
    material_id INTEGER NOT NULL REFERENCES material (material_id),
    quantity INTEGER NOT NULL CHECK (quantity &gt; 0),
    unit_price DECIMAL(10,2) NOT NULL CHECK (unit_price &gt;= 0)
);

INSERT INTO project VALUES
(1, 'Residential Building', 'Berlin', 500000.00, 'under construction'),
(2, 'Bridge', 'Hamburg', 1000000.00, 'planned'),
(3, 'School', 'Munich', 800000.00, 'completed'),
(4, 'Office Building', 'Berlin', 750000.00, 'under construction'),
(5, 'Sports Hall', 'Berlin', 400000.00, 'planned'),
(6, 'Daycare Centre', 'Hamburg', 300000.00, 'planned'),
(7, 'Workshop', 'Munich', 200000.00, 'completed');

-- Managers are explicitly assigned, including across different projects.
INSERT INTO employee VALUES
(1, 'Anna Müller', 4800.00, 'Architecture', 1, 6),
(2, 'Bernd Schmidt', 5200.00, 'Civil Engineering', 2, 6),
(3, 'Carl Weber', 3500.00, 'Bricklaying', 1, 1),
(4, 'Doris Klein', 2800.00, 'Administration', NULL, 6),
(5, 'Erik Wolf', 3900.00, 'Electrical Installation', 3, 6),
(6, 'Farah Neumann', 5000.00, 'Project Management', 4, 7),
(7, 'Greta Hoffmann', 7000.00, 'Executive Management', NULL, NULL),
(8, 'Hannes Braun', 4800.00, 'Bricklaying', 1, 1),
(9, 'Ines Richter', 5100.00, 'Bricklaying', 1, 1),
(10, 'Jonas Koch', 3200.00, 'Electrical Installation', 1, 1),
(11, 'Karin Lange', 4800.00, 'Electrical Installation', 1, 1),
(12, 'Lars Schulz', 4900.00, 'Electrical Installation', 1, 1),
(13, 'Mina Becker', 4100.00, 'Bricklaying', 2, 2),
(14, 'Nils Fischer', 5300.00, 'Bricklaying', 2, 2),
(15, 'Oskar Schmitt', 3800.00, 'Electrical Installation', 3, 5),
(16, 'Pia Wagner', 3900.00, 'Electrical Installation', 3, 5),
(17, 'Quirin Roth', 3000.00, 'Administration', NULL, 6),
(18, 'Rita Wolf', 2900.00, 'Administration', NULL, 6);

INSERT INTO material VALUES
(1, 'Cement', 'bag (25 kg)', 1000),
(2, 'Steel', 'kg', 500),
(3, 'Timber', 'board', 800),
(4, 'Cable', 'm', 1200),
(5, 'Paint', 'litre', 600);

INSERT INTO material_order VALUES
(1, '2023-11-01', 1, 1, 100, 5.00),
(2, '2023-11-02', 1, 2, 50, 10.00),
(3, '2023-11-03', 2, 2, 100, 10.00),
(4, '2023-11-04', 3, 3, 200, 8.00),
(5, '2023-11-05', 3, 4, 300, 3.00),
(6, '2023-11-06', 2, 2, 300, 14.00),
(7, '2023-11-07', 2, 1, 100, 6.00),
(8, '2023-11-08', 2, 1, 50, 10.00),
(9, '2023-11-09', 4, 3, 100, 9.00),
(10, '2023-11-10', 4, 5, 50, 6.00),
(11, '2023-11-11', 5, 2, 20, 10.00),
(12, '2023-11-12', 6, 2, 110, 21.00),
(13, '2023-11-13', 2, 2, 100, 14.00);</initial-sql-script>
</game>
