I am new to SQL and learning at the moment. I am using MySQL installed on Ubuntu 18.04.
I have the following table:
CREATE TABLE IF NOT EXISTS product(name varchar(255) NOT NULL,
availability date NOT NULL);
I want to insert the following record in the table:
INSERT INTO `product` (`name`, `availability`)
VALUES ('Title 1', last Wednesday);
when executing I get the following error:
ERROR 1064 (42000) at line 21: You have an error in your SQL syntax;
check the manual that corresponds to your MySQL server version for the
right syntax to use near 'Wednesday)'
I understand that this format ('last Wednesday', 'next week',..) is not a standard date format. I wanted to know if it is possible to create user-defined types in MySQL to be able to process these bespoke date records.
So far what I have found on the web deals with dates that still contain more or less a standard date format, not as bespoke as those mentioned above (use cast, parse,..).
What you have are functions like NOW(), CURRENT_TIMESTAMP() and CURRENT_DATE(). For anything else like LAST_WEDNESDAY() you can write your own stored functions.
create function last_wednesday() returns date no sql
return current_date() - interval (weekday(current_date()) + 4)%7+1 day;
Or use the same expression inline in your queries.
Update
As asked by Strawberry - Here is something "more scalable":
create function human_to_date(str text, date date) returns date no sql
return case
when str = 'last monday' then date - interval (weekday(date) + 6-0)%7+1 day
when str = 'last tuesday' then date - interval (weekday(date) + 6-1)%7+1 day
when str = 'last wednesday' then date - interval (weekday(date) + 6-2)%7+1 day
when str = 'last thursday' then date - interval (weekday(date) + 6-3)%7+1 day
when str = 'last friday' then date - interval (weekday(date) + 6-4)%7+1 day
when str = 'last saturday' then date - interval (weekday(date) + 6-5)%7+1 day
when str = 'last sunday' then date - interval (weekday(date) + 6-6)%7+1 day
end
;
Use it as
select human_to_date('last wednesday', now())
or for any date as reference
select human_to_date('last sunday', '2019-10-01')
This will return the last sunday in this month (sept. 2019)
See demo
I've tried to remove code duplication, but ended with this:
delimiter //
create function human_to_date(str text, date date) returns date no sql
begin
declare day_of_week int default null;
if str rlike '^last (monday|tuesday|wednesday|thursday|friday|saturday|sunday)$' then
set day_of_week = case substring_index(str, ' ', -1)
when 'monday' then 0
when 'tuesday' then 1
when 'wednesday' then 2
when 'thursday' then 3
when 'friday' then 4
when 'saturday' then 5
when 'sunday' then 6
end;
return date - interval (weekday(date) + 6-day_of_week)%7+1 day;
end if;
return null;
end //
delimiter ;
db-fiddle
When it comes to dates, the natural language capabilities of some application languages is so good that I'd be tempted to handle the logic there instead.
For instance, here's some PHP:
<?php
echo(date(DATE_RFC850,strtotime( date('Y-m-01', strtotime('next month')).' last wednesday')));
?>
Today is Thursday 26th September. This echoes Wednesday, 25-Sep-19 00:00:00 BST
When storing in a db, you need information that make sense. a value "last Wednesday" for a product, does not give any reasonable availability info. For example, last Wednesday from which date? If someone asks for all available products on last Wednesday but 2 months later than today, what will a query looking for "last Wednesday" return? Definitely invalid results. So, if you want to store a date, then use a date/datetime datatype. You can add any custom description you want by using an additional column with varchar datatype, but i wouldn't use this as a primary info source.
Here is code to insert last Wednesday's date into your table:
INSERT INTO product(name, availability)
SELECT
'Title 1',
CASE WHEN WEEKDAY(CURDATE()) >= 2
THEN TIMESTAMPADD(DAY, 2 - WEEKDAY(CURDATE()), CURDATE())
ELSE TIMESTAMPADD(DAY, 2 - WEEKDAY(CURDATE()) - 7, CURDATE()) END;
Notes: It would be hard to pull this off using a select with a VALUES clause, unless perhaps we define a UDF, so I am using an INSERT INTO ... SELECT. The CASE expression checks if the current date is a Wednesday (2) through Sunday (6). If so, then we offset the current date by the difference, to shift it back to Wednesday. Similar logic applies for Monday (0) and Tuesday (1).
Related
I've managed to select the data from the current week but the week itself starts from Sunday which is not the right format for me, it should starts from Monday. I'm using MySQL to query the data.
SELECT IFNULL(SUM(rendeles_dbszam),0) as eladott_pizzak_szama
FROM rendeles
WHERE WEEK(rendeles_idopont) = WEEK(CURRENT_DATE())
'Week' in mysql has 2 inputs: date and week type. By default it's equal 0. That means week starts from sunday. Try this code:
SELECT IFNULL(SUM(rendeles_dbszam),0) as eladott_pizzak_szama FROM rendeles WHERE WEEK(rendeles_idopont) = WEEK(CURRENT_DATE(),1)
You can use this little formula to get the Monday starting the week of any given DATE, DATETIME, or TIMESTAMP object.
FROM_DAYS(TO_DAYS(datestamp) -MOD(TO_DAYS(datestamp) -2, 7))
I like to use it in a stored function named TRUNC_MONDAY(datestamp) defined like this.
DELIMITER $$
DROP FUNCTION IF EXISTS TRUNC_MONDAY$$
CREATE
FUNCTION TRUNC_MONDAY(datestamp DATETIME)
RETURNS DATE DETERMINISTIC NO SQL
COMMENT 'preceding Monday'
RETURN FROM_DAYS(TO_DAYS(datestamp) -MOD(TO_DAYS(datestamp) -2, 7))$$
DELIMITER ;
Then you can do stuff like this
SELECT IFNULL(SUM(rendeles_dbszam),0) as eladott_pizzak_szama
FROM rendeles
WHERE TRUNC_MONDAY(rendeles_idopont) = TRUNC_MONDAY(CURRENT_DATE())
or even this to get a report covering eight previous weeks and the current week.
SELECT SUM(rendeles_dbszam) as eladott_pizzak_szama,
TRUNC_MONDAY(rendeles_idopont) as week_beginning
FROM rendeles
WHERE rendeles_idopont >= TRUNC_MONDAY(CURDATE()) - INTERVAL 8 WEEK
AND rendeles_idopoint < TRUNC_MONDAY(CURDATE()) + INTERVAL 1 WEEK
GROUP BY TRUNC_MONDAY(rendeles_idopont)
I particularly like this TRUNC_MONDAY() approach because it works unambiguously even for calendar weeks that contain New Years' Days.
(If you want TRUNC_SUNDAY() change the -2 in the formula to -1.)
I have a problem with dates of 29 Feb.
I have a query that receives the year as dynamic parameter from PHP/Laravel. The month and day are not inserted. The query that is sent to the database includes the current year from current date. I want to compare the date, against a specific column in the database, if the date is invalid (ex: 02-29-2019), I want to compare the date against 03-01-2019
->whereRaw("DATE(CONCAT_WS('-',YEAR(?),MONTH(sendDate),DAY(sendDate)))
>= birthdate",[$date])
The query above returns null if the resulting concatenated date is invalid ex: 2019-02-29
My desired PUSEDO Code/function:
if "DATE(CONCAT_WS('-',YEAR(?),MONTH(sendDate),DAY(sendDate) is NULL
and MONTH(sendDate)=03 and DAY(sendDate)=01" THEN
WHERE Birthdate>= DATE(CONCAT_WS('-',YEAR(?),03,01)
ELSE
WHERE Birthdate>= DATE(CONCAT_WS('-',YEAR(?),MONTH(sendDate),DAY(sendDate)
Is there a function in MySQL that can help overcome this problem? The problem with my current query
Below code checks the string in format yyyy-mm-dd.
If it is a a valid date then converts it to date.
Else returns the first day of the next month as date.
Below is the sample code:
DECLARE #T AS VARCHAR(10)
SET #T = '2019-02-29'
select #T as Original,
TRY_PARSE(#T AS DATE USING 'en-gb') as DateIfValid,
TRY_PARSE(LEFT(#T,4) + '-' + CAST(CAST(SUBSTRING(#T,6,2) AS tinyint)+1 AS VARCHAR(2)) + '-1' AS DATE USING 'en-gb') as Replacement
Returns
Original DateIfValid Replacement
---------- ----------- -----------
2019-02-29 NULL 2019-03-01
And below code demonstrates how to use it as a function:
select #T as Original,
,ISNULL(TRY_PARSE(#T AS DATE USING 'en-gb'), TRY_PARSE(LEFT(#T,4) + '-' + CAST(CAST(SUBSTRING(#T,6,2) AS tinyint)+1 AS VARCHAR(2)) + '-1' AS DATE USING 'en-gb')) as FinalResult
Original FinalResult
---------- -----------
2019-02-29 2019-03-01
Hope this helps, BR
No, there is not.
However if can help you to know that leap years are once every four years: 2008, 2012, 2016...
Considered as integer numbers they are also multiple of 4. So, if a year is a leap year then:
YEAR % 4 = 0
You can try to use this at your advantage.
I have a tble which has fromdate and todate fields. I have been trying to fetch data for a particular date.
Below is the query that i have been trying with no luck,
SELECT *
FROM install
WHERE customer != ''
AND billtype = 'QUARTERLY'
AND (STR_TO_DATE (fromdate, ' %m-%Y') + INTERVAL 3 MONTH=STR_TO_DATE('01-01-2018', ' %m-%Y'))
For Example
if he user selects january month of 2019, any row with fromdate + 3 months should come up.
Any suggestions
Try using date inequalities, which avoid the calls to STR_TO_DATE:
SELECT *
FROM install
WHERE
customer <> '' AND
billtype = 'QUARTERLY' AND
DATE_ADD(fromdate, INTERVAL 3 MONTH) BETWEEN '2018-01-01' AND '2018-01-31';
FYI your current syntax is off, which is why it won't work as you expected.
But actually, a better way to express your logic would be to subtract 3 months from January 2018:
SELECT *
FROM install
WHERE
customer <> '' AND
billtype = 'QUARTERLY' AND
fromdate BETWEEN '2017-10-01' AND '2017-10-31';
This version of the query is also SARGable, meaning that an index involving the fromdate column could still be used.
I have an Oracle SQL Query, which I am trying to re-write in MySQL.
PS : The dates are just arbitrary here, in the actual scenario I use this custom query inside Tableau where it accepts user defined Dates.The "Reference Start Date" is being compared to "Start Date".
Part of the Oracle SQL query:
CASE WHEN psr.dt_orgn IS NULL THEN NULL
ELSE
CASE WHEN CAST('2017-05-22' AS DATE) >= CAST('2017-06-22' AS DATE) THEN TRUNC(psr.dt_orgn,'IW')
ELSE TRUNC(psr.dt_orgn + ((CAST('2017-06-22' AS DATE)-CAST('2017-05-22' AS DATE))*(INTERVAL '1' DAY)),'IW')
END
END) week
The above uses an existing Calender table in a Database to convert dates into 'Week Start Dates' i.e Monday Date of that Week
MySQL version:
CASE WHEN psr.originated IS NULL THEN NULL
ELSE
CASE WHEN CAST('2017-05-22' AS DATE) >= CAST('2017-06-22' AS DATE) THEN DATE_ADD(psr.originated,
INTERVAL - WEEKDAY(psr.originated) DAY)
ELSE DATE_ADD(psr.originated + ((CAST('2017-06-22'AS DATE) - CAST('2017-05-22' AS DATE)) * (INTERVAL '1' DAY))
END
END) week
I am using a solution I found to get 'Week Start Date' in MySQL as follows:
DATE_ADD(mydate, INTERVAL(1-DAYOFWEEK(mydate)) DAY)
And I am getting error while doing this.
My question is how do I apply the same to the 2nd else condition with the '1 day Interval' in the MySQL query?
Or is there an easier solution to get week start date in MySQL?
I guess you're asking how to take any arbitrary DATE value and return the DATE value of the most recent Monday. In Oracle you can use TRUNC(datestamp, 'W') to do that.
Here's one way to do it in MySQL:
FROM_DAYS(TO_DAYS(datestamp) -MOD(TO_DAYS(datestamp) -2, 7))
If you wanted the Sunday, you would use
FROM_DAYS(TO_DAYS(datestamp) -MOD(TO_DAYS(datestamp) -1, 7))
I wrote it up here. http://www.plumislandmedia.net/mysql/sql-reporting-time-intervals/
You can get the day of a date using the function on MySQL called dayname(), for example dayname("2006-04-24") will return you Monday. In this case you only need to put the row that contains your date into the function. After that you can easily compare two dates.
In perl I have the following database query:
my $list = $db->SelectARef("SELECT p.*, u.usr_login, u.usr_money, u.usr_email, u.usr_pay_email, u.usr_pay_type
FROM Payments p, Users u
WHERE status='PENDING'
AND p.usr_id=u.usr_id
ORDER BY u.usr_pay_type");
Within the array result there is a field named "created".
What I want to do is add another element to the array for each row as "next payment".
Payments are processed after 30 full days from the datetime value but only on the 6th day of every month. Basically I want each result to have a "next payment" element stating which day and month they should get paid on.
e.g created = 2013-07-29 18:55:37
30 days from this is the 28th August 2013
Therefore the next payment date would be the 6th September
I have no idea where to start with this, any help anyone can provide would be greatly appreciated!
Thanks
The following shows the logic:
select (date(t) - interval (day(t) - 1) day) + interval 1 month + interval 5 day
from (select cast(now() as datetime) as t) t;
You can put this into your query as:
SELECT p.*, u.usr_login, u.usr_money, u.usr_email, u.usr_pay_email, u.usr_pay_typel,
created - interval (day(created) - 1) day) + interval 1 month + interval 5 day as nextpay
FROM Payments p join
Users u
on p.usr_id=u.usr_id
WHERE status='PENDING'
ORDER BY u.usr_pay_type;
The expression can be simplified to:
created - interval (day(created) + 4) day) + interval 1 month as nextpay
If I understand correctly, you want to compute a next_payment date which is the nearest 6th day of a month that is at least 30 days from the created date. Another way of saying that is that your due date is always the 6th of the month with at least a 30 day grace period from the close date.
Unfortunately, we can't do a simple rounding or stepping here, because the Gregorian calendar isn't quite a regular series. (It quite isn't a regular series?) So, we'll put conditional logic into a query. As this looks just awful, we'll hide it in a function:
DELIMITER //
CREATE FUNCTION next_payment(close_date DATE)
RETURNS DATE
DETERMINISTIC
READS SQL DATA
LANGUAGE SQL
BEGIN
DECLARE min_grace INT DEFAULT 30; -- minimum grace period of 30 days
DECLARE bill_dom INT DEFAULT 6; -- billing day of month is the 6th
DECLARE d DATE;
SET d = close_date + INTERVAL min_grace DAY;
IF DAY(d) > bill_dom THEN -- Did 30 days\' grace put us beyond the 6th?
SET d = d + INTERVAL 1 MONTH; -- If so, advance to next month.
END IF;
RETURN d + INTERVAL(bill_dom - DAY(d)) DAY; -- Now "round" to the 6th of the month
END
//
DELIMITER ;
How you use the logic is up to you. Adding SELECT next_payment(p.created) AS "next_payment", ... will give you the additional column you want. You could parameterize the above (e.g., CREATE FUNCTION next_payment(close_date DATE, min_grace INT, bill_dom INT)) for greater flexibility. You might rip the SET/IF logic out of the function and jam it into one unwieldy CASE/END statement in your SELECT. You could create a VIEW that automatically "appends" a next_payment column to the table, or ALTER TABLE and populate a new column, etc.