electronic leave application database design - php

I am currently working on a leave application (which is a subset of my e-scheduler project) and I have my database design as follows:
event (event_id, dtstart, dtend... *follows icalendar standard*)
event_leave (event_id*, leave_type_id*, total_days)
_leave_type (leave_type_id, name, max_carry_forward)
_leave_allocation (leave_allocation_id, leave_type_id*, name, user_group_id, total_days, year)
_leave_carry_forward(leave_carry_forward_id, leave_type_id*, user_id, year)
Does anyone here in stackoverflow also working on an e-leave app? mind to share your database design as I am looking for a better design than mine. The problem with my current design only occurs at the beginning of the year when the system is calculating the number of days that can be carried forward.
In total I would have to run 1 + {$number_of users} * 2 queries (the first one to find out the number of allocation rules and the maximum carry forward quota. Then for each user, I need to find out the balance, and then to insert the balance to the database)

I'm not following the schema very well (it looks like each leave_type would have a carry forward? There's no user on the event* tables?) but you should be able to dynamically derive the balance at any point in time - including across years.
AAMOF, normalization rules would require you to be able to derive the balance. If you then chose to denormalize for performance is up to you, but the design should support the calculated query. Given that, then calculating the year end carryforward is a single set based query.
Edit: I had to change the schema a bit to accommodate this, and I chose to normalize to make the logic easier - but you can insert denormalization along the way for performance if you need to:
First the tables that are important for this scenario...hopefully my pseudo-syntax will make sense:
User { User_Id (PK) }
// Year may be a tricky business logic issue here...Do you charge the Start or End year
// if the event crosses a year boundary? Or do you just do 2 different events?
// You want year in this table, though, so you can do a FK reference to Leave_Allocation
// Some RDBMS will let you do a FK from a View, though, so you could do that
Event { Event_Id (PK), User_Id, Leave_Type_Id, Year, DtStart, DtEnd, ...
// Ensure that events are charged to leave the user has
FK (User_Id, Leave_Type_Id, Year)->Leave_Allocation(User_Id, Leave_Type_Id, Year)
}
Leave_Type { Leave_Type_Id, Year, Max_Carry_Forward
// Max_Carry_Forward would probably change per year
PK (Leave_Type_Id, Year)
}
// Starting balance for each leave_type and user, per year
// Not sure the name makes the most sense - I think of Allocated as used leave,
// so I'd probably call this Leave_Starting_Balance or something
Leave_Allocation { Leave_Type_Id (FK->Leave_Type.Leave_Type_Id), User_Id (FK->User.User_Id), Year, Total_Days
PK (Leave_Type_Id, User_Id, Year)
// Ensure that leave_type is defined for this year
FK (Leave_Type_Id, Year)->Leave_Type(Leave_Type_Id, Year)
}
And then, the views (which is where you may want to apply some denormalization):
/* Just sum up the Total_Days for an event to make some other calcs easier */
CREATE VIEW Event_Leave AS
SELECT
Event_Id,
User_Id,
Leave_Type_Id,
DATEDIFF(d, DtEnd, DtStart) as Total_Days,
Year
FROM Event
/* Subtract sum of allocated leave (Event_Leave.Total_Days) from starting balance (Leave_Allocation) */
/* to get the current unused balance of leave */
CREATE VIEW Leave_Current_Balance AS
SELECT
Leave_Allocation.User_Id,
Leave_Allocation.Leave_Type_Id,
Leave_Allocation.Year,
Leave_Allocation.Total_Days - SUM(Event_Leave.Total_Days) as Leave_Balance
FROM Leave_Allocation
LEFT OUTER JOIN Event_Leave ON
Leave_Allocation.User_Id = Event_Leave.User_Id
AND Leave_Allocation.Leave_Type_Id = Event_Leave.Leave_Type_Id
AND Leave_Allocation.Year = Event_Leave.Year
GROUP BY
Leave_Allocation.User_Id,
Leave_Allocation.Leave_Type_Id,
Leave_Allocation.Year,
Leave_Allocation.Total_Days
Now, our Leave CarryForward query is just the minimum of current balance or maximum carryforward as of midnight on 1/1.
SELECT
User_Id,
Leave_Type_Id,
Year,
/* This is T-SQL syntax...your RDBMS may be different, but should be able to do the same thing */
/* If not, you'd do a UNION ALL to Max_Carry_Forward and select MIN(BalanceOrMax) */
CASE
WHEN Leave_Balance < Max_Carry_Forward
THEN Leave_Balance
ELSE
Max_Carry_Forward
END as Leave_Carry_Forward
FROM Leave_Current_Balance
JOIN Leave_Type ON
Leave_Current_Balance.Leave_Type_Id = Leave_Type.Leave_Type_Id
/* This assumes max_carry_forward is how much you can carry_forward into the next year */
/* eg,, a max_carry_forward of 300 hours for year 2008, means I can carry_forward up to 300 */
/* hours into 2009. Otherwise, you'd join on Leave_Current_Balance.Year + 1 if it's how much */
/* I can carry forward into *this* year. */
AND Leave_Current_Balance.Year = Leave_Type.Year
So, at the end of the year, you'd insert the CarryForward balances back into LeaveAllocation with the new year.

