Getting all entries whose birthday is today in PostgreSQL - php

I have the following query and I need to implement a Mailer that needs to be send out to all clients who's Birthday is today. This happens on a daily manner. Now what I need to achieve is only to select the Birthday clients using a Postgres SQL query instead of filtering them in PHP.
The date format stored in the database is YYYY-MM-DD eg. 1984-03-13
What I have is the following query
SELECT cd.firstname,
cd.surname,
SUBSTRING(cd.birthdate,6),
cd.email
FROM client_contacts AS cd
JOIN clients AS c ON c.id = cd.client_id
WHERE SUBSTRING(birthdate,6) = '07-20';
Are there better ways to do this query than the one I did above?

You could set your where clause to:
WHERE
DATE_PART('day', birthdate) = date_part('day', CURRENT_DATE)
AND
DATE_PART('month', birthdate) = date_part('month', CURRENT_DATE)

In case it matters, the age function will let you work around the issue of leap years:
where age(cd.birthdate) - (extract(year from age(cd.birthdate)) || ' years')::interval = '0'::interval
It case you want performance, you can actually wrap the above with an arbitrary starting point (e.g. 'epoch'::date) into a function, too, and use an index on it:
create or replace function day_of_birth(date)
returns interval
as $$
select age($1, 'epoch'::date)
- (extract(year from age($1, 'epoch'::date)) || ' years')::interval;
$$ language sql immutable strict;
create index on client_contacts(day_of_birth(birthdate));
...
where day_of_birth(cd.birthdate) = day_of_birth(current_date);
(Note that it's not technically immutable, since dates depend on the timezone. But the immutable part is needed to create the index, and it's safe if you're not changing the time zone all over the place.)
EDIT: I've just tested the above a bit, and the index suggestion actually doesn't work for feb-29th. Feb-29th yields a day_of_birth of 1 mon 28 days which, while correct, needs to be added to Jan-1st in order to yield a valid birthdate for the current year.
create or replace function birthdate(date)
returns date
as $$
select (date_trunc('year', now()::date)
+ age($1, 'epoch'::date)
- (extract(year from age($1, 'epoch'::date)) || ' years')::interval
)::date;
$$ language sql stable strict;
with dates as (
select d
from unnest('{
2004-02-28,2004-02-29,2004-03-01,
2005-02-28,2005-03-01
}'::date[]) d
)
select d,
day_of_birth(d),
birthdate(d)
from dates;
d | day_of_birth | birthdate
------------+---------------+------------
2004-02-28 | 1 mon 27 days | 2011-02-28
2004-02-29 | 1 mon 28 days | 2011-03-01
2004-03-01 | 2 mons | 2011-03-01
2005-02-28 | 1 mon 27 days | 2011-02-28
2005-03-01 | 2 mons | 2011-03-01
(5 rows)
And thus:
where birthdate(cd.birthdate) = current_date

The #Jordan answer is correct but, it wont work if your date format is string. If it is string you have type cast it using to_date function. then apply the date_part function.
If date of birth (DOB) is 20/04/1982 then the query is:
SELECT * FROM public."studentData" where date_part('day',TO_DATE("DOB", 'DD/MM/YYYY'))='20'
AND date_part('month',TO_DATE("DOB", 'DD/MM/YYYY'))='04';
or
EXTRACT(MONTH FROM TO_DATE("DOB", 'DD/MM/YYYY'))='04' AND EXTRACT(DAY FROM TO_DATE("DOB", 'DD/MM/YYYY'))='20'
I add double quotes to table name("studentData") and field name ("DOB") because it was string.
Credit to #Jordan

WHERE date_part('month', cd.birthdate) = '07' AND date_part('day', cd.birthdate) = '20'
you can read more about this here

Try with something like:
WHERE EXTRACT(DOY FROM TIMESTAMP cd.birthdate) = EXTRACT(DOY FROM TIMESTAMP CURRENT_TIMESTAMP)

The best way IMO is to use to_char(birthday, 'MM-DD') in (?) where you just give some date range mapped to 'MM-DD' in place of ?. Unless you have to support very big date ranges this solution is very simple, clean and bug resistant.

What you are trying to do is, extract the person detail who would be wished using SQL manually, and send the wish separately manually. What if I suggest you a better approach?
Extract the wish details as excel and let wishing app take care of everything.
At minimal it just need two things excel file with wish details (Date, name, email) and a configuration file (application.properties) and that is it, you are good to go.
Further there various options to run the application locally (Command line, foreground, background, docker, windows scheduler, unix cron etc) Cloud.
Application is highly configurable , you can configure various details like:
Workbook loading options
Image options to send with wishes.
SMTP Configurations
Other application level configurations like, when to send wish, belated wish, logging etc.
Disclaimer : I am the owner of the application

Related

Grouping shifts in MySQL table by week starting with sunday using PHP & MySQL

I'm writing a script using PHP & MySQL where I can record the shifts I work (HGV driver).
Upon posting the form data PHP calculates shift duration, wages accumulated, overtime, distance driven, etc, and stores it in the MySQL database.
I want to then display all shifts in a table but group them by my pay week which unfortunately starts on a Sunday.
If the pay week was Mon-Sun I wouldn't have this problem as I could use week numbers but I can't due to the week starting on a Sunday.
My code is as follows:
^^^^^^^^^^^^^^^^^^^
// DB Connection //
// Return the earliest shift in the database //
$result = $db->query("SELECT * FROM `shifts` ORDER BY `shift_start` ASC LIMIT 1");
$data = $result->fetch_assoc();
// Establish the previous Sunday //
$week_from = strtotime(date('Y-m-d',mktime(0,0,0,date('m',$data['shift_start']),date('d',$data['shift_start']),date('y',$data['shift_start']))) . 'last sunday');
// PHP Loop Goes Here //
Firstly, is the above code the most efficient way of getting the start date (previous Sunday)?
Secondly, what's the best way to loop through the weeks where there are shifts?
TIA
This is a two part question, so I will try to cover them separately.
Regarding your first question, I would suggest using the MIN() function when selecting the smallest or earliest value in a database, and ensuring you have an index on the "shift_start" column. More information on the difference between MIN() and ORDER BY/LIMIT can be found here.
Then your query would look a something like this:
SELECT MIN(`shift_start`) FROM `shifts`;
Personally, I also find MIN() far more readable.
Now, for the other (and far more complicated) question:
You've not provided much detail on what your database (or the contents) looks like. Since you're using the PHP date function, I am assuming you're saving the timestamps as UNIX instead of MySQL TIMESTAMP/DATETIME types.
Firstly, I would suggest you migrate to using a TIMESTAMP/DATETIME column type. It'll simplify the query you're attempting to run.
If you're unable to change to a TIMESTAMP/DATETIME column, then you can convert a UNIX timestamp to a DATETIME.
MySQL has a YEARWEEK() function that you can use to group by:
SELECT STR_TO_DATE(CONCAT(YEARWEEK(`shift_start`), ' Monday'), '%X%V %W') AS `date`, SUM(`wage`) AS `wage` FROM `shifts` GROUP BY YEARWEEK(`shift_start`);
This will output something similar to:
+------------+------+
| Date | Wage |
+------------+------+
| 2021-11-29 | 50 |
| 2021-12-06 | 15 |
+------------+------+

Daily post count out of a timestamp column

My application is built using Laravel 4.1 and supports pretty much all databases that Laravel officially does today. I have an INT(10) column that stores the Unix timestamp for each "post" to indicate the create time of the post.
Is there a way I can get the daily number of posts out of this sort of setup? Basically, I want to make a graph out of Google Chart API to indicate the trend of number of posts made per day for the last 1 month.
Also, I am not looking for a solution around DB::raw(), I'd prefer using the query builder.
If you want to perform an aggregation on the db-side IMHO you'll have to use DB::raw() because you need to manipulate your timestamp values.
If understand correctly your major concern is not in using DB::raw() method but rather not using vendor specific datetime functions (like FROM_UNIXTIME() in MySQL).
You can issue a query like this to get aggregated results in an almost db agnostic way (AFAIK all major DBMSes have implementation for FLOOR() function)
SELECT FLOOR(timestamp / 86400) AS date , COUNT(*) AS total
FROM post
GROUP BY FLOOR(timestamp / 86400)
which translates into something like this with Query Builder
$result = DB::table('posts')
->select(DB::raw('FLOOR(timestamp / 86400) AS date, COUNT(*) AS total'))
->groupBy(DB::raw('FLOOR(timestamp / 86400) AS date, COUNT(*) AS total'))
->get();
Sample output:
| DATE | TOTAL |
|-------|-------|
| 16081 | 2 |
| 16082 | 3 |
Here is SQLFiddle demo
And then in the client code convert date column value into a human readable form by
date('m/d/Y', 16081 * 86400)
while you iterate over the resultset.
Firstly, you should probably using Laravel's built in timestamps for created_at and updated_at columns. If you don't want to use Laravel's built in timestamps, you should at least be setting your timestamp columns to the timestamp datatype rather than int.
With that said, the raw SQL you'd probably want to use for something like this would look a bit like;
SELECT DATE(timestamp_column_name) as day, count(*) as post_count from posts group by day order by day asc;
There's no real, pure Eloquent way of doing this. Something like the following is going to be as close as you'll get.
Post::groupBy('day')->get([
DB::raw('DATE(FROM_UNIXTIME(timestamp_column_name)) as day'),
DB::raw('count(*) as post_count')
]);
This will return two columns, day being the date in YYYY-MM-DD format, and post_count being an integer with the count.
===========
In the above example, I am using the MySQL DATE function. You can of course manipulate your timestamp column however you want however you want. This just seemed to make the most sense to me.

how to solve time issue in where clause

My 'user` table looks likes following..
id | name | time
1 | a1 | 2012-11-15 06:20:28
2 | a2 | 2012-11-14 06:20:28
3 | a3 | 2012-11-13 06:20:28
Now my problem is , how can I get the user detail whose time is in between present or 30
second less than present time..
I tried..
"SELECT *
FROM user
WHERE strtotime(user_session.session_timestamp)-30 < ".time().")";
its not working... It will throw syntax error
please help me to solve the issue
No need for PHP. You can use the MySQL Date and Time functions.
SELECT * FROM user WHERE time BETWEEN (NOW() - INTERVAL 30 SECOND) AND NOW();
Note: This query is not optimized as the date function prevent the query from being cached. If this is a large concern, then using the PHP equivalent may be more performant.
strtotime() is a PHP function, not MySQL.
You can't do it that way. You need to do two different SELECT statements. One to collect the user_session.session_timestamp. Call the PHP strtotime() function -30 then use that variable in this current SELECT statement. Depending on how you have this setup you may be using a foreach( $array as $key => $value) to step through and run as many selects as you need to get individual returns. The many problem is the PHP function being used in a SQL statement.
Or do what Jason said! Pure SQL is always better, but I assumed you need or wanted to use PHP for some reason, there are some times important reasons for needing access to the data in PHP.

