MySql check if the dates range overlaps [duplicate] - mysql

This question already has answers here:
Check if date is overlapping in SQL
(2 answers)
Closed 11 months ago.
+----+------------+---------------------+---------------------+---------+----------+
| id | percentage | from_date | to_date | type_id | tag |
+----+------------+---------------------+---------------------+---------+----------+
| 1 | 10.00 | 2022-04-01 00:00:01 | 2022-04-05 23:59:59 | 1 | discount |
| 2 | 10.00 | 2022-04-06 00:00:01 | 2022-04-10 23:59:59 | 1 | uplift |
| 3 | 10.00 | 2022-04-12 00:00:01 | 2022-04-15 23:59:59 | 1 | discount |
| 4 | 10.00 | 2022-04-20 00:00:01 | 2022-04-25 23:59:59 | 1 | uplift |
+----+------------+---------------------+---------------------+---------+----------+
I'm trying to create a function in php for user to create discount/uplift the prices. The user can select the from date and to date from date picker.
Now I want to restrict the user from creating discount/uplift if the selected date range falls between existing range.
Given the above table, there is a discount on the products from 2022-04-01 to 2022-04-05 and 2022-04-12 and 2022-04-15. So user can't create any discount/uplift for this range.
As above there is an uplift in the prices between 2022-04-06 to 2022-04-10 and 2022-04-20 to 2022-04-25 and user can't create any discount/uplift for this range.
SELECT * FROM `discounts` WHERE type_id = 1 AND (`from_date` <= $fromDate AND `to_date` >= $toDate);
SELECT * FROM discounts WHERE type_id = 1 AND '$fromDate' BETWEEN from_date AND to_date
SELECT * FROM discounts WHERE type_id = 1 AND '$toDate' BETWEEN from_date AND to_date
All above queries are working.
But there is a window to create discount/uplift between 2022-04-11 00:00:00 to 2022-04-11 23:59:59 and 2022-04-16 00:00:00 to 2022-04-19 23:59:59
Is there any way to check the above condition.
EDIT
My question is:
How can I validate if the user input fromDate as 2022-04-16 and toDate as 2022-04-18, because it's a valid date range which does not fall under any range in the table. So user can create record to this range.

Yo can check like this:
SELECT *
FROM `discounts`
WHERE type_id = 1
AND `from_date` <= $toDate AND `to_date` >= $fromDate;
For $fromDate = '2022-04-11 00:00:00' and $toDate = '2022-04-11 23:59:59' the query will return nothing which means that this datetime interval is available.
See the demo.

You can insert data depending on condition. The CTE in the below query emulates a source of parameters in question
insert into tbl(id, percentage, from_date, to_date, type_id, tag )
with params as(
select timestamp '2022-04-16 00:00:01' fromDate, timestamp '2022-04-18 00:00:01' toDate
)
select 5, 11 ,fromDate, toDate, 1, 'discount'
from params p
where not exists(
select 1
from tbl t
where type_id = 1 and p.fromDate < t.to_Date and t.from_Date < p.toDate)

Related

GROUP BY custom date intervals per year

