🎁 First Order? Get Up To 10% OFF | 📞 Talk to an Expert on a WhatsApp Call | Assignment Help | Dissertation Help | Research Proposal Help | Proofreading & Editing | Harvard, APA & OSCOLA Referencing | 24/7 WhatsApp Support | 🎁 First Order? Get Up To 10% OFF | 📞 Talk to an Expert on a WhatsApp Call | Assignment Help | Dissertation Help | Research Proposal Help | Proofreading & Editing | Harvard, APA & OSCOLA Referencing | 24/7 WhatsApp Support | 🎁 First Order? Get Up To 10% OFF | 📞 Talk to an Expert on a WhatsApp Call | Assignment Help | Dissertation Help | Research Proposal Help | Proofreading & Editing | Harvard, APA & OSCOLA Referencing | 24/7 WhatsApp Support | 🎁 First Order? Get Up To 10% OFF | 📞 Talk to an Expert on a WhatsApp Call | Assignment Help | Dissertation Help | Research Proposal Help | Proofreading & Editing | Harvard, APA & OSCOLA Referencing | 24/7 WhatsApp Support |
Computing, IT & Databases sample

SunCrest Hotels & Resorts Database Project: ER Diagram, Normalisation and MySQL Sample

A worked preview of a full database course project: Crow's Foot ER diagram, EER hierarchy, 3NF normalisation and tested MySQL queries, a cursor procedure and an audit trigger.

FIZBS Academic Team Published 10 October 2026 7 parts, 36 SQL tasks

This sample shows how the FIZBS team approaches a full database design and SQL project. The SunCrest Hotels & Resorts brief asks students to model a 15-hotel chain, normalise a flat reservation export to third normal form, and then write MySQL queries, views, a stored function, a cursor-based procedure and a trigger.

Below you will find a summary of the brief, our Crow's Foot ER diagram, the key design decisions and a preview of the tested SQL. Only part of the solution is shown, and no personal details are included.

For reference only. This sample shows our approach and quality. Do not copy it or submit it as your own work. Personal details have been removed, and only part of the solution is shown.

Category
Computing, IT & Databases
Subject
Database Systems
Level
University course project
Scope
7 parts, 36 SQL tasks
Tools
MySQL 8, Crow's Foot ERD, EER, Normalisation

What the brief asked for

  1. 1Part I: identify every entity, attribute and relationship in the business narrative, with cardinality and participation, and resolve the many-to-many link between reservations and services.
  2. 2Part II: draw a complete ER diagram in Chen or Crow's Foot notation, with written justification for two relationships.
  3. 3Part III: extend the model with an EER hierarchy for individual and corporate guests, including a negotiated rate relationship with hotels.
  4. 4Part IV: normalise a flat RESERVATION_RECORD relation through 1NF, 2NF and 3NF, and show that common anomalies are removed.
  5. 5Part V: justify primary keys, NOT NULL and UNIQUE constraints, and explain entity integrity versus referential integrity.
  6. 6Part VI: write 25 business queries and 5 challenge queries using joins, subqueries, indexes and views on a ready-made 10-table MySQL database.
  7. 7Part VII: advanced SQL with single-row and group functions, set operators, a stored function, a stored procedure with a cursor and handler, and an audit trigger.

Step 1: Entities and primary keys

We read the narrative line by line and turned every "thing the business needs to remember" into an entity. Each one gets a surrogate primary key so that rows can be referenced safely, even when natural values such as room numbers repeat.

HOTEL

What it represents
One SunCrest property with its name, location, star rating and on-site manager
Primary key
HotelID

ROOM_TYPE

What it represents
A shared category such as Standard, Deluxe or Suite, with a base nightly rate and maximum occupancy
Primary key
RoomTypeID

ROOM

What it represents
One physical room in one hotel; numbers like "101" repeat across hotels
Primary key
RoomID

GUEST

