Create a calendar database table like this - mysql

I would like to create a calendar SQL table like this one but for some years.
(I'm using mysql 5.7.28)
Date is DD-MM-YYYY
Is it possible?

If you are running MySQL 8.0, you can use a recursive query:
with recursive cte as (
select '2019-01-01 00:00:00' dt
union all
select dt + interval 1 hour from cte where dt < '2020-01-01' - interval 1 hour
)
select
row_number() over(order by dt) id,
date(dt) day,
time(dt) start_hour,
time(dt + interval 1 hour) end_hour
from cte
The recursive cte generates a list of datetimes between the given boundaries (here, that's year 2019), with a 1 hour increment. Then, the outer query produces the expected result, by extracting the day and hour parts.
You can adjust the boudaries and the increment as per your exact requirements.
Side note: I would suggest to retain the full datetime in the calendar table as well; there are many situation where having a proper datetime value is more convenient than separated dates and times.
Demo on DB Fiddle for the first day only:
id | day | start_hour | end_hour
-: | :--------- | :-------------- | :--------------
1 | 2019-01-01 | 00:00:00.000000 | 01:00:00.000000
2 | 2019-01-01 | 01:00:00.000000 | 02:00:00.000000
3 | 2019-01-01 | 02:00:00.000000 | 03:00:00.000000
4 | 2019-01-01 | 03:00:00.000000 | 04:00:00.000000
5 | 2019-01-01 | 04:00:00.000000 | 05:00:00.000000
6 | 2019-01-01 | 05:00:00.000000 | 06:00:00.000000
7 | 2019-01-01 | 06:00:00.000000 | 07:00:00.000000
8 | 2019-01-01 | 07:00:00.000000 | 08:00:00.000000
9 | 2019-01-01 | 08:00:00.000000 | 09:00:00.000000
10 | 2019-01-01 | 09:00:00.000000 | 10:00:00.000000
11 | 2019-01-01 | 10:00:00.000000 | 11:00:00.000000
12 | 2019-01-01 | 11:00:00.000000 | 12:00:00.000000
13 | 2019-01-01 | 12:00:00.000000 | 13:00:00.000000
14 | 2019-01-01 | 13:00:00.000000 | 14:00:00.000000
15 | 2019-01-01 | 14:00:00.000000 | 15:00:00.000000
16 | 2019-01-01 | 15:00:00.000000 | 16:00:00.000000
17 | 2019-01-01 | 16:00:00.000000 | 17:00:00.000000
18 | 2019-01-01 | 17:00:00.000000 | 18:00:00.000000
19 | 2019-01-01 | 18:00:00.000000 | 19:00:00.000000
20 | 2019-01-01 | 19:00:00.000000 | 20:00:00.000000
21 | 2019-01-01 | 20:00:00.000000 | 21:00:00.000000
22 | 2019-01-01 | 21:00:00.000000 | 22:00:00.000000
23 | 2019-01-01 | 22:00:00.000000 | 23:00:00.000000
24 | 2019-01-01 | 23:00:00.000000 | 00:00:00.000000
In earlier versions, you would typically create a large table of numbers by cross-joining subqueries, and use the number range to increment the initial date. row_number() can be emulated with a MySQL variable:
select
#id:=#id + 1 id,
date(dt) day,
time(dt) start_hour,
time(dt + interval 1 hour) end_hour,
0 prenoted
from (
select '2019-01-01' + interval d0.n + 10 * d1.n + 100 * d2.n + 1000 * d3.n hour dt
from
(
select 0 n union all select 1 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
) d0
cross join (
select 0 n union all select 1 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
) d1
cross join (
select 0 n union all select 1 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
) d2
cross join (
select 0 n union all select 1 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
) d3
where '2019-01-01' + interval d0.n + 10 * d1.n + 100 * d2.n + 1000 * d3.n hour < '2020-01-01'
) t
cross join (select #id := 0 id) i
order by dt
The above query gives you a maximum span of 10 000 hours (wich represent a little more than 416 days); you can add another cross join and update the arithmetic to handle up to 100 000 hours (and so on).
Demo on DB Fiddle for the first 24 hours.

Related

Count records from mysql table between two dates and return zero if rows are empty

I have a table in mysql with two fields:
DATA | PAGE
2020-07-30 21:44:08 | abc
2020-07-31 20:14:18 | abc
2020-08-02 12:23:08 | xyz
2020-08-04 10:21:27 | abc
2020-08-04 12:54:32 | def
2020-08-04 15:41:44 | abc
I would like to query the table for page "abc" and get the sum of the daily views from the last 7 days and return zero if the page has no views:
DATA | VIEWS
2020-07-29 | 0
2020-07-30 | 1
2020-07-31 | 1
2020-08-01 | 0
2020-08-02 | 0
2020-08-03 | 0
2020-08-04 | 2
After some trials and errors adapting some queries I found here, I reached this version that return the results I want, but I don't think it's perfect and the limit using the calendar should not be needed.
SELECT DATE_FORMAT(data, "%Y-%m-%d") as data, count(*)-1 as views
FROM (
SELECT date(data) as data FROM visitas as t1 where page like "abc"
UNION ALL
SELECT curdate() - interval a day AS data
FROM (
SELECT 0 as a UNION
SELECT 1 UNION
SELECT 2 UNION
SELECT 3 UNION
SELECT 4 UNION
SELECT 5 UNION
SELECT 6
) as t2
) as t3 GROUP BY data
ORDER BY data DESC
LIMIT 7
Can this query be improved?
Thanks
You can do that without any JOIN and use UNION instead:
SELECT dd,SUM(cnt) as VIEWS FROM (
SELECT DATE(NOW())- INTERVAL 6 DAY as dd , 0 as cnt
UNION
SELECT DATE(NOW())- INTERVAL 5 DAY as dd , 0 as cnt
UNION
SELECT DATE(NOW())- INTERVAL 4 DAY as dd , 0 as cnt
UNION
SELECT DATE(NOW())- INTERVAL 3 DAY as dd , 0 as cnt
UNION
SELECT DATE(NOW())- INTERVAL 2 DAY as dd , 0 as cnt
UNION
SELECT DATE(NOW())- INTERVAL 1 DAY as dd , 0 as cnt
UNION
SELECT DATE(NOW())- INTERVAL 0 DAY as dd , 0 as cnt
UNION ALL
SELECT DATE(`date`) as dd, 1 as cnt FROM `test3` WHERE page='abc' AND DATE(`date`) >= DATE(NOW()) - INTERVAL 7 DAY
) as tb1 GROUP BY dd
The result will be
dd | VIEWS
--------------------
2020-07-29 | 0
2020-07-30 | 1
2020-07-31 | 1
2020-08-01 | 0
2020-08-02 | 0
2020-08-03 | 0
2020-08-04 | 2
With a LEFT join of the calendar to the table:
SELECT DATE_FORMAT(c.data, "%Y-%m-%d") data,
COUNT(page) views
FROM (
SELECT CURDATE() - INTERVAL a day data
FROM (
SELECT 0 as a UNION
SELECT 1 UNION
SELECT 2 UNION
SELECT 3 UNION
SELECT 4 UNION
SELECT 5 UNION
SELECT 6
) t
) c
LEFT JOIN visitas v
ON DATE(v.data) = c.data AND page = 'abc'
GROUP BY c.data
ORDER BY c.data DESC
See the demo.
Results:
| data | views |
| ---------- | ----- |
| 2020-08-04 | 2 |
| 2020-08-03 | 0 |
| 2020-08-02 | 0 |
| 2020-08-01 | 0 |
| 2020-07-31 | 1 |
| 2020-07-30 | 1 |
| 2020-07-29 | 0 |

YTD Calculations in MySQL from (SELECT statement) including All Days of Year

Given this query...
select
sum(count) as quotes,
date
from (
select
1 as count,
DATE_FORMAT(CONVERT_TZ(createdAt, 'UTC', 'US/Pacific'), "%Y-%m-%d") as date
from quotes
where deletedAt IS NULL
) q1
group by date
order by date;
I get the following results (showing 2020-02 results only, but actual results would go back several years)...
NOTE: 2020-02-02 received 0 quotes and is missing
+-------+------------+
| count | date |
+-------+------------+
| 1 | 2020-02-01 |
| 2 | 2020-02-03 |
| 1 | 2020-02-04 |
| 1 | 2020-02-05 |
| 1 | 2020-02-06 |
| 1 | 2020-02-07 |
| 3 | 2020-02-08 |
| 3 | 2020-02-09 |
| 3 | 2020-02-10 |
| 1 | 2020-02-11 |
+-------+------------+
How do I modify the query to...
Fill in the missing days (e.g. 2020-02-02 in this example)
add the ytdCount column, which is a rolling count by year
so that the output is like this...
add the ytdCount column
\/
+-------+----------+------------+
| count | ytdCount | date |
+-------+----------+------------+
| 1 | 1 | 2020-02-01 |
| 0 | 1 | 2020-02-02 | <- was missing from previous example
| 2 | 3 | 2020-02-03 |
| 1 | 4 | 2020-02-04 |
| 1 | 5 | 2020-02-05 |
| 1 | 6 | 2020-02-06 |
| 1 | 7 | 2020-02-07 |
| 3 | 10 | 2020-02-08 |
| 3 | 13 | 2020-02-09 |
| 3 | 16 | 2020-02-10 |
| 1 | 17 | 2020-02-11 |
+-------+----------+------------+
References
I found MYSQL to calculate YTD which shows how to do this if I were selecting from a simple table, but since my "table" is actually a select statement, I'm not sure how to translate the example to my use case.
I found get all dates in the current month which shows how to generate all the dates in a month, but I need all the days for a particular year.
I solved the problem by creating two temporary tables and a UNION
Create temp table for quotes
DROP TABLE IF EXISTS __quotes__;
CREATE TABLE __quotes__ (quotes INT(11), ytdQuotes INT(11), date DATE)
ENGINE=MEMORY
AS
select
sum(count) as quotes,
NULL as ytdQuotes,
date
from (select 1 as count, DATE_FORMAT(CONVERT_TZ(createdAt, 'UTC', 'US/Pacific'), "%Y-%m-%d") as date from quotes where deletedAt IS NULL) q1 group by date order by date;
Create temp table for the date range
note: you can easily get more/less days by changing the wear clause
DROP TABLE IF EXISTS __daterange__;
CREATE TABLE __daterange__ (quotes INT(11), ytdQuotes INT(11), date DATE)
ENGINE=MEMORY
AS
select
NULL as quotes,
NULL as ytdQuotes
date
from (select date
from (
SELECT a.date
FROM (
SELECT curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a) + + (1000 * d.a)) DAY AS date
FROM (SELECT 0 AS a UNION ALL SELECT 1 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) AS a
CROSS JOIN (SELECT 0 AS a UNION ALL SELECT 1 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) AS b
CROSS JOIN (SELECT 0 AS a UNION ALL SELECT 1 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) AS c
CROSS JOIN (SELECT 0 AS a UNION ALL SELECT 1 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) AS d) a
) daterange
where date >= "2012-11-14") daterange;
Finally, here's the query that brings it all together
select
coalesce(sum(quotes), 0) as quotes,
coalesce((select sum(q2.quotes) from __quotes__ q2 where YEAR(report.date) = YEAR(q2.date) AND q2.date <= report.date), 0) as ytdQuotes,
date
from (
select * from __quotes__
UNION ALL
select * from __daterange__
) report
group by date

MySQL: Select all dates between date range and get table data matching dates

There is a table which has data as such:
-----------------------
| id | date |
-----------------------
| 1 | 2016-07-11 |
| 2 | 2016-07-11 |
| 3 | 2016-07-15 |
| 4 | 2016-07-15 |
| 5 | 2016-07-15 |
| 6 | 2016-07-16 |
| 7 | 2016-07-19 |
| 8 | 2016-07-20 |
-----------------------
I want to get a date range (all dates) and the count of IDs for each date, returning 0 when no records exist.
If run for dates between 2016-07-10 to 2016-07-20, the result should look like this:
--------------------------
| date | count(id) |
--------------------------
| 2016-07-10 | 0 |
| 2016-07-11 | 2 |
| 2016-07-12 | 0 |
| 2016-07-13 | 0 |
| 2016-07-14 | 0 |
| 2016-07-15 | 3 |
| 2016-07-16 | 1 |
| 2016-07-17 | 0 |
| 2016-07-18 | 0 |
| 2016-07-19 | 1 |
| 2016-07-20 | 1 |
--------------------------
I've found solutions for getting a date range but couldn't figure out how to get it to count the IDs that exist for those dates within a table.
Thanks!
I figured this out by modifying the query given in the solution for getting all dates.
The following query returns all dates, and counts of the IDs if any records exist:
select d.date, count(v.id) from
(select adddate('1970-01-01',t4.i*10000 + t3.i*1000 + t2.i*100 + t1.i*10 + t0.i) date 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) t0,
(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) t1,
(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) t2,
(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) t3,
(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) t4) d
left join visitors v on d.date = v.date
where d.date between '2016-06-01' and '2016-06-30'
group by d.date
order by d.date
Courtesy for getting the dates range goes to #mark-bannister and a simple join on the query matching for results, and sorting gets the solution.
Please try the following, basically a group by is needed that would help you get the desired result. Also have the where condition with range as needed
SELECT date ,count(date) from datetable group by date;

