GROUP BY custom date intervals per year - mysql

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

Related

MySql check if the dates range overlaps [duplicate]

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)

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.

multi sql select statement

i have a table of this sort:
| name | salary | day | month |
| james | 200.00 | 2 | january |
| marie | 400.00 | 4 | january |
| jimmy | 300.00 | 7 | january |
| Fredd | 700.00 | 3 | february |
| rosieli | 500.00 | 5 | february |
| rosela | 800.00 | 6 | february |
if the table name is 'db_table', how do I write an sql select query to select records from 4th January to 5th February.
something like:
select * from db_table between day='4',month='january' and day='5' and month='february'";
please how do I write a proper sql statement to get the desired results.so that the table looks like this:
| name | salary | day | month |
| marie | 400.00 | 4 | january |
| jimmy | 300.00 | 7 | january |
| Fredd | 700.00 | 3 | february |
| rosieli | 500.00 | 5 | february |
thank you
You'll need to make the day a number but this is it:
SELECT *
FROM db_table
WHERE (day >= 4 and month = 'January')
OR (day <= 5 and month = 'February')
For e.g. January to April:
SELECT *
FROM db_table
WHERE (day >= 4 and month = 'January')
OR (day <= 5 and month = 'April')
OR month IN ('February','March')
You really should do this using dates.
select t.*
from t
where str_to_date(concat_ws(2020, month, day), '%Y %M %d') between '2020-01-04' and '2020-02-05';
When possible, date comparisons should be made using dates.
I used 2020 because it is a leap year, so it will handle February 29th.
Once you've solved this, you should fix your data model to contain an actual date rather than a month/day combination.
how do I write an sql select query to select records from 4th January to 5th February.
select *
from db_table
where (month = 'january' and 4 <= cast(day as int)) or
(month = 'february' and cast(day as int) <= 5)
Note that a table design with separate month and day columns makes querying hard. It would get even harder at year boundaries. A better design would make use of your database's native datetime column type. Then you can query like:
select *
from db_table
where dt_col between '2019-01-04' and '2019-02-05'
You must create a comparable string out of month and day to use in a between statement:
select * from db_table where
concat(case month
when 'january' then '01'
when 'february' then '02'
........................
when 'december' then '12'
end, case when day < 10 then '0' else '' end, day) between '0104' and '0205'
Like this you can compare any date range by modifying only the starting and ending dates.

mysql return all rows whether data exists or not

Here is a part of table from which I am retrieving data for the last 3 months including current month
+-------------+-----------------------+
| Wo_id | updated_at |
+-------------+-----------------------+
| 1 | 2018-12-05 10:38:06 |
| 2 | 2018-12-02 15:21:17 |
| 3 | 2018-12-01 22:18:53 |
| 4 | 2018-10-25 10:38:06 |
| 5 | 2018-10-18 15:21:17 |
| 6 | 2018-10-16 22:18:53 |
| 7 | 2018-10-19 10:26:19 |
| 8 | 2018-10-27 07:06:52 |
| 9 | 2018-09-25 11:35:09 |
| 10 | 2018-09-18 12:54:27 |
The query I tried is
SELECT MONTHNAME(updated_at) month,YEAR(updated_at) year_name,
MONTH(updated_at) month_no, COUNT(*) work_orders
FROM work_orders where updated_at >= last_day(now()) + interval 1 day - interval 3 month
GROUP by MONTH(updated_at),YEAR(updated_at)
ORDER BY MONTH(updated_at) DESC
The Output I am getting is
+-------------+-------------+----------+-------------+
| month | year_name | month_no | work_orders |
+-------------+-------------+----------+-------------+
| December | 2018 | 12 | 3 |
| October | 2018 | 10 | 5 |
| September | 2018 | 9 | 2 |
As you can see the query is neglecting November as its data is not in the table. It is Including September in order to complete the cycle of 3 months which is wrong. I want the output like this
+-------------+-------------+----------+-------------+
| month | year_name | month_no | work_orders |
+-------------+-------------+----------+-------------+
| December | 2018 | 12 | 3 |
| November | 2018 | 9 | 0 |
| October | 2018 | 10 | 5 |
Can someone guide me in modifying the above mentioned query. Thanks
You need to create a table of the last three months and then LEFT JOIN that to your work orders table (using the month of the work order) to get the results you want. The table of the last 3 months can be generated using a UNION:
SELECT NOW() AS month
UNION
SELECT NOW() - INTERVAL 1 MONTH
UNION
SELECT NOW() - INTERVAL 2 MONTH
Output (as of 2018-12-07):
month
2018-12-07 11:06:15
2018-11-07 11:06:15
2018-10-07 11:06:15
Note that it is OK to subtract 1 month from the date as if the day number is larger than the number of days in the previous month it will be adjusted downward to make the date valid (see the manual).
The final query then becomes:
SELECT MONTHNAME(m.month) AS month_name, YEAR(m.month) AS year_name,
MONTH(m.month) AS month_no, COUNT(wo.Wo_id) work_orders
FROM (SELECT NOW() AS month
UNION
SELECT NOW() - INTERVAL 1 MONTH
UNION
SELECT NOW() - INTERVAL 2 MONTH) m
LEFT JOIN work_orders wo ON MONTH(wo.updated_at) = MONTH(m.month) AND
YEAR(wo.updated_at) = YEAR(m.month)
GROUP by m.month, year_name
ORDER BY m.month DESC
Note that we don't need a WHERE clause as the values in the month table restrict the data to the last 3 months that we are interested in. Also we use a LEFT JOIN so that we get a result for each month even if there were no work orders that month.
Output:
month_name year_name month_no work_orders
December 2018 12 3
November 2018 11 0
October 2018 10 5
Demo on dbfiddle

