MySQL - count with coalesce and add missing rows - mysql

I have one tables with two date columns (Date_open and Date_closed). All I want to do it to count occurrences per day. So to see how many were opened and closed each day. We look at the last 7 days from today. The problem is that some dates are not present in either of the columns and I can not find a way to either link tables with sub query (example code 1) or get coalesce work (example code 2)?
The table looks like that:
+------+------------+-------------+------+
| code | Date_open | Date_closed | Prio |
+------+------------+-------------+------+
| 1 | 2018-01-08 | 2018-01-08 | A |
| 2 | 2018-01-01 | 2018-01-08 | B |
| 3 | 2018-01-06 | 2018-01-07 | C |
| 4 | 2018-01-06 | 2018-01-06 | A |
| 5 | 2018-01-04 | 2018-01-06 | B |
| 6 | 2018-01-03 | 2018-01-01 | C |
| 7 | 2018-01-03 | 2018-01-02 | C |
| 8 | 2018-01-03 | 2018-01-02 | C |
+------+------------+-------------+------+
And the results I want are as follows:
Date OpenNo CloseNo
2018-01-01 1 1
2018-01-02 2
2018-01-03 3
2018-01-04 1
2018-01-05
2018-01-06 2 2
2018-01-07 1
2018-01-08 1 2
The first code I tried was:
SELECT *
FROM
(SELECT t1.Date_open,
COUNT(t1.Date_open) AS 'OpenNo'
FROM
Tbl AS t1
GROUP BY t1.Date_open)
AS A
JOIN
(SELECT t2.Date_closed,
COUNT(t2.Date_closed) AS 'CloseNo'
FROM
Tbl AS t2
GROUP BY t2.Date_closed)
AS B ON A.Date_open = B.Date_closed;
This code works as long as there is data for each day.
The second code I tried was:
SELECT
COALESCE (Date_open, Date_closed) AS Date1,
COUNT(Date_closed) AS ClosedNo,
COUNT(Date_open) AS OpenNo
FROM tbl
GROUP BY Date1;
Both do not work. Any ideas please?
Below is the code to create tbl.
create table Tbl(
code int(10) primary key,
Date_open DATE not null,
Date_closed DATE not null,
Prio varchar(10));
insert into Tbl values (1,'2018-01-08','2018-01-08' ,'A');
insert into Tbl values (2,'2018-01-01','2018-01-08' ,'B');
insert into Tbl values (3,'2018-01-06','2018-01-07' ,'C');
insert into Tbl values (4,'2018-01-06','2018-01-06' ,'A');
insert into Tbl values (5,'2018-01-04','2018-01-06' ,'B');
insert into Tbl values (6,'2018-01-03','2018-01-01' ,'C');
insert into Tbl values (7,'2018-01-03','2018-01-02' ,'C');
insert into Tbl values (8,'2018-01-03','2018-01-02' ,'C');

You may use a calendar table, and then left join to your current table twice to generate the counts for each date:
SELECT
d.dt,
COALESCE(t1.open_cnt, 0) AS OpenNo,
COALESCE(t2.closed_cnt, 0) AS CloseNo
FROM
(
SELECT '2018-01-01' AS dt UNION ALL
SELECT '2018-01-02' UNION ALL
SELECT '2018-01-03' UNION ALL
SELECT '2018-01-04' UNION ALL
SELECT '2018-01-05' UNION ALL
SELECT '2018-01-06' UNION ALL
SELECT '2018-01-07' UNION ALL
SELECT '2018-01-08'
) d
LEFT JOIN
(
SELECT Date_open, COUNT(*) AS open_cnt
FROM Tbl
GROUP BY Date_open
) t1
ON d.dt = t1.Date_open
LEFT JOIN
(
SELECT Date_closed, COUNT(*) AS closed_cnt
FROM Tbl
GROUP BY Date_closed
) t2
ON d.dt = t2.Date_closed
GROUP BY
d.dt
ORDER BY
d.dt;
Demo
The reason I aggregate the open and closed date counts in separate subqueries is that if were to try to just do a straight join across all tables involved, we would have to deal with double counting.
Edit:
If you wanted to just use the current date and seven days immediately preceding it, then here is a CTE which would do that:
WITH dates (
SELECT CURDATE() AS dt UNION ALL
SELECT DATE_SUB(CURDATE(), INTERVAL 1 DAY) UNION ALL
SELECT DATE_SUB(CURDATE(), INTERVAL 2 DAY) UNION ALL
SELECT DATE_SUB(CURDATE(), INTERVAL 3 DAY) UNION ALL
SELECT DATE_SUB(CURDATE(), INTERVAL 4 DAY) UNION ALL
SELECT DATE_SUB(CURDATE(), INTERVAL 5 DAY) UNION ALL
SELECT DATE_SUB(CURDATE(), INTERVAL 6 DAY) UNION ALL
SELECT DATE_SUB(CURDATE(), INTERVAL 7 DAY)
)
You could inline the above into my original query which is aliased as d, and it should work.