There is always a better design!!
Does your current design work? How many users do you expect (ie does it matter you would have to run x thousand queries).
If the problem of the current design is only at the beginning of the year then perhaps you could live with it!
Cheers
NZS

Further notes on my database design and some use cases.
Table Design
This is the main table (basically based on iCalendar schema) that stores event. The event may be a typical event, or a meeting, public holiday etc.
event (event_id (PK), dtstart, dtend, ... --other icalendar fields--)
If a particular type of event has extra information that I have to keep track, I decorate it with another table. For instance, the table to store e-leave specific information. (total_days is not a computed field as part of the requirements)
event_leave (event_id (PK/FK->event), total_days, leave_type_id (FK->leave_type))
Leave type table stores some information on each leave type. For instance, does the application needs approval/recommendation etc. Besides that, it also stores the maximum carry forward allowed. I assume the maximum carry forward would not be altered frequently.
leave_type (leave_type_id (PK), name, require_support, require_recommend, max_carry_forward)
Users are divided into groups, and each group will be given a number of days available for leave for some of the leave_type. Data stored in this table will be populated annually (a new revision for each year). It only stores the number of leave given for each group, not per user.
leave_allocation (leave_allocation_id, year(PK), leave_type_id (PK/FK->leave_type), total_days, group_id)
Next is the table to store carry forward information. This table will be populated once every year for each user. This table will be populated once a year as calculation on the fly is not easy. The formula of counting leave_carry_forward for the user is:
leave_carry_forward(2009) = min(leave_allocation(2008) + leave_carry_forward(2007) - leave_taken(2008), maximum_carry_forward());
leave_carry_forward (leave_carry_forward_id, user_id, year, total_days)
Some Example Use Cases and Solution
Calculate Balance (WIP)
To calculate balance, I make a query to the view declared as follows
DROP VIEW IF EXISTS leave_remaining_days;
CREATE OR REPLACE VIEW leave_remaining_days AS
SELECT year, user_id, leave_type_id, SUM(total_days) as total_days
FROM (
SELECT allocated.year, usr.uid AS "user_id", allocated.leave_type_id,
allocated.total_days
FROM users usr
JOIN app_event._leave_allocation allocated
ON allocated.group_id = usr.group_id
UNION
SELECT EXTRACT(year FROM event.dtstart) AS "year", event.user_id,
leave.leave_type_id, leave.total_days * -1 AS total_days
FROM app_event.event event
LEFT JOIN app_event.event_leave leave
ON event.event_id = leave.event_id
UNION
SELECT year, user_id, leave_type_id, total_days
FROM app_event._leave_carry_forward
) KKR
GROUP BY year, user_id, leave_type_id;
Populate leave_allocation table at the beginning of year
public function populate_allocation($year) {
return $this->db->query(sprintf(
'INSERT INTO %s (%s)' .
"SELECT '%s' AS year, %s " .
'FROM %s ' .
'WHERE "year" = %s',
'event_allocation',
'year, leave_type_id, total_days ...', //(all the fields in the table)
empty($year) ? date('Y') : $year,
'leave_type_id, total_days, ..', //(all fields except year)
$this->__table,
empty($year) ? date('Y') - 1 : $year - 1
))
->count() > 0; // using the database query builder in Kohana PHP framework
}
Populate leave_carry_forward table at the beginning of year
Find out leave type assigned to the user
I would probably need to rename this view (I am bad in naming stuff...). It is actually a leave_allocation table for a user.
DROP VIEW IF EXISTS user_leave_type;
CREATE OR REPLACE VIEW user_leave_type AS
SELECT la.year, usr.uid AS user_id, lt.leave_type_id, lt.max_carry_forward
FROM users usr
JOIN app_event._leave_allocation la
JOIN app_event._leave_type lt
ON la.leave_type_id = lt.leave_type_id
ON usr.group_id = la.group_id
The actual query
INSERT INTO leave_carry_forward (year, user_id, leave_type_id, total_days)
SELECT '{$this_year}' AS year, user_id, leave_type_id, MIN(carry_forward) AS total_days
FROM (
SELECT year, user_id, leave_type_id, total_days AS carry_forward
FROM leave_remaining_days
UNION
SELECT year, user_id, leave_type_id, max_carry_forward AS carry_forward
FROM user_leave_type
) KKR
WHERE year = {$last_year}
GROUP BY year, user_id, leave_type_id;

