I'm currently working on an Equipment Reservation System for my school.
Here's basically what my tables look like:
tblEquipment:
id name description
1 Camera Takes pictures
2 Projector Projects images
3 Stereo Plays music
tblEvents:
id equipmentID start end
1 2,3 1251312300 1251315900 //Should I use comma delimited entries for equipmentID?
2 1 1251312300 1251315900
Regarding my project, I have a couple of questions:
1) If multiple pieces of equipment are being reserved, (which will happen more times than not) should the "equipmentIDs" be comma delimited in the equipmentID field?
2) Currently, when a user makes a reservation, they first select their "requested times", then are presented with available items at that time. Here's what I am using for that query:
$start = //user's requested time
$start = //user's requested time
SELECT equipmentID FROM tblEvents
WHERE ($start >= start && $start <= end)
OR ($end >= start && $end <= end)
OR ($start <= start && $end >= end
while($row = mysql_fetch_array($data)) {
echo $row['equipmentID']; //Would echo something like:
echo "<br>"; // 2,3
// 1
}
My question is this:
How can I take the 'results' of the above query to then re-query the 'tblequipment' table, but exclude the items that were in the 'results' above (because they would not be available). Keeping in mind, that my query above may return multiple rows.
Any help on this would be great, thanks!
Regarding #1: No! No, no, no, no, no! If you have multiple pieces of equipment being reserved, then you should have multiple rows in the reservations table (what looks like tblEvents here). To avoid duplicating the other fields in tblEvents, you'd typically create a third table, perhaps tblEventEquipment that simply lists what equipment belongs with what event.
If you need a comma-separated list for the purposes of output (which doesn't seem likely), you can always generate one with GROUP_CONCAT(), but inside the table you want one row per reserved piece of equipment. Otherwise, SQL cannot effectively (or efficiently) determine what equipment is reserved at a particular time.
Regarding #2, you want a query like:
SELECT *
FROM tblEquipment
WHERE NOT EXISTS (
SELECT 1
FROM tblEvents
WHERE tblEvents.equipmentID = tblEquipment.equipmentID
AND $end >= start AND $start <= end
)
This selects equipment for which there isn't a reservation. Note that I simplified your logic for determining whether the equipment is reserved or not by doing just two comparisons.
Finally, an unrelated note: I recommend strongly against storing timestamps as integers in the database table. Use MySQL's built-in DATETIME type. If you need to convert to/from a timestamp, you can use the UNIX_TIMESTAMP() and FROM_UNIXTIME() functions.
No, don't use comma-seperated values. If you want a user to have the ability to check-out multiple items, you'll need a new table:
Tables: Users, Property, Checkout
The new Checkout table will have the following fields:
id
person_id
property_id
checkout_date
checkin_date
This table can have multiple entries for any particular user. A user may be in there once for a company laptop, and again for a company projector:
1 | 12 | 23 | 2009-08-17 | 0000-00-00
2 | 12 | 28 | 2009-08-17 | 0000-00-00
As for checking if an item is reserved, I would as a field in the property table to hold a boolean value:
is_reserved (BOOL)
Finding items that are available is nothing more than checking all items with a BOOL value of false, and no presence in the checkout table coupled with no checkin_date.
Related
I have some tasks that I have to store in my database. And each task has an array of dates in which the tasks were completed. I've learn that it is better to not use a array (serialize) to store dates, but instead make another table. So I did:
taskTable contains columns: taskID, userid, description, name
task_days contains columns: taskID, day
But Im having trouble with php,
usually I can easily send my data to client with:
function getTasks(){
$app = \Slim\Slim::getInstance();
$userid = $app->request->params('userid');
$db = getDB();
$result = $db->prepare("Select * From taskTable where userid = ?");
$result->execute(array($userid));
$result->setFetchmode(PDO::FETCH_ASSOC);
echo json_encode($result->fetchAll());
}
I encode it, then client can easily read it as an array of JSON. But now with two tables, I'm not sure how to do it efficiently. I know I can get the required information with this query:
Select * from taskTable as t, task_days as d where t.taskID = d.taskID
But how do I make it so the days will be in an array associated with the correct task.
Do I first Select * From taskTable where userid = $userid, then for each task, I will do a query on table task_days? that seems extremely inefficient though.
So I want something like the following:
[
{taskid: 123, userid: 1, description: "do task", name: "tony", day:[1998-01-02, 1998-02-03]},
{taskid: 124, userid: 2, description: "do task2", name: "Ann", day:[2016-01-02, 2016-02-03, 2016-01-01]},
...
]
There's a couple of approaches.
1) One approach, as you already outline, is to run a query that returns the the columns from just `taskTable`. And for each row returned, run another query to get the associated rows from task_days. And you are right, that's usually not the most efficient approach. But for a reasonably small number of rows, performance should be reasonable as long as appropriate indexes are available.)
2) Another approach, assuming `taskid` is the primary key of `taskTable` is to perform a join, and use a "GROUP BY" to collapse the rows. The "GROUP_CONCAT" aggregate function can convert the multiple values of `day` from the `task_days` table into a single string. For example:
SELECT t.taskid
, t.userid
, t.description
, t.name
, GROUP_CONCAT(d.day ORDER BY d.day) AS `day`
FROM taskTable t
LEFT
JOIN task_days d
ON d.taskid = t.taskid
GROUP BY t.taskid
ORDER BY t.taskid
This would return the day as a string, not an array. If you need an array, your code would need to do that. (As a convenient way to do that, the PHP explode function might be suitable.)
NOTE: the length of the string returned by GROUP_CONCAT is limited by group_concat_max_len variable, and also by max_allowed_packet.
3) Another way to approach this is to perform a join operation, and pull back the "duplicated" task information, ordered by taskid and day
SELECT t.taskid
, t.userid
, t.description
, t.name
, d.day
FROM taskTable t
LEFT
JOIN task_days d
ON d.taskid = t.taskid
ORDER BY t.taskid, d.day
That would get a result set like this:
taskid userid description name day
------ ------ ----------- ----- ----------
123 1 do task tony 1998-01-02
123 1 do task tony 1998-02-03
124 2 do task2 Ann 2016-01-02
124 2 do task2 Ann 2016-02-03
124 2 do task2 Ann 2016-01-01
Then your code would need to do some rudimentary "control break" processing. Basically, compare the taskid of the current row to the taskid from the previous row. If they match, you are processing just a new `day` value for the same task.
If the taskid of the current row is different than the taskid from the previous row, then you are starting a new task.
Your code would effectively be ignoring the duplicated rows from `taskTable`, basically squinting at the result set and seeing it like this:
taskid userid description name day
------ ------ ----------- ----- ----------
- 123 1 do task tony 1998-01-02
+ 1998-02-03
- 124 2 do task2 Ann 2016-01-02
+ 2016-02-03
+ 2016-01-01
FOLLOWUP
The second option is closest to your original implementation, a comma separated list of values as a string, in a character column.
As far as storing a comma separated list, that's a SQL anti-pattern, and it's usually best avoided it. Multi-valued attributes can be stored in a separate table, like you have done.
The exception would be if you never, ever need the database to see the values in the list as separate values.
If you are storing that "list of dates" as if it were an image, for example like the contents of a jpeg... if you always store the entire value into the column, and always extract the contents of the column as a single value... if never need to search for an individual date, or add a date to an existing list, or remove a date from a list... and if you never need the database to enforce any constraints on the values, or do any validation of the contents...
If all of those conditions are satisfied, only then might it make sense to store a comma separated list as a single column.
My personal preference, if the implementation is targeted only to MySQL, would be the second option... using GROUP_CONCAT. If the length of the string generated by the GROUP_CONCAT exceeds group_concat_max_len, the string will be truncated, with no warning or error. (I believe that's a limitation in bytes, and not characters.)
The safest coding practice would be to do perform a query:
SELECT ##session.group_concat_max_len
save the value returned by that. Then, for the values returned from the GROUP_CONCAT expression, compare the length (in bytes) to the saved value, to see if truncation has occurred. (If the length of the returned string is less than the value of group_concat_max_len, then you can be pretty confident that truncation has not occurred.) It's also possible to override the current value of the variable (before you run the statement containing GROUP_CONCAT, with a separate SET statement. Something like this:
SET SESSION group_concat_max_len = 131072 ;
(Just be careful not to exceed max_allowed_packet.)
I have a one-to-many relationship of rooms and their occupants:
Room | User
1 | 1
1 | 2
1 | 4
2 | 1
2 | 2
2 | 3
2 | 5
3 | 1
3 | 3
Given a list of users, e.g. 1, 3, what is the most efficient way to determining which room is completely/perfectly filled by them? So in this case, it should return room 3 because, although they are both in room 2, room 2 has other occupants as well, which is not a "perfect" fit.
I can think of several solutions to this, but am not sure about the efficiency. For example, I can do a group concatenate on the user (ordered ascending) grouping by room, which will give me comma separated strings such as "1,2,4", "1,2,3,5" and "1,3". I can then order my input list ascending and look for a perfect match to "1,3".
Or I can do a count of the total number of users in a room AND containing both users 1 and 3. I will then select the room which has the count of users equal to two.
Note I want to most efficient way, or at least a way that scales up to millions of users and rooms. Each room will have around 25 users. Another thing I want to consider is how to pass this list to the database. Should I construct a query by concatenating AND userid = 1 AND userid = 3 AND userid = 5 and so on? Or is there a way to pass the values as an array into a stored procedure?
Any help would be appreciated.
For example, I can do a group concatenate on the user (ordered ascending) grouping by room, which will give me comma separated strings such as "1,2,4", "1,2,3,5" and "1,3". I can then order my input list ascending and look for a perfect match to "1,3".
First, a word of advice, to improve your level of function as a developer. Stop thinking of the data, and of the solution, in terms of CSVs. It limits you to thinking in spreadsheet terms, and prevents you from thinking in Relational Data terms. You do not need to construct strings, and then match strings, when the data is in the database, you can match it there.
Solution
Now then, in Relational data terms, what exactly do you want ? You want the rooms where the count of users that match your argument user list is highest. Is that correct ? If so, the code is simple.
You haven't given the tables. I will assume room, user, room_user, with deadly ids on the first two, and a composite key on the third. I can give you the SQL solution, you will have to work out how to do it in the non-SQL.
Another thing I want to consider is how to pass this list to the database. Should I construct a query by concatenating AND userid = 1 AND userid = 3 AND userid = 5 and so on? Or is there a way to pass the values as an array into a stored procedure?
To pass the list to the stored proc, because it needs a single calling parm, the length of which is variable, you have to create a CSV list of users. Let's call that parm #user_list. (Note, that is not contemplating the data, that is passing a list to a proc in a single parm, because you can't pass an unknown number of identified users to a proc otherwise.)
Since you constructed the #user_list on the client, you may as well compute #user_count (the number of members in the list) while you are at it, on the client, and pass that to the proc.
Something like:
CREATE PROC room_user_match_sp (
#user_list CHAR(255),
#user_count INT
...
)
AS
-- validate parms, etc
...
SELECT room_id,
match_count,
match_count / #user_count * 100 AS match_pct
FROM (
SELECT room_id,
COUNT(user_id) AS match_count -- no of users matched
FROM room_user
WHERE user_id IN ( #user_list )
GROUP BY room_id -- get one row per room
) AS match_room -- has any matched users
WHERE match_count = MAX( match_count ) -- remove this while testing
It is not clear, if you want full matches only. In that case, use:
WHERE match_count = #user_count
Expectation
You have asked for a proc-based solution, so I have given that. Yes, it is the fastest. But keep in mind that for this kind of requirement and solution, you could construct the SQL string on the client, and execute it on the "server" in the usual manner, without using a proc. The proc is faster here only because the code is compiled and that step is removed, as opposed to that step being performed every time the client calls the "server" with the SQL string.
The point I am making here is, with the data in a reasonably Relational form, you can obtain the result you are seeking using a single SELECT statement, you don't have to mess around with work tables or temp tables or intermediate steps, which requires a proc. Here, the proc is not required, you are implementing a proc for performance reasons.
I make this point because it is clear from your question that your expectation of the solution is "gee, I can't get the result directly, I have work with the data first, I am ready and willing to do that". Such intermediate work steps are required only when the data is not Relational.
Maybe not the most efficient SQL, but something like:
SELECT x.room_id,
SUM(x.occupants) AS occupants,
SUM(x.selectees) AS selectees,
SUM(x.selectees) / SUM(x.occupants) as percentage
FROM ( SELECT room_id,
COUNT(user_id) AS occupants,
NULL AS selectees
FROM Rooms
GROUP BY room_id
UNION
SELECT room_id,
NULL AS occupants,
COUNT(user_id) AS selectees
FROM Rooms
WHERE user_id IN (1,3)
GROUP BY room_id
) x
GROUP BY x.room_id
ORDER BY percentage DESC
will give you a list of rooms ordered by the "best fit" percentage
ie. it works out a percentage of fulfilment based on the number of people in the room, and the number of people from your set who are in the room
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
}
I have a table which contains due dates for individual member records. Each row contains four fields:
ID | Next_Due | Next_Due_Missed | Amount
=============================================================
123 | 2010-12-05 | NULL | 41.32
456 | 2010-12-10 | 2010-12-05 | 21.44
789 | 2010-12-20 | 2010-12-10 | 39.99
ID is the unique id of each MEMBER
Next Due - is the next due day of their regular subscription period
Next_Due_Missed is populated ONLY if there was an error collecting the first round of subscription payment.
Amount is amount owned for subscription.
My goal is to create a sql query that checks if next_due_missed exists and is not null. If it does, use that value as the '$date'. If not, set $date = value of next_due
this is done easily enough except my results are grouped by Next_Due in normal circumstances and will omit next_due_missed if I combine the way I currently am.
Every payment period, there may be 600+ records with next_due equal to the desired date (and 10-15 equal to next_due_missed).
My current query is:
$stmt = $db->prepare("SELECT next_due, next_due_missed FROM table_name WHERE (next_due > CURDATE() OR next_due_missed > CURDATE()) GROUP BY next_due ASC");
This only returns results for next_due however. Omitting the GROUP BY clause returns hundreds of results (while I need to group in this stage).
Similarly at a later point, I will need to break out those individual records and actually create payment records based on the 'next_due' and 'next_due_missed' values.
Any ideas what I am missing?
I am not sure the purpose of your GROUP BY other than to get DISTINCT values, but left it in in case you provided a partial query:
SELECT coalesce(next_due_missed, next_due) as EffectiveNextDue
FROM table_name
WHERE coalesce(next_due_missed, next_due) > CURDATE()
GROUP BY coalesce(next_due_missed, next_due)
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.