Mysql query to get count per months

I have a table for my users that have a field named "created" that have the registration date.
How can i get a list that contains a count for the registrations number per month in last 12 months?
Like this:
Month Count
1 1232
2 2222
3 122
4 4653
... ...
12 7654
I'm not used to working with mysql, so until now i just know how to count the number of registrations in last year, not how to group that count by last 12 months. Thanks in advance!
UPDATE
Now I'm getting this, using #fthiella solution:
+------------------------------+-------------------------------+----------+
| Year(FROM_UNIXTIME(created)) | Month(FROM_UNIXTIME(created)) | Count(*) |
+------------------------------+-------------------------------+----------+
| 2012 | 4 | 9927 |
| 2012 | 5 | 5595 |
| 2012 | 6 | 4431 |
| 2012 | 7 | 3299 |
| 2012 | 8 | 429 |
| 2012 | 10 | 3698 |
| 2012 | 11 | 6208 |
| 2012 | 12 | 5142 |
| 2013 | 1 | 1196 |
| 2013 | 2 | 10 |
+------------------------------+-------------------------------+----------+
How can i force query to give me the months with count = 0?
Solution by #fthiella (thanks a lot!):
SELECT y, m, Count(users.created)
FROM (
SELECT y, m
FROM
(SELECT YEAR(CURDATE()) y UNION ALL SELECT YEAR(CURDATE())-1) years,
(SELECT 1 m 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) months) ym
LEFT JOIN users
ON ym.y = YEAR(FROM_UNIXTIME(users.created))
AND ym.m = MONTH(FROM_UNIXTIME(users.created))
WHERE
(y=YEAR(CURDATE()) AND m<=MONTH(CURDATE()))
OR
(y<YEAR(CURDATE()) AND m>MONTH(CURDATE()))
GROUP BY y, m;
And the results:
+------+----+----------------------+
| y | m | Count(users.created) |
+------+----+----------------------+
| 2012 | 5 | 5595 |
| 2012 | 6 | 4431 |
| 2012 | 7 | 3299 |
| 2012 | 8 | 429 |
| 2012 | 9 | 0 |
| 2012 | 10 | 3698 |
| 2012 | 11 | 6208 |
| 2012 | 12 | 5142 |
| 2013 | 1 | 1196 |
| 2013 | 2 | 10 |
| 2013 | 3 | 0 |
| 2013 | 4 | 0 |
+------+----+----------------------+
If created is an INT field, you should use FROM_UNIXTIME function to convert it to a date field, and then MONTH function to extract the month:
SELECT Month(FROM_UNIXTIME(created)), Count(*)
FROM yourtable
WHERE FROM_UNIXTIME(created) >= CURDATE() - INTERVAL 1 YEAR
GROUP BY Month(FROM_UNIXTIME(created))
this will count all the rows that have been created in the last 12 months. Please notice that it's probably better to also group by the YEAR:
SELECT Year(FROM_UNIXTIME(created)), Month(FROM_UNIXTIME(created)), Count(*)
FROM yourtable
WHERE FROM_UNIXTIME(created) >= CURDATE() - INTERVAL 1 YEAR
GROUP BY Year(FROM_UNIXTIME(created)), Month(FROM_UNIXTIME(created))
If you need to count the registration numbers instead of the rows, you could use something like
COUNT(registration_number)
to skip null values, or
COUNT(DISTINCT registration_number)
to count only distinct ones.
Edit
If you also need to show months that have count=0, I would use a query like this that returns all of the months for the current and for the previous year:
SELECT y, m
FROM
(SELECT YEAR(CURDATE()) y UNION ALL SELECT YEAR(CURDATE())-1) years,
(SELECT 1 m 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) months;
And then I'd use a LEFT JOIN, that returns all of the rows of the first query, and only the rows of the second query that matches:
SELECT y, m, Count(yourtable.created)
FROM (
SELECT y, m
FROM
(SELECT YEAR(CURDATE()) y UNION ALL SELECT YEAR(CURDATE())-1) years,
(SELECT 1 m 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) months) ym
LEFT JOIN yourtable
ON ym.y = YEAR(FROM_UNIXTIME(yourtable.created))
AND ym.m = MONTH(FROM_UNIXTIME(yourtable.created))
WHERE
(y=YEAR(CURDATE()) AND m<=MONTH(CURDATE()))
OR
(y<YEAR(CURDATE()) AND m>MONTH(CURDATE()))
GROUP BY y, m
(please notice that here I am considering just the last 12 months, so if we are in the middle April 2013 it will count rows in the interval May 2012 - April 13, if this is not the correct behaviour please let me know)
SELECT MONTH(reg_date) , COUNT(reg_date)
FROM your_table
WHERE reg_date >= NOW() - INTERVAL 1 YEAR
GROUP BY MONTH(reg_date)
In case this helps anyone else, I additionally wanted this query but for a given time interval that was not necessarily the last 12 months (also with a timestamp for the date):
set #start='2013-05-01 00:00:00';
set #end='2015-03-31 00:00:00';
SELECT YEAR(FROM_UNIXTIME(yourtable.created_date)), MONTH(FROM_UNIXTIME(yourtable.created_date)), COUNT(yourtable.created_date)
FROM yourtable
WHERE created_date BETWEEN UNIX_TIMESTAMP( DATE(#start) ) AND UNIX_TIMESTAMP( DATE(#end) )
GROUP BY YEAR(FROM_UNIXTIME(yourtable.created_date)), MONTH(FROM_UNIXTIME(yourtable.created_date))
ORDER BY YEAR(FROM_UNIXTIME(yourtable.created_date)), MONTH(FROM_UNIXTIME(yourtable.created_date));

MySQL left join with a single table

Using the following query to get a hourly breakdown of transactions
SELECT hour(Stamp) AS Hour, count(1) AS Count FROM Transactions GROUP by 1 WITH ROLLUP;
Results in the following output:
+------+-------+
| Hour | Count |
+------+-------+
| 0 | 269 |
| 1 | 342 |
| 2 | 319 |
| 3 | 284 |
| 4 | 235 |
| 5 | 174 |
| 6 | 91 |
| 7 | 54 |
| 8 | 31 |
| 9 | 21 |
| 10 | 21 |
| 11 | 1 |
| NULL | 1842 |
+------+-------+
I would like to display the hours with 0 transactions (e.g. in this example, every hour between 12 and 23 would show '0'). What would be the simplest way to do this?
try something like this (the -1 hour_id is for the rollup total):
drop table if exists hours;
create table hours(hour_id tinyint primary key) engine=innodb;
insert into hours (hour_id) values
(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),
(13),(14),(15),(16),(17),(18),(19),(20),(21),(22),(23),(0),(-1);
select
h.hour_id,
if(t.counter is null, 0, t.counter) as counter
from
hours h
left outer join
(
select
if(hour(stamp) is null, -1, hour(stamp)) as hour_id,
count(stamp) as counter
from
transactions group by stamp with rollup
) t
on h.hour_id = t.hour_id;
if you want it by months create a months table 1..12 + -1 etc...
SELECT Temp.Hour, COUNT(1)
FROM (
SELECT 0 AS Hour 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 UNION
SELECT 12 UNION
SELECT 13 UNION
SELECT 14 UNION
SELECT 15 UNION
SELECT 16 UNION
SELECT 17 UNION
SELECT 18 UNION
SELECT 19 UNION
SELECT 20 UNION
SELECT 21 UNION
SELECT 22 UNION
SELECT 23
) AS Temp
LEFT JOIN Transactions
ON Temp.Hour = HOUR(Transactions.Stamp)
GROUP BY Temp.Hour
ORDER BY Temp.Hour