Related

Add more columns in NOT IN MySQL?

I have a table in MySQL called events and another one called vehicles.
events table - id, start_date, end_date, start_time, end_time, user, vehicle
vehicles table - id, registration, model, enabled.
When a person is going to make a reservation, the system has to assign a vehicle automatically (while it is enabled), but for this I have to know that it is not reserved for events in the range of dates and times that the user wants.
I have the following code:
SELECT vehicles.registration
FROM vehicles WHERE vehiculos.registration NOT IN (
SELECT VEHICLE FROM events
WHERE '$ aux_ini'> = CONCAT (date_ini, '', hour_ini)
OR '$ aux_fin' <= CONCAT (end_date, '', end_time)
) AND enabled = 'S'
ORDER BY id ASC
LIMIT 1
Where $aux_ini and $aux_fin is start_date + start_time concatenated and end_date + end_time concatenated.
It worked correctly until the reservations of all vehicles have been made, now you can not reserve any vehicle (although the dates are different) because it ignores the second WHERE (the NOT IN) since it only leaves me select a field. Any solution?

Finding events in a MySQL table, manipulating and storing the data

This is question I have still not been able to resolve, so maybe I need to be clearer in what I need.
I have a datasource which I receive and upload to a MySQL database each month. I cannot alter the data although it may not be in the most helpful format. It looks like this:
Ref Action Date/Time User Location
00123 Create 01:02:12_09:13:13 J Jones Home
00456 Create 01:02:12_09:13:13 J Jones Home
00123 Revise 03:02:12_15:20:01 A Smith Home
00789 Create 01:02:12_09:13:13 J Jones Home
00123 Delete 05:02:12_10:51:45 B Halls Home
x 1000's
It tracks events that occur against a reference number, which is generated by the first event (the Create event). These events occur at varying intervals and are done by various people in various locations.
Using the above example I need to be able to loop through the data for a particular month, pull out a reference number and its 'Create' event, then locate all the other events for that ref number.
I then need to be able to manipulate this information, for example calculate the time difference between Create and Revise, and then Revise and Delete, and who did them, where.
I need to be able to this for all the ref numbers created in the month or a date range.
So I am hoping I can create a query that can do this - find a ref number, find the other events, string them together in some way - so that in the end I have new data -
Ref Number, Time of Create, Create By Who, Time of Revise/By Who, Time of Delete etc
It would be useful if this new data could be stored in a new table, I would think(?)
Can this be done as a query or perhaps combination of query and PHP (arrays?)
Okay, the first stage is to transform this data in a MySQL table, and
you will have something like
ref integer,
action enum('create','revise','delete'), -- ordered by life cycle: create first, delete last
ts timestamp,
user varchar(32), -- or maybe user_id
loc varchar(32), -- again maybe location_id, or ENUM
So to pull out an event given its reference
SELECT * FROM mytable WHERE ref = 123 ORDER BY action;
For date ranges you can use
WHERE ts BETWEEN 'date1' AND 'date2'
To get time differences,
SELECT TIMEDIFF(b.ts, a.ts) AS delta, b.*
FROM mytable AS a
JOIN mytable AS b ON (a.action = 'CREATE' AND a.ref = b.ref)
WHERE a.ref = 123;
Finally to denormalize the table:
SELECT _create.*,
TIMEDIFF(_revise.ts - _create.ts) AS revise_delta,
TIMEDIFF(_delete.ts - _delete.ts) AS delete_delta,
FROM mytable AS _create
LEFT JOIN mytable AS _revise ON (_create.ref = _revise.ref
AND _create.action = 'CREATE' AND AND _revise.action = 'REVISE' AND ***)
LEFT JOIN mytable AS _delete ON (_create.ref = _delete.ref
AND _create.action = 'CREATE' AND AND _delete.action = 'DELETE' AND ***)
;
Here * is some way of identifying the unique relation between a CREATE event and the corresponding DELETE event.
If refs are unique, then AND *** is not needed (* equals to True).
For example if the refs are recycled every month, and it never happens that an event may span two months, you can impose that the year and month of _create should be the same as those of _revise; that and the equality of .ref establish a biunivocal match.
Otherwise it gets much more complicated, and I'd try creating a VIEW that for each CREATE event selects the COALESCE of NOW() and the datetime of the earliest CREATE event with that same ref but ts greater or equal to the current. This way you identify a "window" in which events with that ref should be attributed to that specific CREATE. But this is based on the hypothesis that it never happens that
00123 CREATE 01-NOV-2012 Jack
00123 CREATE 04-NOV-2012 Jill
00123 DELETE 05-NOV-2012 Joe <-- which event is this one?
Well, to get field values for a particular action:
$db_connection = mysqli_connect();//Have DB variables here
$ref = ;//Ref number here e.g 123
$action = '';//action to check for here e.g create
$query = "SELECT * FROM yourtable WHERE ref=".$ref." AND action=".$action."";
$q = mysqli_query($db_connection,$query);
while($row = mysqli_fetch_asocc($q)){
//The variables are in the $row array with each on an index based on a column name
}