Coalesce can be confusing - it returns the first non-null value from the list you provide to it.
I don't know if this question requires a super-complex answer.
To get the count of open and closed for each unique date, the following could work.
SELECT
COALESCE (Date_open, Date_closed) AS Date1,
SUM(IF(Date_closed != null,1,0)) AS ClosedNo,
SUM(IF(Date_open != null,1,0)) AS OpenNo
FROM tbl
GROUP BY Date1;

Related

count by person by month between days in mysql

I have a table of absences with 3 columns id, begin_dt, end_dt. I need to give a count of how many id's has at least one day of absence in that month. So for example there is a row as follow:
id begin_dt end_dt
1 01/01/2020 02/02/2020
2 02/02/2020 02/02/2020
my result has to be
month count
01-2020 1
02-2020 2
I thought with a group by on DATE_FORMAT(SYSDATE(), '%Y-%m'), but I don't know how to manage the fact that we had to look for the whole period begin_dt till end_dt
you can find a working creation of table of this example here: https://www.db-fiddle.com/f/rYBsxQzTjjQ9nGBEmeAX6W/0
Schema (MySQL v5.7)
CREATE TABLE absence (
`id` VARCHAR(6),
`begin_dt` DATETIME,
`end_dt` DATETIME
);
INSERT INTO absence
(`id`, `begin_dt`, `end_dt`)
VALUES
('1', DATE('2019-01-01'), DATE('2019-02-02')),
('2', DATE('2019-02-02'), DATE('2019-02-02'));
Query #1
select * from absence;
| id | begin_dt | end_dt |
| --- | ------------------- | ------------------- |
| 1 | 2019-01-01 00:00:00 | 2019-02-02 00:00:00 |
| 2 | 2019-02-02 00:00:00 | 2019-02-02 00:00:00 |
View on DB Fiddle
SELECT DATE_FORMAT(startofmonth, '%Y-%m-01') year_and_month,
COUNT(*) absent_person_count
FROM absence
JOIN ( SELECT DATE_FORMAT(dt + INTERVAL n MONTH, '%Y-%m-01') startofmonth,
DATE_FORMAT(dt + INTERVAL n MONTH, '%Y-%m-01') + INTERVAL 1 MONTH - INTERVAL 1 DAY endofmonth
FROM ( SELECT MIN(begin_dt) dt
FROM absence ) startdate,
( SELECT 0 n UNION ALL
SELECT 1 UNION ALL
SELECT 2 UNION ALL
SELECT 3 UNION ALL
SELECT 4 UNION ALL
SELECT 5 ) numbers,
( SELECT DATE_FORMAT(MIN(begin_dt), '%Y-%m') mindate,
DATE_FORMAT(MAX(end_dt), '%Y-%m') maxdate
FROM absence ) datesrange
WHERE DATE_FORMAT(dt + INTERVAL n MONTH, '%Y-%m') BETWEEN mindate AND maxdate ) dateslist
ON begin_dt <= endofmonth
AND end_dt >= startofmonth
GROUP BY year_and_month;
fiddle