How to Trigger total number of days in mySQL

"I need to create a field that will show the number of days between a date and the present.
TABLE: reg_add
+------------+---------+
| Name | Type |
+------------+---------+
| hm_date | date |
+------------+---------+
| total_days | date |
+------------+---------+
My client will type in the following date for example in "hm_date": May 1, 2012.
I need "total_days" to show the total days between May 1, 2012 at the current date.
I want to achieve this on server-side, which is somewhat new to me.
I need to create a TRIGGER to always have "total_days" updated
I started with this and I'm having trouble making a trigger and getting it correct:
SELECT DATEDIFF(curdate(),hm_date) as total_days FROM reg_add
Any help would be appreciated.
Erik
You can calculate it on the fly very easy, using TIMESTAMPDIFF function -
SELECT TIMESTAMPDIFF(DAY, hm_date, NOW()) FROM reg_add;
I took a look into the MySQL Trigger Docs and from the looks of it, you can only create trigger for event types Insert, Update, and Delete. So your Trigger won't actually update your total_days field (which should be int) as you want it. [It sounds like you want it to update your field on a time basis (aka every x hours update)].
Here is the docs: http://dev.mysql.com/doc/refman/5.0/en/create-trigger.html
I would suggest writing a cron job in php that runs once (or multiple times) per day that will update the field for you.
In your cron you should just have to run one sql statement: (This should go through the whole table and update the total_days field for each row)
UPDATE reg_add SET total_days = DATEDIFF(curdate(),hm_date)
Erik, I am using tsql, and not as familiar with mySQL, but in tsql the DATEDIFF function requires 3 parameters. The sytax is
DATEDIFF(date part, date1, date2).
I would try DATEDIFF(d,GETDATE(),hm_date) AS total_days FROM reg_add
Instead of triggers, you could use a View:
CREATE VIEW reg_add_with_total_days_VIEW
AS
SELECT hm_date
, DATEDIFF( CURDATE(), hm_date ) AS total_days
FROM reg_add ;
Then you can use the view, every time you need the total days - which will be calculated on the fly:
SELECT hm_date, total_days
FROM reg_add_with_total_days_VIEW
You can see it working in SQL-Fiddle