Situation: I need a custom interval between dates. The problem I face when I try to GROUP BY the year and the result I get amounts to by the given year. I need a custom interval per year from December 20th with time: 00:00:00 of previous year to December 19th with time: 23:59:59 of said year. Here is some of my data:
Table - History:
id | date | income | spent
--------------------------------------------
1 | 2019-12-21 17:15:00 | 600,00 | NULL
2 | 2019-12-23 12:55:00 | 183,00 | NULL
3 | 2019-12-30 20:05:00 | NULL | 25,00
4 | 2020-01-01 15:35:00 | NULL | 13,00
5 | 2020-01-01 20:25:00 | NULL | 500,50
6 | 2020-12-10 10:25:00 | NULL | 5,50
7 | 2021-05-22 12:45:00 | 1098,00 | NULL
8 | 2021-05-23 10:18:00 | NULL | 186,00
9 | 2021-11-25 12:32:00 | NULL | 10,00
10 | 2021-12-23 10:35:00 | NULL | 10,00
The expected result:
Year | Summary Income | Summary Spent | Difference
--------------------------------------------------
2020 | 783,00 | 544,00 | 239,50
2021 | 1098,00 | 196,00 | 902,00
2022 | 0,00 | 10,00 | -10,00
I have managed to get a result with the help of a loop within a procedure:
...
SET #Aa = (SELECT MIN(date) FROM History);
CREATE TEMPORARY TABLE Yr (Year VARCHAR(4), Income FLOAT(8,2), Spent FLOAT(8,2), differ FLOAT(8,2));
Yearly: LOOP
SET #Aa = #Aa + 1;
SET #From = CONCAT((#Aa - 1), '-12-20 00:00:00');
SET #To = CONCAT(#Aa, '-12-19 23:59:59');
SET #Count = (SELECT SUM(income) FROM History WHERE date >= #From AND date <= #To);
SET #diff = (SELECT SUM(spent) FROM History WHERE date >= #From AND date <= #To);
INSERT INTO Yr (Year, Income, Spent, differ) VALUES (#Aa, #Count, #diff, (#Count - #diff));
IF (#Aa = (SELECT MAX(YEAR(date)) FROM History)) THEN LEAVE Yearly; END IF;
END LOOP;
SELECT * FROM Yr;
...
Question: I wonder if it's possible to get a custom interval for an annual summary with an condensed SQL query without using a loop?
You can simply add 11 days to the date before applying the year function to get this grouping, e.g.
SELECT YEAR(DATE_ADD(date, INTERVAL 11 DAY)) AS Year,
SUM(income) AS income,
SUM(spent) AS Spent,
IFNULL(SUM(income),0) - IFNULL(SUM(spent),0) AS difference
FROM History
GROUP BY YEAR(DATE_ADD(date, INTERVAL 11 DAY));
Example on db-fiddle

select the lists of date between from date and to date and union with result table

I have a result table.
I want to union this result table with list of dates .I wanted to know how to find the lists of date between from date and to date for union operation.
for example,if from date is '2022-06-10' and to date is '2022-06-17'.I wanted to find the list of date between from and to .If there is no data in the result for specific date set the total to 0.My expected output.
My current result query
SELECT sum(q.amount) as total,date(q.created_date) as date FROM quotation q
where q.created_date <= '2022-06-10'
and q.created_date >= '2022-06-17'
group by DATE_FORMAT(q.created_date, "%d/%m/%Y")
union
(select 0 as total,date(DATE_SUB(CURDATE(), INTERVAL 1 DAY) ))
order by DATE_FORMAT(date,"%d/%m/%Y");
The result
If you want so sum by day for the last 7 days from now and have version 8 or above
DROP TABLE IF EXISTS T;
create table t (dt date, val int);
insert into t values
('2022-06-15',10),('2022-06-15',10),('2022-06-14',10),('2022-06-14',10),('2022-06-14',10);
WITH recursive Date_Ranges AS (
select DATE(NOW()) as Date
union all
select Date - interval 1 day
from Date_Ranges
where Date > DATE(NOW()) - INTERVAL 7 DAY)
select date_ranges.date, coalesce(sum(t.val),0) sumval
from Date_Ranges
left join t on t.dt = date_ranges.date
group by date_ranges.date
order by date_ranges.date desc;
+------------+--------+
| date | sumval |
+------------+--------+
| 2022-06-16 | 0 |
| 2022-06-15 | 20 |
| 2022-06-14 | 30 |
| 2022-06-13 | 0 |
| 2022-06-12 | 0 |
| 2022-06-11 | 0 |
| 2022-06-10 | 0 |
| 2022-06-09 | 0 |
+------------+--------+
8 rows in set (0.002 sec)

Query to check if a given range of date doesn't fits between multiple range of dates

I have a table with 2 columns, checkinDate and checkoutDate. What I have to do is to add a range in the table only if it doesn't overlap the others ranges. How can I know if a given range of dates fits between all those ranges with a query?
For example, from the following rows:
startDate - endDate
2019-12-10 - 2019-12-15
2019-12-16 - 2019-12-22
2019-12-29 - 2019-01-05
2020-01-20 - 2020-01-25
If the given range of date goes from 2019-12-23 to 2019-12-28, it doesn't overlap the others ranges so I can add it in the table.
But if the range goes from from 2019-12-23 to 2019-12-30, it overlap a range so I cannot add it in the table.
I know how to check the range line by line but not how to check it with the entire table.
Here is a simple way to check for date overlap in an insert query
insert into mytable(startDate, endDate)
select i.startDate, i.endDate
from (select '2019-12-23' startDate, '2019-12-30' endDate) i
where not exists (
select 1
from mytable t
where t.startDate <= i.endDate and t.endDate >= i.startDate
)
The date range to insert is declared in the subquery aliased i. If any record in the table overlaps with that range, the insert is skipped, else it happens.
Demo on DB Fiddle:
-- set up
CREATE TABLE mytable(
id int auto_increment primary key
,startDate DATE NOT NULL
,endDate DATE NOT NULL
);
INSERT INTO mytable(startDate,endDate) VALUES ('2019-12-10','2019-12-15');
INSERT INTO mytable(startDate,endDate) VALUES ('2019-12-16','2019-12-22');
INSERT INTO mytable(startDate,endDate) VALUES ('2019-12-29','2019-01-05');
INSERT INTO mytable(startDate,endDate) VALUES ('2020-01-20','2020-01-25');
-- initial table content
select * from mytable order by startDate
id | startDate | endDate
-: | :--------- | :---------
1 | 2019-12-10 | 2019-12-15
2 | 2019-12-16 | 2019-12-22
3 | 2019-12-29 | 2019-01-05
4 | 2020-01-20 | 2020-01-25
-- this range does not overlap
insert into mytable(startDate, endDate)
select i.startDate, i.endDate
from (select '2019-12-23' startDate, '2019-12-30' endDate) i
where not exists (
select 1
from mytable t
where t.startDate <= i.endDate and t.endDate >= i.startDate
)
-- confirm it was inserted
select * from mytable order by id
id | startDate | endDate
-: | :--------- | :---------
1 | 2019-12-10 | 2019-12-15
2 | 2019-12-16 | 2019-12-22
3 | 2019-12-29 | 2019-01-05
4 | 2020-01-20 | 2020-01-25
5 | 2019-12-23 | 2019-12-30
-- this range overlaps
insert into mytable(startDate, endDate)
select i.startDate, i.endDate
from (select '2019-12-23' startDate, '2019-12-28' endDate) i
where not exists (
select 1
from mytable t
where t.startDate <= i.endDate and t.endDate >= i.startDate
)
-- it was not inserted
select * from mytable order by id
id | startDate | endDate
-: | :--------- | :---------
1 | 2019-12-10 | 2019-12-15
2 | 2019-12-16 | 2019-12-22
3 | 2019-12-29 | 2019-01-05
4 | 2020-01-20 | 2020-01-25
5 | 2019-12-23 | 2019-12-30
Well if they overlap either on of the boundaries of the given range is in an range in the table. So you could use something along the lines of:
SELECT *
FROM elbat
WHERE '2019-12-23' > startdate
AND '2019-12-23' < enddate
OR '2019-12-28' > startdate
AND '2019-12-28' < enddate;

mysql sum group by month and date using a contract start and end date

I have a table full of monthly contracts. There is a monthly price, a start date, and an end date for each. I am trying to graph each month's total revenue and am wondering if it's possible to do this in one query (vs. a query for each month).
I know how to group by month and year in mysql, but this requires a more complex solution that "understands" whether to include in the sum for a given month/year based on the start and end date of the contract.
Shorthand example
| contract_id | price | start_date | end_date |
| 1 | 299 | 1546318800 (1/1/19) | 1554004800 (3/31/19) |
| 2 | 799 | 1551416400 (3/1/19) | 1559275200 (5/31/19) |
With this example, there's an overlap in March. Both contracts are running in March, so the sum returned for that month should be 1098.
I'd like to be able to produce a report that includes every month between two dates, so in this case I'd send 1/1/19 - 12/31/19, the full year of 2019 and would hope to see 0 results as well.
| month | year | price_sum |
| 1 | 2019 | 299 |
| 2 | 2019 | 299 |
| 3 | 2019 | 1098 |
| 4 | 2019 | 799 |
| 5 | 2019 | 799 |
| 6 | 2019 | 0 |
| 7 | 2019 | 0 |
| 8 | 2019 | 0 |
| 9 | 2019 | 0 |
| 10 | 2019 | 0 |
| 11 | 2019 | 0 |
| 12 | 2019 | 0 |
Here is a full working script for your problem, which uses a calendar table approach to represent every month in 2019. Specifically, we represent each month using the first of that month. Then, a given price from your table is applicable to that month if there is overlap with the start and end range.
WITH yourTable AS (
SELECT 1 AS contract_id, 299 AS price, '2019-01-01' AS start_date, '2019-03-31' AS end_date UNION ALL
SELECT 2, 799, '2019-03-01', '2019-05-31'
),
dates AS (
SELECT '2019-01-01' AS dt UNION ALL
SELECT '2019-02-01' UNION ALL
SELECT '2019-03-01' UNION ALL
SELECT '2019-04-01' UNION ALL
SELECT '2019-05-01' UNION ALL
SELECT '2019-06-01' UNION ALL
SELECT '2019-07-01' UNION ALL
SELECT '2019-08-01' UNION ALL
SELECT '2019-09-01' UNION ALL
SELECT '2019-10-01' UNION ALL
SELECT '2019-11-01' UNION ALL
SELECT '2019-12-01'
)
SELECT
d.dt,
SUM(t.price) AS price_sum
FROM dates d
LEFT JOIN yourTable t
ON d.dt < t.end_date
AND DATE_ADD(d.dt, INTERVAL 1 MONTH) > t.start_date
GROUP BY
d.dt;
Demo
Notes:
If your dates are actually stored as UNIX timestamps, then just call FROM_UNIXTIME(your_date) to convert them to dates, and use the same approach I gave above.
I had to use the overlapping date range formula here, because the criteria for overlap in a given month is that the range of that month intersects the range given by a start and end date. Have a look at this SO question for more information on that.
My code is for MySQL 8+, though in practice you may wish to create a bona fide calendar table (the CTE version of which I called dates above), which contains the range of months/years which you want to cover your data set.
I understand that you will be given a range of dates for which you will need to report. My solution requires you to initialize a temporary table, such as date_table with the first day of each month for which you want to report on:
create temporary table date_table (
d date,
primary key(d)
);
set #start_date = '2019-01-01';
set #end_date = '2019-12-01';
set #months = -1;
insert into date_table(d)
select DATE_FORMAT(date_range,'%Y-%c-%d') AS result_date from (
select (date_add(#start_date, INTERVAL (#months := #months +1 ) month)) as date_range
from mysql.help_topic a limit 0,1000) a
where a.date_range between #start_date and last_day(#end_date);
Then this should do it:
select month(dt.d) as month, year(dt.d) as year, ifnull(sum(c.price), 0) as price_sum
from date_table dt left join contract c on
dt.d >= date(from_unixtime(c.start_date)) and dt.d < date(from_unixtime(c.end_date))
group by dt.d
order by dt.d
;
Resulting in:
+-------+------+-----------+
| month | year | price_sum |
+-------+------+-----------+
| 1 | 2019 | 299 |
| 2 | 2019 | 299 |
| 3 | 2019 | 1098 |
| 4 | 2019 | 799 |
| 5 | 2019 | 799 |
| 6 | 2019 | 0 |
| 7 | 2019 | 0 |
| 8 | 2019 | 0 |
| 9 | 2019 | 0 |
| 10 | 2019 | 0 |
| 11 | 2019 | 0 |
| 12 | 2019 | 0 |
+-------+------+-----------+
See demo
I am not sure about the semantics of the column end_date. Right now I am comparing the first a follows: start_date <= first_of_month < end_date. Perhaps the test should be start_date <= first_of_month <= end_date, in which case:
dt.d >= date(from_unixtime(c.start_date)) and dt.d < date(from_unixtime(c.end_date))
becomes:
dt.d between date(from_unixtime(c.start_date)) and date(from_unixtime(c.end_date))
With end_date being the last day of the month, it would not matter either way.

UPDATE + SET + WHERE - Dynamic minimum value

This is a follow-up to:
Dynamic minimum value for specfic range (mysql)
I do have the query to fetch the third column (lowest of the last 3 days) "Low_3_days" via SELECT command:
-----------------------------------------
| Date | Unit_ | Lowest_in_last_|
| | price | 3_days |
|----------------------------------------
| 2015-01-01 | 15 | 15 |
| 2015-01-02 | 17 | 15 |
| 2015-01-03 | 21 | 15 |
| 2015-01-04 | 18 | 17 |
| 2015-01-05 | 12 | 12 |
| 2015-01-06 | 14 | 12 |
| 2015-01-07 | 16 | 12 |
|----------------------------------------
select S.Date,Unit_price,
(select S.Date, Unit_price,
(SELECT min(s2.Unit_Price)
FROM table s2
WHERE s2.DATE BETWEEN s.DATE - interval 3 day and
s.DATE - interval 1 day
) as min_price_3_days
FROM table S;
My new challenge is - what is the best way to use UPDATE-SET-WHERE so I could add the ("Lowest_in_last_3_days") values to a new column in a table (instead of having temporary results displayed to me via SELECT).
By following the UPDATE-SET-WHERE syntax, the query would be:
UPDATE table
SET min_price_3_days =
(select S.Date, Unit_price,
(SELECT min(s2.Unit_Price)
FROM table s2
WHERE s2.DATE BETWEEN s.DATE - interval 3 day and
s.DATE - interval 1 day
) as min_price_3_days
but I have difficulties constructing the correct query.
What would be the correct approach to this? I do recognize this one is a tough one to solve.
Your UPDATE should look like:
update table set low_3_days=
(SELECT min(Unit_Price)
FROM (select unit_price, date as date2 from table) as s2
WHERE s2.date2 BETWEEN date - interval 3 day and date - interval 1 day
);
You can check it in SQLFiddle
In Fiddle I used different names for table and column. I prefer not to use SQL keywords as names