mysql - search between dates where all dates appear - php

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.

Related

SELECT DISTINCT with other colum

I got a question.
I have following mysql table:
domain | visited
domain1.net | 21.02.2019 - 18:55
domain1.net | 20.02.2019 - 14:01
domain1.net | 22.02.2019 - 12:05
domain2.net | 24.02.2019 - 19:01
domain2.net | 22.02.2019 - 17:21
I want following result:
domain1.net | 22.02.2019 - 12:05
domain2.net | 24.02.2019 - 19:01
No duplicate domain and the latest visited.
My code:
<?php
$sql = "SELECT DISTINCT p_domain FROM table_content";
$result = $db->prepare($sql);
$result->execute();
while ($row = $result->fetch()) {
$domain = $row['domain'];
echo "<tr>"
. "<td>$domain</td>"
. "</tr>";
}
?>
You can use your domain DISTINCT with your column, to get the latest visited domain you can use Order by
SELECT DISTINCT domain FROM table_content Order By visited DESC
ORDER BY: Order by clause is used to sort the data in ascending or descending order.
DISTINCT: The SELECT DISTINCT statement is used to retrieve only distinct (different) values from the database.
You use GROUP BY:
SELECT p_domain, MAX(visited) as most_recent_visit
FROM table_content
GROUP BY p_domain
Group by establishes a list of columns that you want the unique combinations of. Other columns in your select that are not in the group by list do not form a unique grouping key and MUST be expressed as some form of aggregation:
SELECT ethnicity, city, AVG(age), COUNT(*), SUM(number_of_cars_owned), MIN(birthdate), MAX(number_of_children)
FROM person_table
GROUP BY ethnicity, city
This gets the average age, total count, the total of cars owned, the oldest person, the largest family per ethnic group per city
It is important to note that the values for the aggregates come from different rows (in the Chicago Caucasian category, the Smith family might have the most children but the Doe family has the oldest person), and might not be present in the rows at all (the average age might be 27.32615).
A common query is like "I want to find the earliest date someone visited each of my sites and what their ip address was" - that is a site, min(visitdate) group by site but you can't just bang the IP address in there too, either as a grouped thing site, ipaddress min(visitdate) group by site, ipaddress because that is "the first visit per site per ip" and you can't whack the ip into an aggregating function site, min(visitdate), min(ipaddress) group by site because the min ip may come from a different row to the min date.
If you want the site, the first visit date and the ip of that visit you have to run the aggregate as a subquery and join it back to the main table to get the other data from the row with the min date
select *
from
(select site, min(visitdate) min_d from t group by site) x
inner join t on t.site = x.site and t.visitdate = x.min_d
ps: the comment makes a really good point; if your visited date isn't a date but instead a string-that-looks-like-a-date-to-you then getting the MAX() of it will not necessarily return the most recent date, it will return the alphabetically latest date. You should definitely store dates using the proper date/time data types so that functions like MAX can work properly. If you haven't done this please change things so it is done like this.. because writing a query that parses millions of strings into dates every time it is run, just so that max will work properly, is a huge waste of resources.

How can I store dates/attendance?

I'm looking for for an opinion.
I have a list of people and will need to store when they are present at a location so those in charge can check them off a list. I'm not 100% sure how long the dates will be needed but I'm assuming they may need to look at previous attendance lists.
My first instinct is to have a column for each date but that could result in many many columns. I could just store a list of dates next to each person:
"01/01/2012,01/15/2012,02/18/2012..."
that could result in a very long entry. It seems like neither is a good option.
If anyone has a suggestion or guidance on an approach please let me know. Thanks.
A complex, but also very clean approach would be
Table "persons":
id
name
Table "dates":
id
location
date
... whatever info the "dates" table needs
Table "attendances":
date_id (link to an entry in the "dates" table)
person_id (link to an entry in the "persons" table)
attended (yes/no)
Then fill the database with the appropriate dates, and fill the "attendances" table according to which persons need to be present at each date.
This is, as said, complex to implement, but it's incredibly flexible - you can have any number of dates and attendees; you can excuse people from attending a specific date programmatically; you can add people to groups...
Link tables.
One table of people
ID
Name
One table of classes
ID
Name
One table linking person to class to date.
ID
personID
classID
cDate
So all you would need to do to determine which students were preset on a certain date in a certain class:
SELECT *
FROM people p
LEFT JOIN peopletoclass ptc ON p.id = ptc.personid
LEFT join class c ON c.id = ptc.classid
WHERE ptc.cDate = '2011-11-07' AND c.id = '1';
Above (for example) would get all people in class id 1 on November 7th 2011.
Create a table "attendance" consisting of a person_id field and a date_present field. You can't store this into columns or a long list using a string ;-).
Than you can use queries where you join the table Person with Attendance.
Your first instinct would result in a horrible table design. What you should have is a seperate table that stores the users/locations/dates tuples
e.g.
userID locationID date
1 party 1/1/2011 00:00:00
1 bathroom 1/1/2011 00:05:00
1 party 1/1/2011 00:15:00
would show that user #1 was at a New Year's Eve party, then went to pray before the porcelain altar at 12:05am, then returned to the party 10 minutes later.

Trouble combining Two sql queries into one

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)

Select Mulitple Records based on One record's column value

I have a table which contains related records (multiple revisions of the same record). Each record has a string field that resembles a date (date, time, and microtime). I want to select all records that are older than a specific date. If a record has a related record newer than the specific date, I do not want to select any of those related records. Any ideas for that select statement? Eventually, it will be a REMOVE statement.
Edit: Some Sample Rows
id shared_id date type other_data...
1 2 2010-01-01 01:02:03.1234567 original ...
2 3 2010-01-15 11:12:03.1234733 original ...
3 2 2010-02-01 03:04:04.5465654 amendment ...
If my cut-off date was "2010-01-31", I would want to select id #2 only because id #1 has an amendment newer than the cut-off date.
I found this link helping me generate the select statement.
SELECT DISTINCT T.shared_id,T.date,T.id
FROM table T WHERE T.date = (
SELECT MAX( date ) FROM table WHERE shared_id = T.shared_id )
AND T.date < 'my_cut_off_date_string'
This seems to work for me. Thanks for everyone's help.
Maybe you can try the DATEDIFF() function, check this out:
Link 1
Or this one: Link 2
Or maybe you can try the classic query (SELECT FROM table WHERE data <= anotherdata), but in this case you need to convert both data in timestamp format
DELETE from tablename where relatedField = 'relatedValue' AND dateField <= dateToDeleteFrom
Something along those lines should do what you need it to do, if I understand your scenario. If you provide a sample data set I can adjust the statement to more accurately represent your need, but I think this is a good starting point.
HTH,
Jc

electronic leave application database design

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;

Categories