What it represents
A person with a profile, contact details and a loyalty tier
Primary key
GuestID

EMPLOYEE

What it represents
A staff member who works at exactly one hotel
Primary key
EmployeeID

RESERVATION

What it represents
One booking of one room for a date range, handled by one employee
Primary key
ReservationID

PAYMENT

What it represents
The single payment record for a non-cancelled reservation
Primary key
PaymentID

SERVICE

What it represents
An optional extra from the shared catalogue, such as spa, dining or transport
Primary key
ServiceID

RESERVATION_SERVICE

What it represents
Associative entity: which services were ordered during a stay, and how many
Primary key
ReservationServiceID

REVIEW

What it represents
One optional post-stay review tied to a reservation and a guest
Primary key
ReviewID

Entities identified from the SunCrest narrative

The narrative hides one many-to-many relationship: a reservation can include many services, and a service can be ordered on many reservations. Because the link carries its own data (Quantity and ServiceDate), we resolve it with the associative entity RESERVATION_SERVICE instead of a simple foreign key.

Step 2: Relationships, cardinality and participation

Every cardinality and participation choice is backed by a clause from the brief. Markers give partial credit for correct answers without justification, so this column matters as much as the diagram.

HOTEL contains ROOM

Cardinality
1:N
Participation
Total on both sides
Business rule used
"Each hotel contains many rooms"; a room cannot exist outside a hotel

HOTEL employs EMPLOYEE

Cardinality
1:N
Participation
Total on both sides
Business rule used
"Each employee works at exactly one hotel; a hotel employs many staff"

GUEST makes RESERVATION

Cardinality
1:N
Participation
Partial for GUEST, total for RESERVATION
Business rule used
A profile can exist before any booking, but every reservation belongs to one guest

RESERVATION generates PAYMENT

Cardinality
1:0..1
Participation
Partial for RESERVATION, total for PAYMENT
Business rule used
Cancelled reservations have no payment; every payment belongs to one reservation

RESERVATION receives REVIEW

Cardinality
1:0..1
Participation
Partial for RESERVATION, total for REVIEW
Business rule used
A review is optional and is tied to one specific stay

Selected relationships with justification