MySQL : Select number of unique days between two dates

I have a table that shows events that took place during several days in a spatial grid and I want to select the number of unique days for each cell of the grid in order to obtain the number of days where an event happend, here my table structure :
+-----+------------+------------+---------+---------+
| id | start_date | end_date | id_cell | event |
+-----+------------+------------+---------+---------+
| 1 | 2017-03-01 | 2017-03-04 | 250 | envent1 |
| 2 | 2017-03-01 | 2017-03-04 | 251 | envent1 |
| 3 | 2017-03-01 | 2017-03-04 | 307 | envent1 |
| 4 | 2017-03-01 | 2017-03-04 | 308 | envent1 |
| 5 | 2017-03-01 | 2017-03-09 | 250 | event2 |
| 9 | 2017-02-24 | 2017-03-03 | 250 | event3 |
| 13 | 2017-02-24 | 2017-03-24 | 250 | event4 |
| 17 | 2017-02-24 | 2017-03-02 | 250 | event5 |
| 21 | 2017-01-04 | 2017-01-25 | 250 | event6 |
| 25 | 2017-03-26 | 2017-03-28 | 250 | event2 |
+-----+------------+------------+---------+---------+
For example, the expected result for the cell with 250 as id is 51 days :
2017-01-04 -> 2017-01-25 = 21 days
2017-02-24 -> 2017-03-24 = 28 days
2017-03-26 -> 2017-03-28 = 2 days
The other dates are all included between 2017-02-24 and 2017-03-24 so they don't have to be counted so 21 + 28 + 2 = 51 days.
I tried to use DATEDIFF() like this :
select datediff(max(end_date) , min(start_date) ) from cell_date where id_cell = 250
The result is 83 because it counts the number of days between 2017-01-25 and 2017-03-01, the days where no event happened.
I tried some requests with DATEDIFF but I couldn't figure out how to do it. Someone can help me please ? Thanks in advance.
You can achieve this by grouping by cell_id and calculating the sum of the individual differences:
Select cell_id,[other columns],Sum(datediff(days,start_date,end_date)) as Days
From my_table
group by cell_id,[other columns]
Edit:
For your need I think you should use an intermediate table to store individual days(sure it's not the best way to do it), join them with your events and then select the distinct days in your result. Here is the code to achieve it
/*Your example Talbe*/
DECLARE #T
TABLE(ID INT,startDate DATE,EndDate DATE,id_cell INT,evnt NVARCHAR(20) )
INSERT INTO #T
VALUES
(1,'2017-03-01','2017-03-04',250,'event1'),
(2,'2017-03-01','2017-03-04',251,'event1'),
(3,'2017-03-01','2017-03-04',307,'event1'),
(4,'2017-03-01','2017-03-04',308,'event1'),
(5,'2017-03-01','2017-03-09',250,'event2'),
(9,'2017-02-24','2017-03-03',250,'event3'),
(13,'2017-02-24','2017-03-24',250,'event4'),
(17,'2017-02-24','2017-03-02',250,'event5'),
(21,'2017-01-04','2017-01-25',250,'event6'),
(25,'2017-03-26','2017-03-28',250,'event2')
/*Table to store days: ideally get the start and end dates from your table */
DECLARE #STARTDATE DATE='2017-01-04'
DECLARE #ENDDATE DATE='2017-03-28'
DECLARE #DAYS
TABLE(oneday DATE)
WHILE #StartDate <= #endDate
BEGIN
INSERT INTO #days
(
oneday
)
SELECT
#StartDate
SET #StartDate = DATEADD(dd, 1, #StartDate)
END
/*The request */
SELECT id_cell,COUNT(DISTINCT oneday) NUMBER_OF_DAYS
FROM #T t
JOIN #DAYS d ON d.oneday>=t.startDate AND d.oneday<t.EndDate
WHERE id_cell=250
GROUP BY id_cell
You could use max and min date and datediff for obtain the diff in days between the date eg: for event
select event, datediff(max(end_date) , min(star_date) )
from my_table
group by event
or yuo can sum by cell_id for the total for cell_id
select cell_id, sum(datediff(end_date , star_date) )as days
from my_table
group by cell_id
or
Looking to your sample seems you need max end_date for some event occurring starting from the same date
select id_cell, sum(datediff(max_end_date, start_date)) as days
from (
select id_cell, start_date, max(end_date) as max_end_date
from cell_date
group by id_cell, start_date ) t
group by id_cell