Exercises
Rental System
Peter runs a small car rental company with 10 cars and 5 trucks. He engages you to design a web portal to put his operation online.
For the initial phase, the web portal shall provide these basic functions:
- Maintaining the records of the vehicles and customers.
- Inquiring about the availability of vehicle, and
- Reserving a vehicle for rental.
A customer record contains his/her name, address and phone number.
A vehicle, identified by the vehicle registration number, can be rented on a daily basis. The rental rate is different for different vehicles. There is a discount of 20% for rental of 7 days or more.
A customer can rental a vehicle from a start date to an end date. A special customer discount, ranging from 0-50%, can be given to preferred customers.
Database
The initial database contains 3 tables: vehicles
, customers
, and rental_records
. The rental_records
is a junction table supporting many-to-many relationship between vehicles
and customers
.
DROP DATABASE IF EXISTS `rental_db`; CREATE DATABASE `rental_db`; USE `rental_db`; -- Create `vehicles` table DROP TABLE IF EXISTS `vehicles`; CREATE TABLE `vehicles` ( `veh_reg_no` VARCHAR(8) NOT NULL, `category` ENUM('car', 'truck') NOT NULL DEFAULT 'car', -- Enumeration of one of the items in the list `brand` VARCHAR(30) NOT NULL DEFAULT '', `desc` VARCHAR(256) NOT NULL DEFAULT '', -- desc is a keyword (for descending) and must be back-quoted `photo` BLOB NULL, -- binary large object of up to 64KB -- to be implemented later `daily_rate` DECIMAL(6,2) NOT NULL DEFAULT 9999.99, -- set default to max value PRIMARY KEY (`veh_reg_no`), INDEX (`category`) -- Build index on this column for fast search ) ENGINE=InnoDB; -- MySQL provides a few ENGINEs. -- The InnoDB Engine supports foreign keys and transactions DESC `vehicles`; SHOW CREATE TABLE `vehicles` \G SHOW INDEX FROM `vehicles` \G -- Create `customers` table DROP TABLE IF EXISTS `customers`; CREATE TABLE `customers` ( `customer_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, -- Always use INT for AUTO_INCREMENT column to avoid run-over `name` VARCHAR(30) NOT NULL DEFAULT '', `address` VARCHAR(80) NOT NULL DEFAULT '', `phone` VARCHAR(15) NOT NULL DEFAULT '', `discount` DOUBLE NOT NULL DEFAULT 0.0, PRIMARY KEY (`customer_id`), UNIQUE INDEX (`phone`), -- Build index on this unique-value column INDEX (`name`) -- Build index on this column ) ENGINE=InnoDB; DESC `customers`; SHOW CREATE TABLE `customers` \G SHOW INDEX FROM `customers` \G -- Create `rental_records` table DROP TABLE IF EXISTS `rental_records`; CREATE TABLE `rental_records` ( `rental_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `veh_reg_no` VARCHAR(8) NOT NULL, `customer_id` INT UNSIGNED NOT NULL, `start_date` DATE NOT NULL DEFAULT '0000-00-00', `end_date` DATE NOT NULL DEFAULT '0000-00-00', `lastUpdated` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Keep the created and last updated timestamp for auditing and security PRIMARY KEY (`rental_id`), FOREIGN KEY (`customer_id`) REFERENCES `customers` (`customer_id`) ON DELETE RESTRICT ON UPDATE CASCADE, -- Disallow deletion of parent record if there are matching records here -- If parent record (customer_id) changes, update the matching records here FOREIGN KEY (`veh_reg_no`) REFERENCES `vehicles` (`veh_reg_no`) ON DELETE RESTRICT ON UPDATE CASCADE ) ENGINE=InnoDB; DESC `rental_records`; SHOW CREATE TABLE `rental_records` \G SHOW INDEX FROM `rental_records` \G -- Inserting test records INSERT INTO `vehicles` VALUES ('SBA1111A', 'car', 'NISSAN SUNNY 1.6L', '4 Door Saloon, Automatic', NULL, 99.99), ('SBB2222B', 'car', 'TOYOTA ALTIS 1.6L', '4 Door Saloon, Automatic', NULL, 99.99), ('SBC3333C', 'car', 'HONDA CIVIC 1.8L', '4 Door Saloon, Automatic', NULL, 119.99), ('GA5555E', 'truck', 'NISSAN CABSTAR 3.0L', 'Lorry, Manual ', NULL, 89.99), ('GA6666F', 'truck', 'OPEL COMBO 1.6L', 'Van, Manual', NULL, 69.99); -- No photo yet, set to NULL SELECT * FROM `vehicles`; INSERT INTO `customers` VALUES (1001, 'Tan Ah Teck', '8 Happy Ave', '88888888', 0.1), (NULL, 'Mohammed Ali', '1 Kg Java', '99999999', 0.15), (NULL, 'Kumar', '5 Serangoon Road', '55555555', 0), (NULL, 'Kevin Jones', '2 Sunset boulevard', '22222222', 0.2); SELECT * FROM `customers`; INSERT INTO `rental_records` VALUES (NULL, 'SBA1111A', 1001, '2012-01-01', '2012-01-21', NULL), (NULL, 'SBA1111A', 1001, '2012-02-01', '2012-02-05', NULL), (NULL, 'GA5555E', 1003, '2012-01-05', '2012-01-31', NULL), (NULL, 'GA6666F', 1004, '2012-01-20', '2012-02-20', NULL); SELECT * FROM `rental_records`;
Exercises
Customer
'Tan Ah Teck'
has rented'SBA1111A'
from today for 10 days. (Hint: You need to insert a rental record. Use aSELECT
subquery to get thecustomer_id
. UseCURDATE()
(orNOW()
) for today; andDATE_ADD(CURDATE(), INTERVAL x unit)
to compute a future date.)INSERT INTO rental_records VALUES (NULL, 'SBA1111A', (SELECT customer_id FROM customers WHERE name='Tan Ah Teck'), CURDATE(), DATE_ADD(CURDATE(), INTERVAL 10 DAY), NULL);
Customer
'Kumar'
has rented'GA5555E'
from tomorrow for 3 months.List all rental records (start date, end date) with vehicle's registration number, brand, and customer name, sorted by vehicle's categories followed by start date.
SELECT r.start_date AS `Start Date`, r.end_date AS `End Date`, r.veh_reg_no AS `Vehicle No`, v.brand AS `Vehicle Brand`, c.name AS `Customer Name` FROM rental_records AS r INNER JOIN vehicles AS v USING (veh_reg_no) INNER JOIN customers AS c USING (customer_id) ORDER BY v.category, start_date;
List all the expired rental records (end_date before
CURDATE()
).- List the vehicles rented out on
'2012-01-10'
(not available for rental), in columns of vehicle registration no, customer name, start date and end date. (Hint: the given date is in between thestart_date
andend_date
.) - List all vehicles rented out today, in columns registration number, customer name, start date, end date.
- Similarly, list the vehicles rented out (not available for rental) for the period from
'2012-01-03'
to'2012-01-18'
. (Hint:start_date
is inside the range; orend_date
is inside the range; orstart_date
is before the range andend_date
is beyond the range.) - List the vehicles (registration number, brand and description) available for rental (not rented out) on
'2012-01-10'
(Hint: You could use a subquery based on a earlier query). - Similarly, list the vehicles available for rental for the period from
'2012-01-03'
to'2012-01-18'
. - Similarly, list the vehicles available for rental from today for 10 days.
- Foreign Key Test:
- Try deleting a parent row with matching row(s) in child table(s), e.g., delete
'GA6666F'
fromvehicles
table (ON DELETE RESTRICT
). - Try updating a parent row with matching row(s) in child table(s), e.g., rename
'GA6666F'
to'GA9999F'
invehicles
table. Check the effects on the child tablerental_records
(ON UPDATE CASCADE
). - Remove
'GA6666F'
from the database (Hints: Remove it from child tablerental_records
; then parent tablevehicles
.)
- Try deleting a parent row with matching row(s) in child table(s), e.g., delete
Payments: A rental could be paid over a number of payments (e.g., deposit, installments, full payment). Each payment is for one rental. Create a new table called
payments
. Need to create columns to facilitate proper audit check (such ascreate_date
,create_by
,last_update_date
,last_update_by
, etc.)DROP TABLE IF EXISTS `payments`; CREATE TABLE payments ( `payment_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `rental_id` INT UNSIGNED NOT NULL, `amount` DECIMAL(8,2) NOT NULL DEFAULT 0, `mode` ENUM('cash', 'credit card', 'check'), `type` ENUM('deposit', 'partial', 'full') NOT NULL DEFAULT 'full', `remark` VARCHAR(255), `created_date` DATETIME NOT NULL, `created_by` INT UNSIGNED NOT NULL, -- staff_id -- Use a trigger to update create_date and create_by automatically `last_updated_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Updated by the system automatically `last_updated_by` INT UNSIGNED NOT NULL, -- Use a trigger to update created_by PRIMARY KEY (`payment_id`), INDEX (`rental_id`), FOREIGN KEY (`rental_id`) REFERENCES rental_records (`rental_id`) ) ENGINE=InnoDB; DESC `payments`; SHOW CREATE TABLE `payments` \G SHOW INDEX FROM `payments` \G
Staff: Keeping track of staff serving the customers. Create a new
staff
table. Assume that each transaction is handled by one staff, we can add a new column calledstaff_id
in therental_records
table,DROP TABLE IF EXISTS `staff`; CREATE TABLE `staff` ( `staff_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, -- Always use INT for AUTO_INCREMENT column to prvent run-over `name` VARCHAR(30) NOT NULL DEFAULT '', `title` VARCHAR(30) NOT NULL DEFAULT '', `address` VARCHAR(80) NOT NULL DEFAULT '', `phone` VARCHAR(15) NOT NULL DEFAULT '', `report_to` INT UNSIGNED NOT NULL, -- Reports to manager staff_id. Boss reports to himself PRIMARY KEY (`staff_id`), UNIQUE INDEX (`phone`), -- Build index on this unique-value column INDEX (`name`), -- Build index on this column FOREIGN KEY (`report_to`) REFERENCES `staff` (`staff_id`) -- Reference itself ) ENGINE=InnoDB; DESC `staff`; SHOW INDEX FROM `staff` \G INSERT INTO staff VALUE (8001, 'Peter Johns', 'Managing Director', '1 Happy Ave', '12345678', 8001); SELECT * FROM staff; -- Add a new column to rental_records table ALTER TABLE `rental_records` ADD COLUMN `staff_id` INT UNSIGNED NOT NULL; -- Need to set to a valid value, before adding the foreign key UPDATE `rental_records` SET `staff_id` = 8001; ALTER TABLE `rental_records` ADD FOREIGN KEY (`staff_id`) REFERENCES staff (`staff_id`) ON DELETE RESTRICT ON UPDATE CASCADE; SHOW CREATE TABLE `rental_records` \G SHOW INDEX FROM `rental_records` \G -- Also Add a new column to payments table ALTER TABLE `payments` ADD COLUMN `staff_id` INT UNSIGNED NOT NULL; -- Need to set to a valid value, before adding the foreign key UPDATE `payments` SET `staff_id` = 8001; ALTER TABLE `payments` ADD FOREIGN KEY (`staff_id`) REFERENCES staff (`staff_id`) ON DELETE RESTRICT ON UPDATE CASCADE; SHOW CREATE TABLE `payments` \G SHOW INDEX FROM `payments` \G
Advanced Exercises
Adding Photo: We could store photo in MySQL using data type of
BLOB
(Binary Large Object) (up to 64KB),MEDIUMBLOB
(up to 16MBytes),LONGBOLB
(up to 4GBytes). For example,-- Use function LOAD_FILE to load a picture file into a BLOB field UPDATE vehicles SET photo=LOAD_FILE('d:/temp/car.jpg') WHERE veh_reg_no = 'SBA1111A'; SELECT * FROM vehicles WHERE veh_reg_no = 'SBA1111A' \G
You can conveniently load and view the photo via graphical tools such as MySQL Workbench. To load a image in MySQL Workbench ⇒ right-click on the cell ⇒ Load Value From File ⇒ Select the image file. To view the image ⇒ right-click on the BLOB cell ⇒ Open Value in Editor ⇒ choose "Image" pane. I also include a Java program for reading and writing image BLOB from/to the database, based on this example: "
TestImageBLOB.java
".VIEW
: Create aVIEW
calledrental_prices
on therental_records
with an additional column calledprice
. Show all the records of theVIEW
.DROP VIEW IF EXISTS rental_prices; CREATE VIEW **rental_prices** AS SELECT v.veh_reg_no AS `Vehicle No`, v.daily_rate AS `Daily Rate`, c.name AS `Customer Name`, c.discount*100 AS `Customer Discount (%)`, r.start_date AS `Start Date`, r.end_date AS `End Date`, DATEDIFF(r.end_date, r.start_date) AS `Duration`, -- Compute the rental price -- Preferred customer has discount, 20% discount for 7 or more days -- CAST the result from DOUBLE to DECIMAL(8,2) CAST( IF (DATEDIFF(r.end_date, r.start_date) < 7, DATEDIFF(r.end_date, r.start_date)*daily_rate*(1-discount), DATEDIFF(r.end_date, r.start_date)*daily_rate*(1-discount)*0.8) AS DECIMAL(8,2)) AS price FROM rental_records AS r INNER JOIN vehicles AS v USING (veh_reg_no) INNER JOIN customers AS c USING (customer_id); DESC `rental_prices`; SHOW CREATE VIEW `rental_prices` \G -- Try selecting all rows SELECT * FROM `rental_prices`;
It is probably easier to compute the price using a program/procedure, instead of inside the view.
- From the
payments
table, create a view to show the outstanding balance. - Define more views.
FUNCTION
: Write a function to compute the rental price.- Define more procedures and functions.
TRIGGER
: Write a trigger for thecreated_date
andcreated_by
columns of thepayments
table.- Define more triggers.
Implement discount on weekday (Monday to Friday, except public holiday): Need to set up a new table called
public_hoilday
with columnsdate
anddescription
. Use functionDAYOFWEEK
(1=Sunday, …, 7=Saturday) to check for weekday or weekend.-- pseudocode for calculating rental price price = 0; for each date from start_date to end_date { if date is weekend or public_holiday, price += daily_rate; else price += daily_rate*(1-discount); } if (duration >= 7) price *= (1 - long_duration_discount); price *= (1 - perferred_customer_discount);
Product Sales Database
[TODO] Explanation
Link to MySQL References & Resources
Latest version tested: MySQL Community Server 5.6.20 Last modified: September, 2014