This question already has answers here:
SQL Server: How to select all days in a date range even if no data exists for some days
(12 answers)
Closed 4 years ago.
In db i have comments table and for statistics i would like to get total comments count per day (last 7 days).
I use:
select date(created_at) as day, count(*) as total_comments
from comments
where DATEDIFF(NOW(), created_at) <= 7
group by day
but when there are not comments in particular day it wont return anything. How can i fill missing days?
You need a table for all the 7 days
select t1.day, t2. total_comments
from (
select 1 day from dual
union
select 2 from dual
union
select 3 from dual
union
select 4 from dual
union
select 5 from dual
union
select 6 from dual
union
select 7 from dual
) t1
left join (
select date(created_at) as day
, count(*) as total_comments
from comments where DATEDIFF(NOW(), created_at) <= 7
group by day
) t2 on t1.day = t2.day
You can use a different strategy, without the need for different tables:
select
(NOW() - INTERVAL 1 DAY) as day, (SELECT count(*) as total_comment from comments where date(created_at) = day ) as total_comments
UNION
select (NOW() - INTERVAL 2 DAY) as day, (SELECT count(*) as total_comment from comments where date(created_at) = day ) as total_comments
UNION
select (NOW() - INTERVAL 3 DAY) as day, (SELECT count(*) as total_comment from comments where date(created_at) = day ) as total_comments
UNION
select (NOW() - INTERVAL 4 DAY) as day, (SELECT count(*) as total_comment from comments where date(created_at) = day ) as total_comments
UNION
select (NOW() - INTERVAL 5 DAY) as day, (SELECT count(*) as total_comment from comments where date(created_at) = day ) as total_comments
UNION
select (NOW() - INTERVAL 6 DAY) as day, (SELECT count(*) as total_comment from comments where date(created_at) = day ) as total_comments
UNION
select (NOW() - INTERVAL 7 DAY) as day, (SELECT count(*) as total_comment from comments where date(created_at) = day ) as total_comments
Related
I have a table with sell orders and I want to list the COUNT of sell orders per day, between two dates, without leaving date gaps.
This is what I have currently:
SELECT COUNT(*) as Norders, DATE_FORMAT(date, "%M %e") as sdate
FROM ORDERS
WHERE date <= NOW()
AND date >= NOW() - INTERVAL 1 MONTH
GROUP BY DAY(date)
ORDER BY date ASC;
The result I'm getting is as follows:
6 May 1
14 May 4
1 May 5
8 Jun 2
5 Jun 15
But what I'd like to get is:
6 May 1
0 May 2
0 May 3
14 May 4
1 May 5
0 May 6
0 May 7
0 May 8
.....
0 Jun 1
8 Jun 2
.....
5 Jun 15
Is that possible?
Creating a range of dates on the fly and joining that against you orders table:-
SELECT sub1.sdate, COUNT(ORDERS.id) as Norders
FROM
(
SELECT DATE_FORMAT(DATE_SUB(NOW(), INTERVAL units.i + tens.i * 10 + hundreds.i * 100 DAY), "%M %e") as sdate
FROM (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9)units
CROSS JOIN (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9)tens
CROSS JOIN (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9)hundreds
WHERE DATE_SUB(NOW(), INTERVAL units.i + tens.i * 10 + hundreds.i * 100 DAY) BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW()
) sub1
LEFT OUTER JOIN ORDERS
ON sub1.sdate = DATE_FORMAT(ORDERS.date, "%M %e")
GROUP BY sub1.sdate
This copes with date ranges of up to 1000 days.
Note that it could be made more efficient easily depending on the type of field you are using for your dates.
EDIT - as requested, to get the count of orders per month:-
SELECT aMonth, COUNT(ORDERS.id) as Norders
FROM
(
SELECT DATE_FORMAT(DATE_SUB(NOW(), INTERVAL months.i MONTH), "%Y%m") as sdate, DATE_FORMAT(DATE_SUB(NOW(), INTERVAL months.i MONTH), "%M") as aMonth
FROM (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 10 UNION SELECT 11)months
WHERE DATE_SUB(NOW(), INTERVAL months.i MONTH) BETWEEN DATE_SUB(NOW(), INTERVAL 12 MONTH) AND NOW()
) sub1
LEFT OUTER JOIN ORDERS
ON sub1.sdate = DATE_FORMAT(ORDERS.date, "%Y%m")
GROUP BY aMonth
You are going to need to generate a virtual (or physical) table, containing every date in the range.
That can be done as follows, using a sequence table.
SELECT mintime + INTERVAL seq.seq DAY AS orderdate
FROM (
SELECT CURDATE() - INTERVAL 1 MONTH AS mintime,
CURDATE() AS maxtime
FROM obs
) AS minmax
JOIN seq_0_to_999999 AS seq ON seq.seq < TIMESTAMPDIFF(DAY,mintime,maxtime)
Then, you join this virtual table to your query, as follows.
SELECT IFNULL(orders.Norders,0) AS Norders, /* show zero instead of null*/
DATE_FORMAT(alldates.orderdate, "%M %e") as sdate
FROM (
SELECT mintime + INTERVAL seq.seq DAY AS orderdate
FROM (
SELECT CURDATE() - INTERVAL 1 MONTH AS mintime,
CURDATE() AS maxtime
FROM obs
) AS minmax
JOIN seq_0_to_999999 AS seq
ON seq.seq < TIMESTAMPDIFF(DAY,mintime,maxtime)
) AS alldates
LEFT JOIN (
SELECT COUNT(*) as Norders, DATE(date) AS orderdate
FROM ORDERS
WHERE date <= NOW()
AND date >= NOW() - INTERVAL 1 MONTH
GROUP BY DAY(date)
) AS orders ON alldates.orderdate = orders.orderdate
ORDER BY alldates.orderdate ASC
Notice that you need the LEFT JOIN so the rows in your output result set will be preserved even if there's no data in your ORDERS table.
Where do you get this sequence table seq_0_to_999999? You can make it like this.
DROP TABLE IF EXISTS seq_0_to_9;
CREATE TABLE seq_0_to_9 AS
SELECT 0 AS seq UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9;
DROP VIEW IF EXISTS seq_0_to_999;
CREATE VIEW seq_0_to_999 AS (
SELECT (a.seq + 10 * (b.seq + 10 * c.seq)) AS seq
FROM seq_0_to_9 a
JOIN seq_0_to_9 b
JOIN seq_0_to_9 c
);
DROP VIEW IF EXISTS seq_0_to_999999;
CREATE VIEW seq_0_to_999999 AS (
SELECT (a.seq + (1000 * b.seq)) AS seq
FROM seq_0_to_999 a
JOIN seq_0_to_999 b
);
You can find an explanation of all this in more detail at http://www.plumislandmedia.net/mysql/filling-missing-data-sequences-cardinal-integers/
If you're using MariaDB version 10+, these sequence tables are built in.
First create a Calendar Table
SELECT coalesce(COUNT(O.*),0) as Norders, DATE_FORMAT(C.date, "%M %e") as sdate
FROM Calendar C
LEFT JOIN ORDERS O ON C.date=O.date
WHERE O.date <= NOW() AND O.date >= NOW() - INTERVAL 1 MONTH
GROUP BY DAY(date)
ORDER BY date ASC;
I'm using MySQL with PHP. Here is my query on which I'm getting the error.
$query =
"SELECT days.day, count(myDataTable.appId) as countf, count(myDataTable.appId) as counts
FROM
(
select curdate() as day
union select curdate() - interval 1 day
union select curdate() - interval 2 day
union select curdate() - interval 3 day
union select curdate() - interval 4 day
union select curdate() - interval 5 day
union select curdate() - interval 6 day
union select curdate() - interval 7 day
union select curdate() - interval 8 day
union select curdate() - interval 9 day
) days
left join myDataTable as n1
on days.day = n1.date AND n1.appId = '$id' AND n1.status = 'ERROR'
group by days.day
left join myDataTable as n2
on days.day = n2.date AND n2.appId = '$id' AND n2.status = 'SUCCESS'
group by days.day";
The error log is:
You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'left join myDataTable on days.day = myDataTable.date AND myDataTable.appId ' at line 17
It helps to properly indent your SQL so you can spot the mistakes. Indenting by the main keywords (SELECT, FROM, WHERE, HAVING, GROUP BY, and ORDER BY) will help you spot them quickly:
SELECT
days.day,
count (myDataTable.appId) as countf,
count(myDataTable.appId) as counts
FROM
(
select curdate() as day
union select curdate() - interval 1 day
union select curdate() - interval 2 day
union select curdate() - interval 3 day
union select curdate() - interval 4 day
union select curdate() - interval 5 day
union select curdate() - interval 6 day
union select curdate() - interval 7 day
union select curdate() - interval 8 day
union select curdate() - interval 9 day
) days
left join myDataTable as n1
on days.day = n1.date AND n1.appId = '$id' AND n1.status = 'ERROR'
group by
days.day
left join myDataTable as n2
on days.day = n2.date AND n2.appId = '$id' AND n2.status = 'SUCCESS'
group by
days.day
You can see that you have two GROUP BY's which won't work. Furthermore you have a LEFT JOIN hanging out in the first GROUP BY clause, which doesn't work either. Removing that first GROUP BY will get you closer:
SELECT
days.day,
count (myDataTable.appId) as countf,
count(myDataTable.appId) as counts
FROM
(
select curdate() as day
union select curdate() - interval 1 day
union select curdate() - interval 2 day
union select curdate() - interval 3 day
union select curdate() - interval 4 day
union select curdate() - interval 5 day
union select curdate() - interval 6 day
union select curdate() - interval 7 day
union select curdate() - interval 8 day
union select curdate() - interval 9 day
) days
left join myDataTable as n1
on days.day = n1.date AND n1.appId = '$id' AND n1.status = 'ERROR'
left join myDataTable as n2
on days.day = n2.date AND n2.appId = '$id' AND n2.status = 'SUCCESS'
group by
days.day
No you have a proper FROM clause. This is the first part that your Database looks at so it knows from where it is getting it's data and how it joins together. Your table aliases are set here and then used EVERYWHERE else in the query. Which leads you to the second problem.
You reference myDataTable up in your SELECT clause, but by the time the database is looking at your SELECT myDataTable isn't in context. The aliases n1 and n2 are though, so change these to reference your table aliases:
SELECT
days.day,
count (n1.appId) as countf,
count(n2.appId) as counts
FROM
(
select curdate() as day
union select curdate() - interval 1 day
union select curdate() - interval 2 day
union select curdate() - interval 3 day
union select curdate() - interval 4 day
union select curdate() - interval 5 day
union select curdate() - interval 6 day
union select curdate() - interval 7 day
union select curdate() - interval 8 day
union select curdate() - interval 9 day
) days
left join myDataTable as n1
on days.day = n1.date AND n1.appId = '$id' AND n1.status = 'ERROR'
left join myDataTable as n2
on days.day = n2.date AND n2.appId = '$id' AND n2.status = 'SUCCESS'
group by
days.day
Lastly, instead of joining your myDataTable in twice for each status, you can use a CASE statement in your SELECT:
SELECT
days.day,
SUM(CASE WHEN n1.status = 'ERROR' THEN 1 ELSE 0 END) as countf,
SUM(CASE WHEN n1.status = 'SUCCESS' THEN 1 ELSE 0 END) as counts
FROM
(
select curdate() as day
union select curdate() - interval 1 day
union select curdate() - interval 2 day
union select curdate() - interval 3 day
union select curdate() - interval 4 day
union select curdate() - interval 5 day
union select curdate() - interval 6 day
union select curdate() - interval 7 day
union select curdate() - interval 8 day
union select curdate() - interval 9 day
) days
left join myDataTable as n1
on days.day = n1.date AND n1.appId = '$id'
group by
days.day
This is how my table looks like
-----------------------
posts
----------------------
id
created_at
..
..
How should the MySQL Query look like, so that i get the number of entries the last 7 days.
The result should look something like that:
['Mon' => 234, 'Tues' => 12, ...]
you can use datediff for that
select count(*), extract(day from created_at) current_day
from posts
where datediff(now(), created_at) <= 7
group by current_day;
If you want 0 values for other day, then you would have to use a left join on a fake table that generate last seven days.
select count(posts.created_at) as nb_occurence, c.a as number_of_day
from
(select b.a
from (select 1 a
union all select 2 a
union all select 3 a
union all select 4 a
union all select 5 a
union all select 6 a
union all select 7 a) b) c
left join posts on extract(day from posts.created_at) % 7 - c.a in (select extract(day from date_sub(created_at, INTERVAL 7 DAY)) % 7 from posts)
group by c.a;
SELECT COUNT(*)
FROM posts
WHERE created_at<xy
GROUP BY created_at
SELECT COUNT(*), DATE_FORMAT(created_at,'%a')
FROM posts
WHERE created_at <= NOW() AND created_at >= DATE_SUB(created_at, INTERVAL 7 DAY)
GROUP BY DATE_FORMAT(created_at,'%a')
To add 0 for weekday having null in count:
SELECT a.weekday, IFNULL(b.total,0) FROM
(SELECT 'Mon' as weekday from dual union SELECT 'Tue' as weekday from dual union SELECT 'Wed' as weekday from dual union SELECT 'Thu' as weekday from dual union SELECT 'Fri' as weekday from dual union SELECT 'Sat' as weekday from dual union SELECT 'Sun' as weekday from dual) a
LEFT JOIN
(SELECT COUNT(*) as total, DATE_FORMAT(created_at,'%a') as weekday
FROM posts
WHERE created_at <= NOW() AND created_at >= DATE_SUB(created_at, INTERVAL 7 DAY)
GROUP BY DATE_FORMAT(created_at,'%a')) b on a.weekday=b.weekday
I have a table with sell orders and I want to list the COUNT of sell orders per day, between two dates, without leaving date gaps.
This is what I have currently:
SELECT COUNT(*) as Norders, DATE_FORMAT(date, "%M %e") as sdate
FROM ORDERS
WHERE date <= NOW()
AND date >= NOW() - INTERVAL 1 MONTH
GROUP BY DAY(date)
ORDER BY date ASC;
The result I'm getting is as follows:
6 May 1
14 May 4
1 May 5
8 Jun 2
5 Jun 15
But what I'd like to get is:
6 May 1
0 May 2
0 May 3
14 May 4
1 May 5
0 May 6
0 May 7
0 May 8
.....
0 Jun 1
8 Jun 2
.....
5 Jun 15
Is that possible?
Creating a range of dates on the fly and joining that against you orders table:-
SELECT sub1.sdate, COUNT(ORDERS.id) as Norders
FROM
(
SELECT DATE_FORMAT(DATE_SUB(NOW(), INTERVAL units.i + tens.i * 10 + hundreds.i * 100 DAY), "%M %e") as sdate
FROM (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9)units
CROSS JOIN (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9)tens
CROSS JOIN (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9)hundreds
WHERE DATE_SUB(NOW(), INTERVAL units.i + tens.i * 10 + hundreds.i * 100 DAY) BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW()
) sub1
LEFT OUTER JOIN ORDERS
ON sub1.sdate = DATE_FORMAT(ORDERS.date, "%M %e")
GROUP BY sub1.sdate
This copes with date ranges of up to 1000 days.
Note that it could be made more efficient easily depending on the type of field you are using for your dates.
EDIT - as requested, to get the count of orders per month:-
SELECT aMonth, COUNT(ORDERS.id) as Norders
FROM
(
SELECT DATE_FORMAT(DATE_SUB(NOW(), INTERVAL months.i MONTH), "%Y%m") as sdate, DATE_FORMAT(DATE_SUB(NOW(), INTERVAL months.i MONTH), "%M") as aMonth
FROM (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 10 UNION SELECT 11)months
WHERE DATE_SUB(NOW(), INTERVAL months.i MONTH) BETWEEN DATE_SUB(NOW(), INTERVAL 12 MONTH) AND NOW()
) sub1
LEFT OUTER JOIN ORDERS
ON sub1.sdate = DATE_FORMAT(ORDERS.date, "%Y%m")
GROUP BY aMonth
You are going to need to generate a virtual (or physical) table, containing every date in the range.
That can be done as follows, using a sequence table.
SELECT mintime + INTERVAL seq.seq DAY AS orderdate
FROM (
SELECT CURDATE() - INTERVAL 1 MONTH AS mintime,
CURDATE() AS maxtime
FROM obs
) AS minmax
JOIN seq_0_to_999999 AS seq ON seq.seq < TIMESTAMPDIFF(DAY,mintime,maxtime)
Then, you join this virtual table to your query, as follows.
SELECT IFNULL(orders.Norders,0) AS Norders, /* show zero instead of null*/
DATE_FORMAT(alldates.orderdate, "%M %e") as sdate
FROM (
SELECT mintime + INTERVAL seq.seq DAY AS orderdate
FROM (
SELECT CURDATE() - INTERVAL 1 MONTH AS mintime,
CURDATE() AS maxtime
FROM obs
) AS minmax
JOIN seq_0_to_999999 AS seq
ON seq.seq < TIMESTAMPDIFF(DAY,mintime,maxtime)
) AS alldates
LEFT JOIN (
SELECT COUNT(*) as Norders, DATE(date) AS orderdate
FROM ORDERS
WHERE date <= NOW()
AND date >= NOW() - INTERVAL 1 MONTH
GROUP BY DAY(date)
) AS orders ON alldates.orderdate = orders.orderdate
ORDER BY alldates.orderdate ASC
Notice that you need the LEFT JOIN so the rows in your output result set will be preserved even if there's no data in your ORDERS table.
Where do you get this sequence table seq_0_to_999999? You can make it like this.
DROP TABLE IF EXISTS seq_0_to_9;
CREATE TABLE seq_0_to_9 AS
SELECT 0 AS seq UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9;
DROP VIEW IF EXISTS seq_0_to_999;
CREATE VIEW seq_0_to_999 AS (
SELECT (a.seq + 10 * (b.seq + 10 * c.seq)) AS seq
FROM seq_0_to_9 a
JOIN seq_0_to_9 b
JOIN seq_0_to_9 c
);
DROP VIEW IF EXISTS seq_0_to_999999;
CREATE VIEW seq_0_to_999999 AS (
SELECT (a.seq + (1000 * b.seq)) AS seq
FROM seq_0_to_999 a
JOIN seq_0_to_999 b
);
You can find an explanation of all this in more detail at http://www.plumislandmedia.net/mysql/filling-missing-data-sequences-cardinal-integers/
If you're using MariaDB version 10+, these sequence tables are built in.
First create a Calendar Table
SELECT coalesce(COUNT(O.*),0) as Norders, DATE_FORMAT(C.date, "%M %e") as sdate
FROM Calendar C
LEFT JOIN ORDERS O ON C.date=O.date
WHERE O.date <= NOW() AND O.date >= NOW() - INTERVAL 1 MONTH
GROUP BY DAY(date)
ORDER BY date ASC;
I am trying to get the amount of users signed up in the past 7 days to display on a chart and it would be nice if mysql returned 0 instead of no row. Currently it just returns a row for each day THAT has a value
SELECT date(created_at),
count(id)
FROM user_accts
WHERE date(created_at) < NOW()
AND date(created_at) > DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY date(created_at);
Try:
SELECT DATE_SUB(CURDATE(), INTERVAL i DAY) date_created_at,
count(id)
FROM (SELECT 1 i UNION ALL
SELECT 2 UNION ALL
SELECT 3 UNION ALL
SELECT 4 UNION ALL
SELECT 5 UNION ALL
SELECT 6 UNION ALL
SELECT 7) i
LEFT JOIN user_accts ON date(created_at) = DATE_SUB(CURDATE(), INTERVAL i DAY)
/*AND owner_id = '131'*/
GROUP BY DATE_SUB(CURDATE(), INTERVAL i DAY)