I'm having some trouble with my following query:
SELECT DATE_FORMAT(date, "%Y-%m-%d") date, id, c, FromWhere FROM (
SELECT distinct cl.creationtime date, c.id, 1 c, "Created" as FromWhere -- Current amount of contacts in list
FROM `contactsLists` cl, `contacts` c
WHERE cl.contact=c.id AND c.optin="yes" AND c.restriction=0 AND cl.list = 13
AND cl.creationtime > DATE_SUB(now(), interval 13 MONTH)
UNION
SELECT IFNULL(cl.edittime, NOW()) date, c.id, -1 c, "Deleted from list" as FromWhere -- Deleted from list
FROM `contactsLists` cl, `contacts` c
WHERE cl.contact=c.id AND c.optin="yes" AND c.restriction=0 AND cl.deleted=1 AND cl.list = 13
AND IFNULL(cl.edittime, NOW()) > DATE_SUB(now(), interval 13 MONTH)
UNION
SELECT o.optintime date, c.id, 1 c, "Optin added" as FromWhere -- Optins
FROM `contactsLists` cl, `contacts` c, `optin` o
WHERE cl.contact=c.id AND c.email = o.email AND o.reply="yes" AND c.restriction=0 AND cl.list = 13
AND o.optintime > DATE_SUB(now(), interval 13 MONTH)
UNION
SELECT o.optintime date, c.id, -1 c, "Optout added" as FromWhere -- Optouts
FROM `contactsLists` cl, `contacts` c, `optin` o
WHERE cl.contact=c.id AND c.email = o.email AND o.reply="no" AND c.restriction=0 AND cl.list = 13
AND o.optintime > DATE_SUB(now(), interval 13 MONTH)
ORDER BY date) u;
The problem is that the last 2 unions "Optins and Optouts" are reading the optin status o.reply="no"/"yes". But it reads the statement of the situation as now(). It should be read the optin reply while also checking the date at the same time from the o.optintime table. Is it possible todo so?
Everything else works in the query except for that. The idea of the graph is to show the growth or loss of a client database list.
Related
I've a mysql table with me
Now we want to do some calculations like this
count date wise for all courses enrolled
count where course id = 2 for date > start_date AND date < end_date
Expected output where we calculate all courses enrolled
Expected output where we calculate all courses enrolled where course id = 2
*
expected output where course_id = 2 AND date range is between 2022-11-15 to 2022-11-13
The query which I've right now
SELECT COUNT(*), DATE(registered_on)
FROM courses_enrolled
WHERE course_id = 1
GROUP BY DATE(registered_on), course_id
ORDER BY registered_on desc;
You need to use some kind of calendar table approach here:
SELECT d.dt AS date, COUNT(ce.id) AS cnt
FROM (
SELECT '2022-11-12' AS dt UNION ALL
SELECT '2022-11-13' UNION ALL
SELECT '2022-11-14' UNION ALL
SELECT '2022-11-15'
) d
LEFT JOIN courses_enrolled ce
ON DATE(ce.registered_on) = d.dt AND
ce.course_id = 2
GROUP BY d.dt
ORDER BY d.dt;
The calendar table ensures that all dates you want in the output appear. In practice, you may replace the subquery in d with a bona-fide table containing all dates of interest. The left join ensures that no dates are dropped which have no matching courses on that day.
If you are using MySQL 8 you can use a recursive CTE to create your date range.
For all enrolled courses for given date range -
WITH RECURSIVE calendar (date) AS (
SELECT '2022-11-13' # start date
UNION ALL
SELECT date + INTERVAL 1 DAY FROM calendar
WHERE date + INTERVAL 1 DAY <= '2022-11-15' # end date
)
SELECT COUNT(ce.id) count_all, c.date
FROM calendar c
LEFT JOIN courses_enrolled ce
ON ce.registered_on BETWEEN c.date AND (c.date + INTERVAL 1 DAY - INTERVAL 1 SECOND)
GROUP BY c.date
ORDER BY c.date DESC;
Note the use of BETWEEN start AND end of day in the join criteria. For a small dataset this offers negligible benefit but on a large dataset it would allow for use of an index on registered_on, which could offer significantly improved performance.
Or for just the selected course -
WITH RECURSIVE calendar (date) AS (
SELECT '2022-11-13' # start date
UNION ALL
SELECT date + INTERVAL 1 DAY FROM calendar
WHERE date + INTERVAL 1 DAY <= '2022-11-15' # end date
)
SELECT COUNT(ce.id) count_selected_course, c.date
FROM calendar c
LEFT JOIN courses_enrolled ce
ON ce.registered_on BETWEEN c.date AND (c.date + INTERVAL 1 DAY - INTERVAL 1 SECOND)
AND ce.course_id = 2
GROUP BY c.date
ORDER BY c.date DESC;
Or counting both at the same time -
WITH RECURSIVE calendar (date) AS (
SELECT '2022-11-13' # start date
UNION ALL
SELECT date + INTERVAL 1 DAY FROM calendar
WHERE date + INTERVAL 1 DAY <= '2022-11-15' # end date
)
SELECT COUNT(ce.id) count_all, COUNT(IF(ce.course_id = 2, ce.id, NULL)) count_selected_course, c.date
FROM calendar c
LEFT JOIN courses_enrolled ce
ON ce.registered_on BETWEEN c.date AND (c.date + INTERVAL 1 DAY - INTERVAL 1 SECOND)
GROUP BY c.date
ORDER BY c.date DESC;
I have a table like this two
Table A
date amount B_id
'2020-1-01' 3000000 1
'2019-8-01' 15012 1
'2019-6-21' 90909 1
'2020-1-15' 84562 1
--------
Table B
id type
1 7
2 5
I have to show sum of amount until the last date of each month for the last 12 month.
The query i have prepared is like this..
SELECT num2.last_dates,
(SELECT SUM(amount) FROM A
INNER JOIN B ON A.B_id = B.id
WHERE B.type = 7 AND A.date<=num2.last_dates
),
(SELECT SUM(amount) FROM A
INNER JOIN B ON A.B_id = B.id
WHERE B.type = 5 AND A.date<=num2.last_dates)
FROM
(SELECT last_dates
FROM (
SELECT LAST_DAY(CURDATE() - INTERVAL CUSTOM_MONTH MONTH) last_dates
FROM(
SELECT 1 CUSTOM_MONTH UNION
SELECT 0 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 )num
) num1
)num2
ORDER BY num2.last_dates
This gives me the result like this which is exactly how i need it. I need this query to execute faster. Is there any better way to do what i am trying to do?
2019-05-31 33488.69 109.127800
2019-06-30 263.690 1248932.227800
2019-07-31 274.690 131.827800
2019-08-31 627.690 13.687800
2019-09-30 1533.370000 08.347800
2019-10-31 1444.370000 01.327800
2019-11-30 5448.370000 247.227800
2019-12-31 61971.370000 016.990450
2020-01-31 19550.370000 2535.185450
2020-02-29 986.370000 405.123300
2020-03-31 1152.370000 26.793300
2020-04-30 9404.370000 11894.683300
2020-05-31 3404.370000 17894.683300
I'd use conditional aggregation, and pre-aggregate the monthly totals in one pass, instead of doing twenty-six individual passes repeatedly through the same data.
I'd start with something like this:
SELECT CASE WHEN A.date < DATE(NOW()) + INTERVAL -14 MONTH
THEN LAST_DAY( DATE(NOW()) + INTERVAL -14 MONTH )
ELSE LAST_DAY( A.date )
END AS _month_end
, SUM(IF( B.type = 5 , B.amount , NULL)) AS tot_type_5
, SUM(IF( B.type = 7 , B.amount , NULL)) AS tot_type_7
FROM A
JOIN B
ON B.id = A.B_id
WHERE B.type IN (5,7)
GROUP
BY _month_end
(column amount isn't qualified in original query, so just guessing here which table that is from. adjust as necessary. best practice is to qualify all column references.
That gets us the subtotals for each month, in a single pass through A and B.
We can get that query tested and tuned.
Then we can incorporate that as an inline view in an outer query which adds up those monthly totals. (I'd do an outer join, just in case rows are missing, sow we don't wind up omitting rows.)
Something like this:
SELECT d.dt + INTERVAL -i.n MONTH + INTERVAL -1 DAY AS last_date
, SUM(IFNULL(t.tot_type_5,0)) AS rt_type_5
, SUM(IFNULL(t.tot_type_7,0)) AS rt_type_7
FROM ( -- first day of next month
SELECT DATE(NOW()) + INTERVAL -DAY(DATE(NOW()))+1 DAY + INTERVAL 1 MONTH AS dt
) d
CROSS
JOIN ( -- thirteen integers, integers 0 thru 12
SELECT 0 AS 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 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12
) i
LEFT
JOIN ( -- totals by month
SELECT CASE WHEN A.date < DATE(NOW()) + INTERVAL -14 MONTH
THEN LAST_DAY( DATE(NOW()) + INTERVAL -14 MONTH )
ELSE LAST_DAY( A.date )
END AS _month_end
, SUM(IF( B.type = 5 , B.amount , NULL)) AS tot_type_5
, SUM(IF( B.type = 7 , B.amount , NULL)) AS tot_type_7
FROM A
JOIN B
ON B.id = A.B_id
WHERE B.type IN (5,7)
GROUP
BY _month_end
) t
ON t._month_end < d.dt
GROUP BY d.dt + INTERVAL -i.n MONTH + INTERVAL -1 DAY
ORDER BY d.dt + INTERVAL -i.n MONTH + INTERVAL -1 DAY DESC
The design is meant to do one swoop through the A JOIN B set. We're expecting to get about 14 rows back. And we're doing a semi-join, duplicating the oldest months multiple times, so approx . 14 x 13 / 2 = 91 rows, that get collapsed into 13 rows.
The big rock in terms of performance is going to be materializing that inline view query.
This is how I'd probably approach this in MySQL 8 with SUM OVER:
Get the last 12 months.
Use these months to add empty month rows to the original data, as MySQL doesn't support full outer joins.
Get the running totals for all months.
Show only the last twelve months.
The query:
with months (date) as
(
select last_day(current_date - interval 1 month) union all
select last_day(current_date - interval 2 month) union all
select last_day(current_date - interval 3 month) union all
select last_day(current_date - interval 4 month) union all
select last_day(current_date - interval 5 month) union all
select last_day(current_date - interval 6 month) union all
select last_day(current_date - interval 7 month) union all
select last_day(current_date - interval 8 month) union all
select last_day(current_date - interval 9 month) union all
select last_day(current_date - interval 10 month) union all
select last_day(current_date - interval 11 month) union all
select last_day(current_date - interval 12 month)
)
, data (date, amount, type) as
(
select last_day(a.date), a.amount, b.type
from a
join b on b.id = a.b_id
where b.type in (5, 7)
union all
select date, null, null from months
)
select
date,
sum(sum(case when type = 5 then amount end)) over (order by date) as t5,
sum(sum(case when type = 7 then amount end)) over (order by date) as t7
from data
group by date
order by date
limit 12;
Demo: https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=ddeb3ab3e086bfc182f0503615fba74b
I don't know whether this is faster than your own query or not. Just give it a try. (You'd get my query much faster by adding a generated column for last_day(date) to your table and use this. If you need this often, this may be an option.)
You are getting some complicated answers. I think it is easier. Start with knowing we can easily sum for each month:
SELECT SUM(amount) as monthtotal,
type,
MONTH(date) as month,
YEAR(date) as year
FROM A LEFT JOIN B on A.B_id=B.id
GROUP BY type,month,year
From that data, we can use a variable to get running total. Best to do by initializing the variable, but not necessary. We can get the data necessary like this
SET #running := 0;
SELECT (#running := #running + monthtotal) as running, type, LAST_DAY(CONCAT(year,'-',month,'-',1))
FROM
(SELECT SUM(amount) as monthtotal,type,MONTH(date) as month,YEAR(date) as year FROM A LEFT JOIN B on A.B_id=B.id GROUP BY type,month,year) AS totals
ORDER BY year,month
You really need to have a connector that supports multiple statements, or make multiple calls to initialize the variable. Although you can null check the variable and default to 0, you still have an issue if you run the query a second time.
Last thing, if you really want the types to be summed separately:
SET #running5 := 0;
SET #running7 := 0;
SELECT
LAST_DAY(CONCAT(year,'-',month,'-',1)),
(#running5 := #running5 + (CASE WHEN type=5 THEN monthtotal ELSE 0 END)) as running5,
(#running7 := #running7 + (CASE WHEN type=7 THEN monthtotal ELSE 0 END)) as running7
FROM
(SELECT SUM(amount) as monthtotal,type,MONTH(date) as month,YEAR(date) as year FROM A LEFT JOIN B on A.B_id=B.id GROUP BY type,month,year) AS totals
ORDER BY year,month
We still don't show months where there is no data. I'm not sure that is a requirement. But this should only need one pass of table A.
Also, make sure the id on table B is indexed.
Date Range is last seven days. currently i am getting this data from this query
counts dates
1 2018-12-25
1 2018-12-26
3 2018-12-30
query is
select COALESCE(Count(campaign_id), 0) as counts,date(start_date) as dates from campaigns where segment_id=30
and date(start_date) BETWEEN DATE_SUB(CURDATE(),INTERVAL 7 DAY) AND CURDATE()
group by date(start_date)
but i want expected output is
counts dates
0 2018-12-24
1 2018-12-25
1 2018-12-26
0 2018-12-27
0 2018-12-28
0 2018-12-29
3 2018-12-30
You can generate 7 rows by using information_schema's views such as information_schema.tables
select (select count(*)
from campaigns
where start_date = e.dates
) count,
e.dates
from
(
select *
from campaigns c
right join
(
SELECT #cr := #cr + 1 as rn,
date_sub(curdate(), interval 7 - #cr day) as dates
from information_schema.tables c1
cross join (SELECT #cr := 0, #segID := 30) r
where #cr<7
) d on c.campaign_id = d.rn
where coalesce(c.segment_id,#segID) = #segID
) e;
count dates
0 24.12.2018
1 25.12.2018
1 26.12.2018
0 27.12.2018
0 28.12.2018
0 29.12.2018
3 30.12.2018
Rextester Demo
You need a way of generating all the dates. The standard answer is to use a left join and a calendar table or other table with the dates you want.
In your case, your table may already have the all the dates. If so, the simplest method is conditional aggregation:
select date(start_date) as dates,
sum(segment_id = 30) cnt_30
from campaigns
where start_date >= date_sub(curdate(), interval 7 day) and
start_date < date_add(curdate(), interval 1 day)
group by date(start_date);
You'll notice that I also modified the where clause, by removing the function calls on start_date. This allows the MySQL optimizer to use an index, if one is available.
I'm trying to find an answer to the following query:
A customer wants a single room for three consecutive nights. Find the first available date in December 2016.
As per the question, this should be the right answer. But I don't know how to solve it.
+-----+------------+
| id | MIN(i) |
+-----+------------+
| 201 | 2016-12-11 |
+-----+------------+
The link is from question number 14 here.
This is the ER diagram of the database:
I apologize that I'm a bit rusty with this kind of query and I can't guarantee that I got all of the syntax correct, but I think that something like the following might work:
SELECT id, DATE_ADD(b.booking_date, INTERVAL (end_date + 1 DAY) as date
FROM (
SELECT r.id, STR_TO_DATE('2016-01-01', '%Y-%m-%d') as start_of_month, b.booking_date as start_date, DATE_ADD(b.booking_date, INTERVAL (nights - 1) DAY) as end_date
FROM room r
LEFT JOIN booking b ON r.id = b.room_no
ORDER BY r.id, b.booking_date
) as room_bookings
WHERE DATE_DIFF(room_bookings.start_of_month, room_bookings.start_date) >= 3
OR DATE_DIFF(room_bookings.end_date, (
SELECT b2.booking_date FROM booking b2
WHERE b2.room_no = room_bookings.id AND b2.booking_date > room_bookings.start_date
ORDER BY b2.booking_date LIMIT 1)
) >= 3
In fact, now that I type that all out, you might be able to tweak the WHERE of the main query so that you don't even need the room_bookings subselect. Hopefully this helps and isn't too far off the mark.
This seems very hard to do without a calendar table -- because an appropriate room might have no booking at all during the month. Without any booking, there is no record in the month to start with.
select r.id, dte
from rooms r cross join
(select date('2018-12-01') as dte union all
select date('2018-12-02') as dte union all
. . .
select date('2018-12-32') as dte
) d
where not exists (select 1 from bookings b where b.room_no = r.id and b.booking_date = d.dte) and
not exists (select 1 from bookings b where b.room_no = r.id and b.booking_date = d.dte + interval 1 day) and
not exists (select 1 from bookings b where b.room_no = r.id and b.booking_date = d.dte + interval 2 day)
order by d.dte
limit 1;
This assumes that booking_date is the start of the stay. You need to provide the logic for a "single room".
select distinct top 1 alll.i,alll.room_no,
case
when (select count(*) from booking where room_no = alll.room_no and booking_date between dateadd(day,1,alll.i) and dateadd(day,3,alll.i)) > 0 then 'Y'
else 'N'
end as av3
from
(select c.i,b.room_no,b.booking_date
from calendar c cross join booking b
where month(c.i) = 12 and year(c.i) = 2016 and b.room_type_requested = 'single'
) as alll
join
(
select distinct c.i, b.room_no
from calendar c join booking b
on c.i between b.booking_date and DATEADD(day,b.nights-1,b.booking_date)
where month(c.i) = 12 and year(c.i) = 2016 and b.room_type_requested = 'single'
) as booked
on alll.i = booked.i
and alll.room_no <> booked.room_no
order by 1
This works. It is a little complicated but basically first checks all the rooms that are booked and then does a comparison between rooms not booked on each day of the month till the next 3 days.
My solution is separate problem into 2 parts (in the end was 2 queries joined together). May not be the most efficient but the solution is correct.
1) Of the single rooms, look at the last check-out date, and see which one is vacant first (i.e. no more bookings for the rest of the month)
2) check in between current reservations - and see if there's a 3 day gap between them
3) join those together - grab the min
WITH subquery AS( -- existing single-bed bookings in Dec
SELECT room_no, booking_date,
DATE_ADD(booking_date, INTERVAL (nights-1) DAY) AS last_night
FROM booking
WHERE room_type_requested='single' AND
DATE_ADD(booking_date, INTERVAL (nights-1) DAY)>='2016-12-1' AND
booking_date <='2016-12-31'
ORDER BY room_no, last_night)
SELECT room_no, MIN(first_avail) AS first_avail --3) join the 2 together
FROM(
-- 1) check the last date the room is booked in December (available after)
SELECT room_no, MIN(first_avail) AS first_avail
FROM(
SELECT room_no, DATE_ADD(MAX(last_night), INTERVAL 1 DAY) AS first_avail
FROM subquery q3
GROUP BY 1
ORDER BY 2) AS t2
UNION
-- 2) check if any 3-day exist in between reservations
SELECT room_no, DATE_ADD(MIN(end2), INTERVAL 1 DAY) AS first_avail
FROM(
SELECT q1.booking_date AS beg1, q1.room_no, q1.last_night AS end1,
q2.booking_date AS beg2, q2.last_night AS end2
FROM subquery q1
JOIN subquery q2
ON q1.room_no = q2.room_no AND q2.booking_date > q1.last_night
GROUP BY 2,1
ORDER BY 2,1) AS t
WHERE beg2-end1 > 3) AS inner_t
This works conceptually as the first avaiable date should always be the end of the previous booking.
SELECT MIN(DATE_ADD(a.booking_date, INTERVAL nights DAY)) AS i
FROM booking AS a
WHERE DATE_ADD(a.booking_date, INTERVAL nights DAY)
>= '2016-12-01'
AND room_type_requested = 'single'
AND NOT EXISTS
(SELECT 1 FROM booking AS b
WHERE b.booking_date BETWEEN
DATE_ADD(a.booking_date, INTERVAL nights DAY)
AND DATE_ADD(a.booking_date, INTERVAL nights+2 DAY)
AND a.room_no = b.room_no)
After several hours of searching I'm finally giving in and asking for help. I've found something that gets me close, but doesn't seem to be working properly.
Basically looking for users who uses us between 3-6 months ago, but haven't in the last 3 months.
For the sake of the example lets say I have two tables named event(id, clientFK, insertion_date, officeFK) and client(id, name):
SELECT DISTINCT e.id, e.insertion_date, c.name
FROM event e
LEFT JOIN client c ON c.id = e.clientFK
WHERE e.id NOT IN (
SELECT e.id
FROM event e
WHERE e.insertion_date
BETWEEN (
NOW( ) - INTERVAL 3
MONTH
)
AND NOW( )
)
AND e.insertion_date > ( NOW( ) - INTERVAL 6
MONTH )
AND officeFK =1
ORDER BY e.insertion_date DESC
**UPDATE***
id is just an auto incrementing column so obviously it would never show up using the logic above. I meant to search for the clientFK in the event table. Based off the first response below I came up with this:
SELECT DISTINCT e.clientFK, e.insertion_date, c.name
FROM event e
LEFT JOIN client c ON c.id = e.clientFK
WHERE e.clientFK NOT IN (
SELECT e.clientFK FROM event e
WHERE e.insertion_date > (NOW() - INTERVAL 3 DAY)
)
AND e.insertion_date BETWEEN (NOW() - INTERVAL 3 DAY) AND (NOW() - INTERVAL 6 MONTH)
AND officeFK =1
ORDER BY e.insertion_date DESC
However, even when narrowing the NOT IN subquery to just 3 days I'm still returning 0 rows. Changing NOT IN to IN also results in 0 rows returned. There are thousands of rows in my events table where the clientFK is "NOT IN" the last 3 days. I've got to be doing something wrong somewhere.
CORRECT CODE BELOW:
SELECT DISTINCT e.id, e.insertion_date, c.name
FROM event e
JOIN client c ON c.id = e.clientFK
WHERE e.clientFK NOT IN (
SELECT e.clientFK FROM event e
WHERE e.insertion_date > (NOW() - INTERVAL 3 MONTH)
)
AND e.insertion_date < (NOW() - INTERVAL 3 MONTH)
AND insertion_date > (NOW() - INTERVAL 6 MONTH)
AND e.officeFK = 1
ORDER BY e.insertion_date DESC
There are several ways to do this. But I think you're close. You don't seem to have the date range correct for 3-6 months in your primary query. You were checking for anything from 6 months until now. That doesn't match the spec of the OP - users of an event 3-6 months ago, but not within the last 3 months. Logically it seems the same, but try this.
SELECT DISTINCT e.id, e.insertion_date, c.name
FROM event e
JOIN client c ON c.id = e.clientFK
WHERE e.clientFK NOT IN (
SELECT e.clientFK FROM event e
WHERE e.insertion_date > (NOW() - INTERVAL 3 MONTH)
)
AND e.insertion_date BETWEEN (NOW() - INTERVAL 3 MONTH) AND (NOW() - INTERVAL 6 MONTH)
AND e.officeFK = 1
ORDER BY e.insertion_date DESC