PHP/MYSQL datetime ranges overlapping for users

please I need help with this (for better understanding please see attached image) because I am completely helpless.
As you can see I have users and they store their starting and ending datetimes in my DB as YYYY-mm-dd H:i:s. Now I need to find out overlaps for all users according to the most frequent time range overlaps (for most users). I would like to get 3 most frequented datatime overlaps for most users. How can I do it?
I have no idea which mysql query should I use or maybe it would be better to select all datetimes (start and end) from database and process it in php (but how?). As stated on image results should be for example time 8.30 - 10.00 is result for users A+B+C+D.
Table structure:
UserID | Start datetime | End datetime
--------------------------------------
A | 2012-04-03 4:00:00 | 2012-04-03 10:00:00
A | 2012-04-03 16:00:00 | 2012-04-03 20:00:00
B | 2012-04-03 8:30:00 | 2012-04-03 14:00:00
B | 2012-04-06 21:30:00 | 2012-04-06 23:00:00
C | 2012-04-03 12:00:00 | 2012-04-03 13:00:00
D | 2012-04-01 01:00:01 | 2012-04-05 12:00:59
E | 2012-04-03 8:30:00 | 2012-04-03 11:00:00
E | 2012-04-03 21:00:00 | 2012-04-03 23:00:00
What you effectively have is a collection of sets and want to determine if any of them have non-zero intersections. This is the exact question one asks when trying to find all the ancestors of a node in a nested set.
We can prove that for every overlap, at least one time window will have a start time that falls within all other overlapping time windows. Using this tidbit, we don't need to actually construct artificial timeslots in the day. Simply take a start time and see if it intersects any of the other time windows and then just count up the number of intersections.
So what's the query?
/*SELECT*/
SELECT DISTINCT
MAX(overlapping_windows.start_time) AS overlap_start_time,
MIN(overlapping_windows.end_time) AS overlap_end_time ,
(COUNT(overlapping_windows.id) - 1) AS num_overlaps
FROM user_times AS windows
INNER JOIN user_times AS overlapping_windows
ON windows.start_time BETWEEN overlapping_windows.start_time AND overlapping_windows.end_time
GROUP BY windows.id
ORDER BY num_overlaps DESC;
Depending on your table size and how often you plan on running this query, it might be worthwhile to drop a spatial index on it (see below).
UPDATE
If your running this query often, you'll need to use a spatial index. Because of range based traversal (ie. does start_time fall in between the range of start/end), a BTREE index will not do anything for you. IT HAS TO BE SPATIAL.
ALTER TABLE user_times ADD COLUMN time_windows GEOMETRY NOT NULL DEFAULT 0;
UPDATE user_times SET time_windows = GeomFromText(CONCAT('LineString( -1 ', start_time, ', 1 ', end_time, ')'));
CREATE SPATIAL INDEX time_window ON user_times (time_window);
Then you can update the ON clause in the above query to read
ON MBRWithin( Point(0,windows.start_time), overlapping_windows.time_window )
This will get you an indexed traversal for the query. Again only do this if your planning on running the query often.
Credit for the spatial index to Quassoni's blog.
Something like this should get you started -
SELECT slots.time_slot, COUNT(*) AS num_users, GROUP_CONCAT(DISTINCT user_bookings.user_id ORDER BY user_bookings.user_id) AS user_list
FROM (
SELECT CURRENT_DATE + INTERVAL ((id-1)*30) MINUTE AS time_slot
FROM dummy
WHERE id BETWEEN 1 AND 48
) AS slots
LEFT JOIN user_bookings
ON slots.time_slot BETWEEN `user_bookings`.`start` AND `user_bookings`.`end`
GROUP BY slots.time_slot
ORDER BY num_users DESC
The idea is to create a derived table that consists of time slots for the day. In this example I have used dummy (which can be any table with an AI id that is contiguous for the required set) to create a list of timeslots by adding 30mins incrementally. The result of this is then joined to bookings to be able to count the number of books for each time slot.
UPDATE For entire date/time range you could use a query like this to get the other data required -
SELECT MIN(`start`) AS `min_start`, MAX(`end`) AS `max_end`, DATEDIFF(MAX(`end`), MIN(`start`)) + 1 AS `num_days`
FROM user_bookings
These values can then be substituted into the original query or the two can be combined -
SELECT slots.time_slot, COUNT(*) AS num_users, GROUP_CONCAT(DISTINCT user_bookings.user_id ORDER BY user_bookings.user_id) AS user_list
FROM (
SELECT DATE(tmp.min_start) + INTERVAL ((id-1)*30) MINUTE AS time_slot
FROM dummy
INNER JOIN (
SELECT MIN(`start`) AS `min_start`, MAX(`end`) AS `max_end`, DATEDIFF(MAX(`end`), MIN(`start`)) + 1 AS `num_days`
FROM user_bookings
) AS tmp
WHERE dummy.id BETWEEN 1 AND (48 * tmp.num_days)
) AS slots
LEFT JOIN user_bookings
ON slots.time_slot BETWEEN `user_bookings`.`start` AND `user_bookings`.`end`
GROUP BY slots.time_slot
ORDER BY num_users DESC
EDIT I have added DISTINCT and ORDER BY clauses in the GROUP_CONCAT() in response to your last query.
Please note that you will will need a much greater range of ids in the dummy table. I have not tested this query so it may have syntax errors.
I would not do much in SQL, this is so much simpler in a programming language, SQL is not made for something like this.
Of course, it's just sensible to break the day down into "timeslots" - this is statistics. But as soon as you start handling dates over the 00:00 border, things start to get icky when you use joins and inner selects. Especially with MySQL which does not quite like inner selects.
Here's a possible SQL query
SELECT count(*) FROM `times`
WHERE
( DATEDIFF(`Start`,`End`) = 0 AND
TIME(`Start`) < TIME('$SLOT_HIGH') AND
TIME(`End`) > TIME('$SLOT_LOW'))
OR
( DATEDIFF(`Start`,`End`) > 0 AND
TIME(`Start`) < TIME('$SLOT_HIGH') OR
TIME(`End`) > TIME('$SLOT_LOW')
Here's some pseudo code
granularity = 30*60; // 30 minutes
numslots = 24*60*60 / granularity;
stats = CreateArray(numslots);
for i=0, i < numslots, i++ do
stats[i] = GetCountFromSQL(i*granularity, (i+1)*granularity); // low, high
end
Yes, that makes numslots queries, but no joins no nothing, hence it should be quite fast. Also you can easily change the resolution.
And another positive thing is, you could "ask yourself", "I have two possible timeslots, and I need the one where more people are here, which one should I use?" and just run the query twice with respective ranges and you are not stuck with predefined time slots.
To only find full overlaps (an entry only counts if it covers the full slot) you have to switch low and high ranges in the query.
You might have noticed that I do not add times between entries that could span multiple days, however, adding a whole day, will just increase all slots by one, making that quite useless.
You could however add them by selecting sum(DAY(End) - DAY(Start)) and just add the return value to all slots.
Table seems pretty simple. I would keep your SQL query pretty simple:
SELECT * FROM tablename
Then when you have the info saved in your PHP object. Do the processing with PHP using loops and comparisons.
In simplest form:
for($x, $numrows = mysql_num_rows($query); $x < $numrows; $x++){
/*Grab a row*/
$row = mysql_fetch_assoc($query);
/*store userID, START, END*/
$userID = $row['userID'];
$start = $row['START'];
$end = $row['END'];
/*Have an array for each user in which you store start and end times*/
if(!strcmp($userID, "A")
{
/*Store info in array_a*/
}
else if(!strcmp($userID, "B")
{
/*etc......*/
}
}
/*Now you have an array for each user with their start/stop times*/
/*Do your loops and comparisons to find common time slots. */
/*Also, use strtotime() to switch date/time entries into comparable values*/
Of course this is in very basic form. You'll probably want to do one loop through the array to first get all of the userIDs before you compare them in the loop shown above.

Categories