MySQL: How to get the Active members by month in year

I have got the previous year working members and subtracted previous year relieving employees, then got the previous month relieving list and subtracted it from the result set. Then added the newly added members in a current month.
SQL Fiddle Link
I am sensing that there lot of improvements we can do to the current query. But right now I am out of ideas, Can someone kindly help on this?
IF I have interpreted your existing query correctly, I suggest the following:
select
mnth.num, count(*)
from (
select 1 AS num union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all
select 7 union all select 8 union all select 9 union all select 10 union all select 11 union all select 12
) mnth
left join (
select
e.emp_id
, case
when e.hired_date < date_format(current_date(), '%Y-01-01') then 1
else month(e.hired_date)
end AS start_month
, case
when es.relieving_date < date_format(current_date(), '%Y-01-01') then 0
when es.relieving_date >= date_format(current_date(), '%Y-01-01') then month(es.relieving_date)
else month(current_date())
end AS end_month
from employee e
left join employee_separation es on e.emp_id = es.emp_id
) emp on mnth.num between emp.start_month and emp.end_month
where mnth.num <= month(current_date())
group by
mnth.num
;
This produced the following result (current_date() on Nov 21 2017
| num | count(*) |
|-----|----------|
| 1 | 6 |
| 2 | 7 |
| 3 | 8 |
| 4 | 9 |
| 5 | 10 |
| 6 | 9 |
| 7 | 10 |
| 8 | 11 |
| 9 | 12 |
| 10 | 13 |
| 11 | 14 |
DEMO
Depending on data volumes adding a where clause in the emp subquery may help, this also affect a case expression:
, case
when es.relieving_date >= date_format(current_date(), '%Y-01-01') then month(es.relieving_date)
else month(current_date())
end AS end_month
from employee e
left join employee_separation es on e.emp_id = es.emp_id
where es.relieving_date >= date_format(current_date(), '%Y-01-01')
I think what you need to do is to get all the employees who are already working from the employee table with:
SELECT * FROM employee WHERE hired_date<= CURRENT_DATE;
Then get the list of employees whose relieving date is still in the future using:
SELECT * FROM employee_separation WHERE relieving_date > CURRENT_DATE;
Then join the two results and group by the month and year of the reliving date as shown below:
SELECT DATE_FORMAT(B.relieving_date, "%Y-%M") RELIEVING_DATE, COUNT(*)
NUMBER_OF_ACTIVE_MEMBERS FROM
(SELECT * FROM employee WHERE hired_date <= CURRENT_DATE) A INNER JOIN
(SELECT * FROM employee_separation WHERE relieving_date > CURRENT_DATE) B
ON A.emp_id=B.emp_id
GROUP BY DATE_FORMAT(B.relieving_date , "%Y-%M");
Here is a Demo on sql fiddle.

Adding duration value from one column to the latest date in another column grouped by customers

I have a customer contract_table in my database with customer_id, contract_duration (in months) and invoice_date. I'd like to to add the duration to the latest invoice date grouped by customer.
customer_id invoice_date duration
1 2016-01-01 12
1 2017-01-01 6
2 2016-02-01 24
3 2014-03-01 24
3 2016-03-01 3
The desired output would be
customer_id contract_end
1 2017-07-01
2 2018-02-01
3 2016-05-01
I tried the following, which is not working. Up front I thought MySQL would use the duration value corresponding to the MAX(invoice_date). Is there a quick way to get the above result?
SELECT customer_id, MAX(invoice_date) + INTERVAL duration MONTH
FROM contract_table
GROUP BY customer_id
I don't quite understand your result set so I've ignored it. If my result is wrong, please try to explain why in comments...
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(customer_id INT NOT NULL
,invoice_date DATE NOT NULL
,duration INT NOT NULL
,PRIMARY KEY(customer_id,invoice_date)
);
INSERT INTO my_table VALUES
(1,'2016-01-01',12),
(1,'2017-01-01',6),
(2,'2016-02-01',24),
(3,'2014-03-01',24),
(3,'2016-03-01',3);
SELECT a.*
, a.invoice_date + INTERVAL a.duration MONTH contract_end
FROM my_table a
JOIN
( SELECT customer_id
, MAX(invoice_date + INTERVAL duration MONTH) x
FROM my_table
GROUP
BY customer_id
) b
ON b.x = a.invoice_date + INTERVAL a.duration MONTH;
+-------------+--------------+----------+--------------+
| customer_id | invoice_date | duration | contract_end |
+-------------+--------------+----------+--------------+
| 1 | 2017-01-01 | 6 | 2017-07-01 |
| 2 | 2016-02-01 | 24 | 2018-02-01 |
| 3 | 2016-03-01 | 3 | 2016-06-01 |
+-------------+--------------+----------+--------------+
Try this select:
SELECT customer_id, MAX(d) AS contract_end FROM (
SELECT
customer_id,
#num:=CAST(duration AS UNSIGNED),
DATE_ADD(invoice_date, INTERVAL #num MONTH) AS d
FROM my_table
) t
GROUP BY customer_id
SQLFiddle here.

Count of dates that are 5 days from the previous

I have a table with dates and users. I need help trying to figure out how to count only the dates that are 5 days after the previous date.
id|users_id|date
--------------------------------
1 | 1 | 2013-08-01 00:00:00
2 | 2 | 2013-08-03 00:00:00
3 | 1 | 2013-08-04 00:00:00
4 | 1 | 2013-08-06 00:00:00
5 | 2 | 2013-08-06 00:00:00
6 | 2 | 2013-08-10 00:00:00
7 | 2 | 2013-08-11 00:00:00
With the following example, I should get 2 for user 1 and 2 for user 2. I'm tried to do a subquery, but I was unable to pass in the timestamp to compare against. Any help would be much appreciated. Thank you.
Here was some of my example queries.
SELECT
tbl1.users_id,
COUNT(tbl2.date)
FROM table tbl1
LEFT JOIN table tbl2
ON tbl2.users_id = tbl1.users_id
AND tbl2.date > DATE_ADD(tbl1.date, INTERVAL 5 DAY)
GROUP BY tbl1.users_id;
SELECT
tbl1.users_id,
COUNT(tbl2.date)
FROM table tbl1
LEFT JOIN (
SELECT users_id, date
FROM table
WHERE date > DATE_ADD(tbl1.date, INTERVAL 5 DAY)
) tbl2 ON tbl1.users_id = tbl1.users_id
GROUP BY tbl1.users_id;
The last approach obviously doesn't work, since I can't put tbl1's date in the subquery.
Not tested, but something like this maybe:-
SELECT tbl1.users_id, COUNT(tbl2.date)
FROM (SELECT users_id, date, #Counter:=IF(users_id = #PrevId, #Counter + 1, 1) AS SequenceCtr, #PrevId:=users_id
FROM atable
CROSS JOIN (SELECT #Counter:=0, #PrevId:=0) Sub1
ORDER BY users_id, date) AS tbl1
LEFT OUTER JOIN (SELECT users_id, date, #Counter:=IF(users_id = #PrevId, #Counter + 1, 1) AS SequenceCtr, #PrevId:=users_id
FROM atable
CROSS JOIN (SELECT #Counter:=0, #PrevId:=0) Sub1
ORDER BY users_id, date) AS tbl2
ON tbl1.users_id = tbl2.users_id
AND tbl1.SequenceCtr + 1 = tbl2.SequenceCtr
AND tbl2.date > DATE_ADD(tbl1.date, INTERVAL 5 DAY)
GROUP BY tbl1.users_id;
Couple of subselects to get the list of dates but with a sequence number added. Then join as you have done, but also joining where the sequence numbers are 1 different.
EDIT - had a quick test on SQL fiddle and it seems to work:-
http://sqlfiddle.com/#!2/6c69b/1

MySQL count rows within the same intervals to eachother

I have a table where one column is the date:
+----------+---------------------+
| id | date |
+----------+---------------------+
| 5 | 2012-12-10 10:12:37 |
+----------+---------------------+
| 4 | 2012-12-10 09:09:55 |
+----------+---------------------+
| 3 | 2012-12-09 21:12:35 |
+----------+---------------------+
| 2 | 2012-12-09 20:15:07 |
+----------+---------------------+
| 1 | 2012-12-09 20:01:42 |
+----------+---------------------+
What I need, is to count the rows which are for example whitin 3 hours to each other. In this example I want to join the upper row with the 2nd row, and the 3rd row with the 4th and 5th rows. So my output should be like this:
+----------+---------------------+---------+
| id | date | count |
+----------+---------------------+---------+
| 5 | 2012-12-10 10:12:37 | 2 |
+----------+---------------------+---------+
| 3 | 2012-12-09 21:12:35 | 3 |
+----------+---------------------+---------+
How could I do this?
I think you need a self-join for this:
select t.id, t.date, COUNT(t2.id)
from t left outer join
t t2
on t.date between t2.date - interval 3 hour and t2.date + interval 3 hour
group by t.id, t.date
(This is untested code so it might have a syntax error.)
If you are trying to divide everything into 3-hour intervals, you can do something like:
select max(t.date), t.id, count(*)
from (select t.*,
(date(date)*100 + floor(hour(date)/3)*3) as interval
from t
) t
group by interval
I am not sure how to do this with My SQL but i am able to build a set of queries in SQL Server 2005 which will provide the intended results. Here is the working sample, its very complex and may be overly complex but that's how i was able to get the desired result:
WITH BaseData AS
(
SELECT 5 AS ID, '2012-12-10 10:12:37' AS Date
UNION ALL
SELECT 4 AS ID, '2012-12-10 09:09:55' AS Date
UNION ALL
SELECT 3 AS ID, '2012-12-09 21:12:35' AS Date
UNION ALL
SELECT 2 AS ID, '2012-12-09 20:15:07' AS Date
UNION ALL
SELECT 1 AS ID, '2012-12-09 20:01:42' AS Date
),
BaseDataWithRowNum AS
(
SELECT ID,DATE, ROW_NUMBER() OVER (ORDER BY Date DESC) AS RowNum
FROM BaseData
),
InterRelatedDates AS
(
SELECT B1.RowNum AS RowNum1,B2.RowNum AS RowNum2
FROM BaseDataWithRowNum B1
INNER JOIN BaseDataWithRowNum B2
ON B1.Date BETWEEN B2.Date AND DATEADD(hh,3,B2.Date)
AND B1.RowNum < B2.RowNum
AND B1.ID != B2.ID
),
InterRelatedDatesWithinMultipleGroups AS
(
SELECT G1.RowNum1,G2.RowNum2
FROM InterRelatedDates G1
LEFT JOIN InterRelatedDates G2
ON G1.RowNum2 = G2.RowNum2
AND G1.RowNum1 != G2.RowNum1
)
SELECT BN.ID,
BN.Date,
CountExcludingOriginalGrouppingRecord +1 AS C
FROM
(
SELECT RowNum1 AS RowNum,COUNT(1) AS CountExcludingOriginalGrouppingRecord
FROM
(
-- If a row was used in only one group then it is ok. use as it is
SELECT D1.RowNum1
FROM InterRelatedDatesWithinMultipleGroups AS D1
WHERE D1.RowNum2 IS NULL
UNION ALL
-- In case a row was selected in two groups, choose the one with higher date
SELECT Min(D1.RowNum1)
FROM InterRelatedDatesWithinMultipleGroups AS D1
WHERE D1.RowNum2 IS NOT NULL
GROUP BY D1.RowNum2
) T
GROUP BY RowNum1
) T2
INNER JOIN BaseDataWithRowNum BN
ON BN.RowNum = T2.RowNum