Step 3: The ER diagram (Crow's Foot notation)

Crow's Foot ER diagram for the SunCrest Hotels & Resorts database with HOTEL, ROOM, ROOM_TYPE, GUEST, EMPLOYEE, RESERVATION, PAYMENT, SERVICE, RESERVATION_SERVICE and REVIEW
Full ER diagram in Crow's Foot notation. PK = primary key, FK = foreign key, UK = unique (candidate) key. Tap the image to open it full size.

We used Crow's Foot notation consistently, as the brief requires. Look at the link between RESERVATION and PAYMENT: the double bar on the reservation side and the circle on the payment side say that every payment needs a reservation, but a cancelled reservation has none. The same pattern applies to REVIEW.

Step 4: EER extension for individual and corporate guests

  • The specialisation of GUEST is total (every guest must be Individual or Corporate) and disjoint (a guest cannot be both), so it is drawn with a double line and a "d" in the circle.
  • INDIVIDUAL_GUEST adds only DateOfBirth, GovernmentIDNumber and PreferredContactMethod. Inherited attributes such as Email and LoyaltyTier are not redrawn.
  • CORPORATE_GUEST adds CompanyName, TaxID, BillingContactName and BillingContactEmail.
  • NEGOTIATED_RATE is an M:N relationship between CORPORATE_GUEST and HOTEL with the attributes DiscountPct and ContractStartDate. The discount varies by hotel, so it belongs on the relationship, not on the guest.

Why not draw NEGOTIATED_RATE from GUEST? It would suggest that individual travellers can hold corporate contracts, which the business rules forbid. Placing it on the subtype makes the rule visible in the diagram itself.

Step 5: Normalising RESERVATION_RECORD to 3NF

The flat export breaks 1NF because services_booked holds a comma-separated list (for example "Late Checkout, Parking - Valet"). We moved each service onto its own row, which gives a composite key of (reservation_id, service_name).

In 1NF, every reservation column depends on reservation_id alone, which is a partial dependency on the composite key, so 2NF separates the reservation data from the service rows. 3NF then removes the transitive dependencies through guest_id, hotel_id, employee_id and (hotel_id, room_number) to room_type to base_rate.

GUEST

Primary key
guest_id
Foreign keys
None
Other attributes
guest_name, guest_email, guest_phone, guest_city, guest_state, loyalty_tier

HOTEL

Primary key
hotel_id
Foreign keys
None
Other attributes
hotel_name, hotel_city, hotel_state

ROOM_TYPE

Primary key
room_type
Foreign keys
None
Other attributes
base_rate

ROOM

Primary key
(hotel_id, room_number)
Foreign keys
hotel_id → HOTEL, room_type → ROOM_TYPE
Other attributes
None

EMPLOYEE

Primary key
employee_id
Foreign keys
None
Other attributes
employee_name, employee_job_title

RESERVATION

Primary key
reservation_id
Foreign keys
guest_id, (hotel_id, room_number), employee_id
Other attributes
booking_date, checkin_date, checkout_date, status, payment_amount, payment_method

RESERVATION_SERVICE

Primary key
(reservation_id, service_name)
Foreign keys
reservation_id → RESERVATION
Other attributes
None

Final tables in third normal form

Anomaly check: a new room type can now be added to ROOM_TYPE before any physical room exists, a hotel's details are updated in one HOTEL row, and removing an employee no longer deletes reservation history once their bookings are reassigned.

Step 6: Tested SQL query preview

All SQL below was run against a MySQL-compatible server using the 10-table SunCrest schema from the brief. We use explicit JOINs and short, consistent aliases so a marker can follow the logic quickly.

Q6. Total revenue generated by each hotelsql
SELECT h.HotelName,
       SUM(p.Amount) AS TotalRevenue
FROM HOTEL h
JOIN ROOM r          ON r.HotelID = h.HotelID
JOIN RESERVATION res ON res.RoomID = r.RoomID
JOIN PAYMENT p       ON p.ReservationID = res.ReservationID
GROUP BY h.HotelID, h.HotelName
ORDER BY TotalRevenue DESC;
Q7. Guests who have never made a reservationsql
SELECT CONCAT(g.FirstName, ' ', g.LastName) AS FullName,
       g.Email
FROM GUEST g
WHERE NOT EXISTS (
  SELECT 1
  FROM RESERVATION r
  WHERE r.GuestID = g.GuestID
);

NOT EXISTS is a safer choice than NOT IN here, because NOT IN returns no rows at all if the subquery ever produces a NULL.

C2. Revenue summary view, then hotels above the averagesql
CREATE VIEW vw_hotel_revenue_summary AS
SELECT h.HotelID,
       h.HotelName,
       COUNT(DISTINCT res.ReservationID) AS TotalReservations,
       COALESCE(SUM(p.Amount), 0)        AS TotalRevenue,
       ROUND(AVG(p.Amount), 2)           AS AvgPaymentAmount
FROM HOTEL h
JOIN ROOM r          ON r.HotelID = h.HotelID
JOIN RESERVATION res ON res.RoomID = r.RoomID
LEFT JOIN PAYMENT p  ON p.ReservationID = res.ReservationID
GROUP BY h.HotelID, h.HotelName;

SELECT HotelName, TotalReservations, TotalRevenue
FROM vw_hotel_revenue_summary
WHERE TotalRevenue > (
  SELECT AVG(TotalRevenue)
  FROM vw_hotel_revenue_summary
)
ORDER BY TotalRevenue DESC;

TotalReservations counts every booking, including cancelled ones, while revenue only includes reservations that generated a payment. The LEFT JOIN keeps cancelled bookings in the count instead of silently dropping them.

C3. Correlated subquery: guests who out-spend their state averagesql
SELECT CONCAT(g.FirstName, ' ', g.LastName) AS GuestName,
       g.State,
       SUM(p.Amount) AS TotalSpending
FROM GUEST g
JOIN RESERVATION r ON r.GuestID = g.GuestID
JOIN PAYMENT p     ON p.ReservationID = r.ReservationID
GROUP BY g.GuestID, g.FirstName, g.LastName, g.State
HAVING SUM(p.Amount) > (
  -- correlated: average spending of paying guests in the SAME state
  SELECT SUM(p2.Amount) / COUNT(DISTINCT g2.GuestID)
  FROM GUEST g2
  JOIN RESERVATION r2 ON r2.GuestID = g2.GuestID
  JOIN PAYMENT p2     ON p2.ReservationID = r2.ReservationID
  WHERE g2.State = g.State
)
ORDER BY g.State, TotalSpending DESC;

Step 7: Advanced SQL programming preview

F4. Stored function fn_nights_stayedsql
DELIMITER $$
CREATE FUNCTION fn_nights_stayed(p_reservation_id INT)
RETURNS INT
READS SQL DATA
BEGIN
  DECLARE v_nights INT;

  SELECT DATEDIFF(CheckOutDate, CheckInDate)
    INTO v_nights
  FROM RESERVATION
  WHERE ReservationID = p_reservation_id;

  RETURN v_nights;
END$$
DELIMITER ;

SELECT r.ReservationID,
       fn_nights_stayed(r.ReservationID) AS NightsStayed
FROM RESERVATION r
JOIN ROOM rm ON rm.RoomID = r.RoomID
WHERE r.Status = 'Checked-Out'
  AND rm.HotelID = 2;
F5. Procedure with a cursor, NOT FOUND handler and control flowsql
DELIMITER $$
CREATE PROCEDURE sp_apply_loyalty_upgrades(IN p_threshold DECIMAL(10,2))
BEGIN
  DECLARE v_done      INT DEFAULT 0;
  DECLARE v_guest_id  INT;
  DECLARE v_tier      VARCHAR(10);
  DECLARE v_spending  DECIMAL(12,2);
  DECLARE v_next_tier VARCHAR(10);

  -- (1) cursor: each guest, current tier and lifetime spending
  DECLARE cur_guests CURSOR FOR
    SELECT g.GuestID, g.LoyaltyTier, COALESCE(SUM(p.Amount), 0)
    FROM GUEST g
    LEFT JOIN RESERVATION r ON r.GuestID = g.GuestID
    LEFT JOIN PAYMENT p     ON p.ReservationID = r.ReservationID
    GROUP BY g.GuestID, g.LoyaltyTier;

  -- (2) handler: set the done flag when the cursor runs out of rows
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;

  OPEN cur_guests;

  -- (3) loop through every guest
  guest_loop: LOOP
    FETCH cur_guests INTO v_guest_id, v_tier, v_spending;
    IF v_done = 1 THEN
      LEAVE guest_loop;
    END IF;

    -- (4) upgrade exactly one level, never downgrade, skip Platinum
    IF v_spending >= p_threshold AND v_tier <> 'Platinum' THEN
      SET v_next_tier = CASE v_tier
                          WHEN 'None'   THEN 'Silver'
                          WHEN 'Silver' THEN 'Gold'
                          WHEN 'Gold'   THEN 'Platinum'
                        END;

      UPDATE GUEST
      SET LoyaltyTier = v_next_tier
      WHERE GuestID = v_guest_id;
    END IF;
  END LOOP guest_loop;

  -- (5) close the cursor
  CLOSE cur_guests;
END$$
DELIMITER ;

CALL sp_apply_loyalty_upgrades(1500.00);

Each numbered comment maps to one requirement in the brief, which makes the marking easy to follow. The cursor uses LEFT JOINs so guests with no spending are still checked, and the CASE expression moves a guest up exactly one tier.

F6. Audit trigger trg_payment_audit, with a testsql
CREATE TABLE AUDIT_LOG (
  LogID         INT AUTO_INCREMENT PRIMARY KEY,
  PaymentID     INT,
  ReservationID INT,
  Amount        DECIMAL(10,2),
  LoggedAt      DATETIME
);

CREATE TRIGGER trg_payment_audit
AFTER INSERT ON PAYMENT
FOR EACH ROW
  INSERT INTO AUDIT_LOG (PaymentID, ReservationID, Amount, LoggedAt)
  VALUES (NEW.PaymentID, NEW.ReservationID, NEW.Amount, NOW());

-- Test: add a payment against a cancelled reservation (it has no payment yet)
INSERT INTO PAYMENT (PaymentID, ReservationID, PaymentDate, Amount, PaymentMethod)
SELECT 9001, ReservationID, CURDATE(), 150.00, 'Credit Card'
FROM RESERVATION
WHERE Status = 'Cancelled'
ORDER BY ReservationID
LIMIT 1;

SELECT * FROM AUDIT_LOG;

What a complete answer to this brief includes

  • ER and EER diagrams with a written justification for each cardinality and participation choice.
  • Step-by-step 1NF, 2NF and 3NF tables with every functional dependency labelled as partial or transitive.
  • Primary key, NOT NULL and UNIQUE constraints justified with business rules from the scenario.
  • 25 business queries and 5 challenge queries, each with three rows of output as the brief requests.
  • F1 to F6: single-row and group functions, UNION, UNION ALL, INTERSECT and EXCEPT, the stored function, procedure and trigger.
  • A short approach note that states any assumptions made about the business rules.

Common mistakes in database projects like this

  • Treating services_booked as a functional dependency, when it is really a many-to-many relationship.
  • Using RoomNumber alone as a key, even though room "101" exists in every hotel.
  • Drawing NEGOTIATED_RATE from GUEST instead of CORPORATE_GUEST.
  • Using NOT IN with a subquery that can return NULL, which silently returns no rows.
  • Leaving out READS SQL DATA or DETERMINISTIC on a MySQL function, which causes error 1418 when binary logging is on.
  • Writing a cursor loop without a NOT FOUND handler, so the procedure fails with error 1329 after the last row.
FAQ

Frequently Asked Questions

Can I submit this sample as my own work?

No. This sample is published for reference only, so you can see our approach and the quality of our work. Copying it would count as plagiarism. If you need help with your own database project, our experts can guide you through your brief step by step.

Can you help with ER diagrams and SQL projects like this one?

Yes. Our computing experts help with ER and EER modelling, normalisation and SQL, including joins, subqueries, views, stored procedures and triggers. Share your brief on WhatsApp and we will explain how we can help.

How much does help with a database project cost?

Report-based work starts from £20 per 1,000 words. For SQL and code-focused projects, the price depends on scope and we give you a clear quote on WhatsApp. New customers get 10% off their first order.

Why is only part of the solution shown?

We show a preview so the sample stays useful for learning without becoming a ready-made answer. All personal details have been removed.

Should I use Chen or Crow's Foot notation for my ER diagram?

Use the notation your module asks for. If you can choose, Crow's Foot is easier to read for large designs because cardinality sits on the line itself. Whichever you pick, use it consistently across the whole diagram.

Get expert help with your own brief

Tell us what you need and we will reply on WhatsApp with a clear price. 50% to start, 50% on completion.

Get a Free Quote

Takes 30 seconds. We reply on WhatsApp quickly.

📎 Attach brief or draft (optional)▾

Drop files here or click to upload

PDF, DOC, DOCX, JPG, PNG, ZIP — Max 10MB each, up to 5 files

Up to 10% OFFFirst Order