I am trying to calculate the number of users who have an active subscription on a particular day. I also want information related to the subscription plan they are on. I have a subscriptions table which includes the start date and end date of subscription as well as the plan name. I am using a recursive cte to find out the number of subscribers of different plans on a date range but I am getting the error that the cte table doesn't exist. I am using the following code.
SET #start = (SELECT MIN(start_date) FROM subscriptions);
SET #end = (SELECT MAX(end_date) FROM subscriptions);
WITH cte AS (
SELECT #start dt
UNION ALL
SELECT date_add(dt, interval 1 day) FROM cte
WHERE dt < #end
)
SELECT cte.dt, SUM(CASE WHEN subscriptions.plan_name IS NULL THEN 0 ELSE 1 END) FROM cte
LEFT JOIN subscriptions t
ON cte.dt BETWEEN t.start_date AND t.end_date
GROUP BY cte.dt;
the output should look like this
WITH cte AS (
SELECT #start dt
UNION ALL
SELECT date_add(dt, interval 1 day) FROM cte
WHERE dt < #end
)
You are refering cte in itself, which makes it recursive and needs to be defined as such (WITH RECURSIVE).
This is what you need:
WITH RECURSIVE cte AS (
SELECT #start dt
UNION ALL
SELECT date_add(dt, interval 1 day) FROM cte
WHERE dt < #end
)
Related
How to convert this with part to MySQL?
SET #start = '20210101';
SET #end = '20211231';
WITH cte AS
(
SELECT dt = DATEADD(DAY, -(DAY(#start) - 1), #start)
UNION ALL
SELECT DATEADD(MONTH, 1, dt)
FROM cte
WHERE dt < DATEADD(DAY, -(DAY(#end) - 1), #end)
)
SELECT DATENAME(MONTH,dt) Name, DAY(EOMONTH(dt)) as Days into RESULT_TABLE
FROM cte
P.S. as far as I know I cant use WITH in MySQL since version 8.0. But what about version prior to 8.0?
You can create a table from a recursive query this way:
SET #start = '20210101';
SET #end = '20211231';
CREATE TABLE result_table AS
WITH RECURSIVE cte(dt) AS
(
SELECT DATE(#start)
UNION ALL
SELECT DATE_ADD(dt, INTERVAL 1 MONTH)
FROM cte
WHERE DATE_ADD(dt, INTERVAL 1 MONTH) < #end
)
SELECT MONTHNAME(dt) AS Name, DAY(LAST_DAY(dt)) AS Days
FROM cte;
In mysql 8 or mariadb 10.2+, you would do this like:
WITH RECURSIVE cte AS (
SELECT DATE_SUB(#start, INTERVAL DAY(#start) - 1 DAY) AS dt
UNION ALL
SELECT DATE_ADD(dt, INTERVAL 1 MONTH)
FROM cte
WHERE dt < DATE_SUB(#end, INTERVAL DAY(#end) - 1 DAY)
)
SELECT MONTHNAME(dt) AS Name, DAY(LAST_DAY(dt)) AS Days
FROM cte;
But I'm not sure I understand what the "into RESULT_TABLE" was doing.
My table currently has 21000 records, it's daily updated and almost 300 entries are inserted. Now, what I want is to have a query which will fetch the counts of elements that my table had for the previous 10 days, so it returns:
26000
21300
21000
etc
Right now, I wrote this:
"SELECT COUNT(*) from tbl_task where `task_start_time` < '2020-12-01'"
And it returns 21000 but only for 1 day. I want by query to return records according to 10 days.
However, this does it for only 1 day.
edit : database flavor is mysql and date column is date not datetime
The most efficient method may be aggregation and cumulative sums:
select date(task_start_time) as dte, count(*) as cnt_on_day,
sum(count(*)) over (order by date(task_start_time)) as running_cnt
from tbl_task
group by dte
order by dte desc
limit 10;
This returns the last 10 days in the data. You can easily adjust to more days if you like -- in fact all of them -- without much trouble.
I don't know if I'm wrong, but could you not simple add a GROUP BY - statement? Like:
"SELECT COUNT(*) from tbl_task where `task_start_time` < '2020-12-01' GROUP
BY task_start_time"
EDIT:
This should only work if task_start_time is a date, not if it is a datetime
EDIT2:
If it is a datetime you could use the date function:
SELECT COUNT(*) from tbl_task where `task_start_time` < '2020-12-01' GROUP
BY DATE(task_start_time)
You can use UNION ALL and date arithmetic.
SELECT count(*)
FROM tbl_task
WHERE task_start_time < current_date
UNION ALL
SELECT count(*)
FROM tbl_task
WHERE task_start_time < date_sub(current_date, INTERVAL 1 DAY)
...
UNION ALL
SELECT count(*)
FROM tbl_task
WHERE task_start_time < date_sub(current_date, INTERVAL 9 DAY);
Edit:
You might also join a derived table that uses FROM-less SELECTs and UNION ALL to get the days to look back and then aggregate. This might be a little easier to construct dynamically. (But it may be slower I suspect.)
SELECT count(*)
FROM (SELECT 0 x
UNION ALL
SELECT 1
...
UNION ALL
SELECT 9)
INNER JOIN tbl_task t
ON t.task_start_time < date_sub(current_date, INTERVAL x.x DAY)
GROUP BY x.x;
In MySQL version 8+ you can even use a recursive CTE to construct the table with the days.
WITH RECURSIVE x
AS
(
SELECT 0 x
UNION ALL
SELECT x + 1
FROM x
WHERE x + 1 < 10
)
SELECT count(*)
FROM x
INNER JOIN tbl_task t
ON t.task_start_time < date_sub(current_date, INTERVAL x.x DAY)
GROUP BY x.x;
The following query returns the visitors and pageviews of last 7 days. However, if there are no results (let's say it is a fresh account), nothing is returned.
How to edit this in order to return 0 in days that there are no entries?
SELECT Date(timestamp) AS day,
Count(DISTINCT hash) AS visitors,
Count(*) AS pageviews
FROM behaviour
WHERE company_id = 1
AND timestamp >= Subdate(Curdate(), 7)
GROUP BY day
Assuming that you always have at least one record in the table for each of the last 7 days (regardless of the company_id), then you can use conditional aggregation as follows:
select
date(timestamp) as day,
count(distinct case when company_id = 1 then hash end) as visitors,
sum(company_id = 1) as pageviews
from behaviour
where timestamp >= curdate() - interval 7 day
group by day
Note that I changed you query to use standard date arithmetics, which I find easier to understand that date functions.
Otherwise, you would need to move the condition on the date from the where clause to the aggregate functions:
select
date(timestamp) as day,
count(distinct case when timestamp >= curdate() - interval 7 day and company_id = 1 then hash end) as visitors,
sum(timestamp >= curdate() - interval 7 day and company_id = 1) as pageviews
from behaviour
group by day
If your table is big, this can be expensive so I would not recommend that.
Alternatively, you can generate a derived table of dates and left join it with your original query:
select
curdate - interval x.n day day,
count(distinct b.hash) visitors,
count(b.hash) page_views
from (
select 1 n union all select 2 union all select 3 union all select 4
union all select 5 union all select 6 union all select 7
) x
left join behavior b
on b.company_id = 1
and b.timestamp >= curdate() - interval x.n day
and b.timestamp < curdate() - interval (x.n - 1) day
group by x.n
Use a query that returns all the dates from today minus 7 days to today and left join the table behaviour:
SELECT t.timestamp AS day,
Count(DISTINCT b.hash) AS visitors,
Count(b.timestamp) AS pageviews
FROM (
SELECT Subdate(Curdate(), 7) timestamp UNION ALL SELECT Subdate(Curdate(), 6) UNION ALL
SELECT Subdate(Curdate(), 5) UNION ALL SELECT Subdate(Curdate(), 4) UNION ALL SELECT Subdate(Curdate(), 3) UNION ALL
SELECT Subdate(Curdate(), 2) UNION ALL SELECT Subdate(Curdate(), 1) UNION ALL SELECT Curdate()
) t LEFT JOIN behaviour b
ON Date(b.timestamp) = t.timestamp AND b.company_id = 1
GROUP BY day
Use IFNULL:
IFNULL(expr1, 0)
From the documentation:
If expr1 is not NULL, IFNULL() returns expr1; otherwise it returns expr2. IFNULL() returns >a numeric or string value, depending on the context in which it is used.
You can use next trick:
First, get query that return 1 dummy row: SELECT 1;
Next use LEFT JOIN to connect summary row(s) without condition. This join will return values in case data exists on NULL values in other case.
Last select from joined queries onle what we need and convert NULL's to ZERO's
using IFNULL dunction.
SELECT
IFNULL(b.day,0) AS DAY,
IFNULL(b.visitors,0) AS visitors,
IFNULL(b.pageviews,0) AS pageviews
FROM (
SELECT 1
) a
LEFT JOIN (
SELECT DATE(TIMESTAMP) AS DAY,
COUNT(DISTINCT HASH) AS visitors,
COUNT(*) AS pageviews
FROM behaviour
WHERE company_id = 1
AND TIMESTAMP >= SUBDATE(CURDATE(), 7)
GROUP BY DAY
) b ON 1 = 1;
This keeps inserting already existing fields although it shouldn't.
BEGIN
INSERT INTO ohrm_attendance_raw_data (punch_time, device_id, card_number)
SELECT punch_time, device_id, card_number
FROM ohrm_attendance_master
WHERE ohrm_attendance_master.punch_time >= DATE_SUB(now(), INTERVAL 1 MONTH)
AND NOT EXISTS (
SELECT 1 FROM ohrm_attendance_record WHERE ohrm_attendance_record.punch_in_user_time = ohrm_attendance_master.punch_time)
AND NOT EXISTS (
SELECT 1 FROM ohrm_attendance_record WHERE ohrm_attendance_record.punch_out_user_time = punch_time);
end
It looks like your field punch_time is datetime type or something like that... so I think your problem is that you are comparing two dates... and what's the problem with that?, that MySQL and other RDBMS compare that including hour, minutes, seconds and miliseconds... so it can make that the comparison be false... You can trunc the date or give it some format:
With DATE function:
BEGIN
INSERT INTO ohrm_attendance_raw_data (punch_time, device_id, card_number)
SELECT punch_time, device_id, card_number
FROM ohrm_attendance_master
WHERE ohrm_attendance_master.punch_time >= DATE_SUB(now(), INTERVAL 1 MONTH)
AND NOT EXISTS (
SELECT 1 FROM ohrm_attendance_record WHERE DATE(ohrm_attendance_record.punch_in_user_time) = DATE(ohrm_attendance_master.punch_time))
AND NOT EXISTS (
SELECT 1 FROM ohrm_attendance_record WHERE DATE(ohrm_attendance_record.punch_out_user_time) = DATE(punch_time));
END
With DATE_FORMAT function:
BEGIN
INSERT INTO ohrm_attendance_raw_data (punch_time, device_id, card_number)
SELECT punch_time, device_id, card_number
FROM ohrm_attendance_master
WHERE ohrm_attendance_master.punch_time >= DATE_SUB(now(), INTERVAL 1 MONTH)
AND NOT EXISTS (
SELECT 1 FROM ohrm_attendance_record WHERE DATE_FORMAT(ohrm_attendance_record.punch_in_user_time, '%d-%b-%Y') = DATE_FORMAT(ohrm_attendance_master.punch_time, '%d-%b-%Y'))
AND NOT EXISTS (
SELECT 1 FROM ohrm_attendance_record WHERE DATE_FORMAT(ohrm_attendance_record.punch_out_user_time, '%d-%b-%Y') = DATE_FORMAT(punch_time,'%d-%b-%Y'));
END
Hi all i execute this query to get a table where there's statistics of some database information.. i'd like to intialise the fields that don't exist ( because the query is executed in different dates and sometimes there's a day where there's nothing ) so i'd like it to return 0 and NULL ( in TOP column )
SELECT
SUM(IF(`TOP` = 'one',`Nb`,0)) as first_one,
SUM(IF(`TOP` = 'two',`Nb`,0)) as second_one,
SUM(IF(`TOP` = 'three',`Nb`,0)) as thrid_one,
SUM(IF(`TOP` NOT IN ('three','two','one'),`Nb`,0)) as forth_one,
GROUP_CONCAT(IF(`TOP` NOT IN ('three','two','one'),`TOP`,'') SEPARATOR '') as `OR`
FROM (
SELECT
COUNT(*) as Nb,
'one' as `TOP`
FROM
mytable
WHERE
TYPE = 'MSS'
AND YEAR(date) = YEAR(CURDATE())
AND MONTH(date) = MONTH(CURDATE())
UNION ALL
SELECT
COUNT(*) as Nb,
'two' as `TOP`
FROM
mytable
WHERE
TYPE = 'MSS'
AND S=0
AND YEAR(date) = YEAR(CURDATE())
AND MONTH(date) = MONTH(CURDATE())
UNION ALL
SELECT
COUNT(*) as Nb,
'three' as `TOP`
FROM
mytable
WHERE
TYPE = 'MSS'
AND S<>0
AND YEAR(date) = YEAR(CURDATE())
AND MONTH(date) = MONTH(CURDATE())
UNION ALL
SELECT
`Nb`,
`TOP`
FROM(
SELECT
COUNT(*) as Nb ,
`OR` as `TOP`
FROM
mytable
WHERE
TYPE = 'MSS'
AND YEAR(date) = YEAR(CURDATE())
AND MONTH(date) = MONTH(CURDATE())
GROUP BY
`OR`
ORDER BY
Nb DESC
LIMIT 1
) as tmp
)as tmp1
Assuming that in tmp1 you have data you need but with "gaps" (days when there were no data at all) you could RIGHT JOIN tmp1 to table tmp2 using day (I assume that you have such column in tmp1 table). So tmp2 would be just list of days:
SELECT '2013-05-17' as day UNION SELECT '2013-05-18' UNION SELECT ...
I could elaborate my answer if you'd like to provide your DB schema.
You can replace each subquery with:
SELECT
IFNULL(tmp.Nb,0) as Nb,
IFNULL(tmp.`TOP`, 'value') as `TOP`
FROM (
--subquery
) as tmp
Example for the first subquery:
SELECT
IFNULL(tmp.Nb,0) as Nb,
IFNULL(tmp.`TOP`, 'one') as `TOP`
FROM (
SELECT
COUNT(*) as Nb,
'one' as `TOP`
FROM
mytable
WHERE
TYPE = 'MSS'
AND YEAR(date) = YEAR(CURDATE())
AND MONTH(date) = MONTH(CURDATE())
) as tmp
SQL is good at grouping existing entities into categories, but bad at "creating" entities itself. I would advise either a generic number table (really just the numbers from 0 to a few hundredthousand) if you have also non-date categories or as Wiktor suggested a date-Table which gets filled every now and then and has the next few years as well as the time since your program is working.
With a date table
list_dates (
id int(11) not null primary key auto_increment,
dateval date not null
)
you could start your queries from that table (with a reasonable range, of course) and count every thing else:
select list_dates.dateval as date, count(*) as cnt
from list_dates
left join actions on actions.actiontime >= (cast list_dates.date_val as datetime)
and actions.actiontime < (cast list_dates.date_val `interval 1 day as datetime)
where list_dates.dateval between '$fromDate' and '$toDate'
group by list_dates.dateval
;
or starting with a number table numbers
select $fromDate + interval numbers.number day as date, count(*) as cnt
from numbers
left join actions
on actions.actiontime >= (cast $fromDate + interval numbers.number day as datetime)
and actions.actiontime < (cast $fromDate + interval (1 + numbers.number) day as datetime)
where numbers.number >= 0 and numbers.number < $countDates
group by numbers.number
;
One Day
If you really want just that one day (today) then you can of course use a anonymous subselect- Table instead, so it becomes
select list_dates.dateval as date, count(*) as cnt
from ( select curdate() as dateval ) as list_dates
left join actions on actions.actiontime >= (cast list_dates.date_val as datetime)
and actions.actiontime < (cast list_dates.date_val `interval 1 day as datetime)
where list_dates.dateval between '$fromDate' and '$toDate'
group by list_dates.dateval
;