Designing my MySQL Database for rating things on different days

I've been using reddit as my help recently, and they've been great but then I found this site and feel it's a much more appropriate place to ask my questions. Hopefully the helpfulness trend continues here. Let me start by saying I know this project is pretty intensive considering I have never done anything like it before, however that doesn't change the fact that I am doing it and I am willing to learn how.
I am designing a website which will feature the the bar and dining specials for my city. Users will be able to log on, and the home page will display the top 5 rated bar specials/ happy hours, and the top 5 dining specials/happy hours for that day(i.e. monday, tuesday, etc.). Each "special" will have an up down rating for the user to give their input on the special.
A bar page will list all of the bars and their specials for that day, and the option to upvote or downvote the special. Same thing with a dining page. From there, the person can click on the bar name to be taken to a page specifically about that bar. That page will show the specials for the entire week.
I'm in the process of designing the MySQL table now in phpmyadmin, as it seems like that's where I need to start after having written the html and css for the design. I've tried googling for a framework that I could build off of, however I didn't come up with anything relevant.
Would it be best to make separate tables under each database for each day of the week, put everything into one table, or another way entirely. This is what I made so far, let me know if I am on the right track or not. Thanks a million!
!(http://dl.dropbox.com/u/16887445/Screen%20shot%202011-07-06%20at%208.01.10%20PM.png)
Day of the week is an attribute of a "specials" record, not a record unto itself. Picture this -- if you have one table per day, and you want to get all specials for one bar for a week, you'll have to make seven queries:
SELECT * FROM sunday_specials WHERE
establishment_id = <id> and date = <sunday>;
SELECT * FROM monday_specials WHERE
establishment_id = <id> and date = <monday>;
SELECT * FROM tuesday_specials WHERE
establishment_id = <id> and date = <tuesday>;
...
This is (hopefully) obviously a bad design. Whay you want is to do be able to do it with one query:
SELECT * FROM specials WHERE
establishment_id = <id> and date >= <sunday> and date <= <saturday>;
Note also that you don't even really care about storing the day of the week, all you need is the date (which you need anyway), from which you can extract day of the week. I'd create a schema something like this:
-- This holds one record for each bar/restaurant.
CREATE TABLE establishments
(
id INT SERIAL,
name TEXT NOT NULL,
address TEXT,
phone TEXT,
uri TEXT,
type INT NOT NULL DEFAULT 1,
PRIMARY KEY (id)
);
-- This holds one record per day per establishment. Not storing the
-- day of the week allows you to keep all records going back in time.
-- Up/Down voting simply increments or decrements the rating field.
CREATE TABLE specials
(
id INT SERIAL,
establishment_id INT NOT NULL REFERENCES establishments(id),
special_date DATE NOT NULL DEFAULT NOW()::DATE,
name TEXT NOT NULL,
description NOT NULL,
rating INT NOT NULL DEFAULT 0,
PRIMARY KEY (id)
);
So, to display the specials for "My Bar", you do something like:
$start_date = <whenever>
$end_date = <whenever>
$id = SELECT id FROM establishments WHERE name = 'My Bar';
$specials = SELECT * FROM specials WHERE
id = $id AND special_date >= $start_date AND special_date <= $end_date;
To display the top five rated specials for Monday:
SELECT * FROM specials WHERE special_date = <monday> ORDER BY rating DESC LIMIT 5;
If I understand what you're going for correctly, this is probably how I'd design the schema:
TABLE location // For storing details about the bars & restaurants
location_id (pkey)
location_name
location_type // For flagging if it's a restaurant, bar, other, etc.
location_address
...
TABLE specials // Holds all the specials for all days of the week
specials_id (pkey)
location_id (fkey)
day_of_week // 1-7 values
special_name
special_detail
...
TABLE votes
vote_id (pkey)
specials_id (fkey)
vote_type // Flag for up/down (1, -1)
...
Once you have the data normalised, you can use queries or views to pull the aggregated data. For example, to find all the up votes for a special, you could use a query like this:
SELECT COUNT(*) FROM votes WHERE specials_id = '1234' AND vote_type = '1'

Creating class schedule table in SQL

I need to create a table to store users' class schedules. These schedules have 7 blocks a day for Monday through Friday. However, not all blocks are filled with classes.
I was planning on creating a table that stored stored a user's id, the period id, the class name, and the class subject in each record. If I implement it this way, what is the best way to determine when a user does not have classes using PHP? Is there a better layout for this?
You need to make three tables, and set up a many-to-many relationship.
But if you don't want to get real complex, why not just insert the students free time like a class, call it 'free time', then you can just search for those.
SELECT * FROM records WHERE student_id = '0001' AND class = 'free time'
Otherwise, I'm not sure how you'd find an empty block without having a table devoted to the blocks.
i wouldnt say you NEED to do anything, but i think you'll eventually find normalizing is a very good idea here. http://en.wikipedia.org/wiki/Database_normalization
you probably want tables for:
student (id, name, whatever)
course (id, name, subject/dept)
section (id, course id and time info)
student_to_section (student id, section id)
Time depends. You can put start/end times on sections (SQL timestamps or integer unix time stamps would each be fine) or keep a table of time slots with unique id (then sections would just have a foreign key to this id). EDIT: sounds like you've chosen the second way
As for your free-time, find the time periods for all sections taken by a student and free time is whats left. The following will give the time blocks where a student is BUSY.
SELECT T.*
FROM section S
INNER JOIN time_blocks T on S.time_id = T.id
INNER JOIN student_to_section STS on STS.section_id = S.id
WHERE STS.student_id = ###
For free time, use:
SELECT T2.*
FROM time_blocks T2
WHERE T2.id NOT IN
(put above statement here)

mysql - search between dates where all dates appear

I'm working with some imported data that stores details about whether a "room" is available on a specific day or not. Each room has an individual entry for the date that it is available.
| id | date | price |
--------------------------------
| 1 | 2010-08-04 | 45.00 |
A user can search across a date range and the search needs to bring back the relevant rooms that are available between those two dates.
In other words using a sql query to search:
where date>=2010-08-04 AND date<=2010-08-09
would not suffice as this would bring back all rooms available at SOME point between the chosen dates not the rooms that are available for ALL of the dates concerned.
I am considering using a temporary date table in some way to cross-reference that there is an entry for every date in the range but are uncertain as to the best way to implement this.
The end code platform is PHP and I'm also exploring whether the data can be processed subsequently within the code but would like to keep everything with the sql if possible.
Any suggestions that put forward would be greatly appreciated.
Thanks
Update: my original answer was identical to Quassnoi's but 1 minute too late, so I decided to delete it and do something different instead. This query does not assume that (id, date) is unique. If there is more than one entry, it selects the cheapest. Also, it also sums the total cost and returns that too which might also be useful.
SELECT id, SUM(price) FROM (
SELECT id, date, MIN(price) AS price
FROM Table1
GROUP BY id, date) AS T1
WHERE `date` BETWEEN '2010-08-05' AND '2010-08-07'
GROUP BY id
HAVING COUNT(*) = DATEDIFF('2010-08-07','2010-08-05') + 1
Provided that (id, date) combination is unique:
SELECT id
FROM mytable
WHERE date BETWEEN '2010-08-04' AND '2010-08-09'
GROUP BY
id
HAVING COUNT(*) = DATEDIFF('2010-08-09', '2010-08-04') + 1
Make sure you have a UNIQUE constraint on (id, date) and the date is stored as DATE, not DATETIME.

Categories