MySql graph query multiple series aligned to same time x-axis - mysql

I have queries that I'm using to make a graph of earnings. But now people are able to earn from two different sources, so I want to separate this out into two lines on the same chart
This one for standard earnings:
SELECT DATE_FORMAT(earning_created, '%c/%e/%Y') AS day, SUM(earning_amount) AS earning_standard
FROM earnings
WHERE earning_account_id = ? AND earning_referral_id = 0 AND (earning_created > DATE_SUB(now(), INTERVAL 90 DAY))
GROUP BY DATE(earning_created)
ORDER BY earning_created
And this one for referral earnings:
SELECT DATE_FORMAT(e.earning_created, '%c/%e/%Y') AS day, SUM(e.earning_amount) AS earning_referral
FROM earnings AS e
INNER JOIN referrals AS r
ON r.referral_id = e.earning_referral_id
WHERE e.earning_account_id = ? AND e.earning_referral_id > 0 AND (e.earning_created > DATE_SUB(now(), INTERVAL 90 DAY)) AND r.referral_type = 0
GROUP BY DATE(e.earning_created)
ORDER BY e.earning_created
How do I get it to run the queries together, so that it outputs two columns/series for the y-axis: earning_standard and earning_referral.
But with them both aligned to the same day column/scale for the x-axis - substituting zero when there are no earnings for a specific series.

You'll need to set both of those queries as subqueries
SELECT DATE_FORMAT(earnings.earning_created, '%c/%e/%Y') AS day,
COALESCE(es.earning_standard, 0) AS earning_standard,
COALESCE(er.earning_referral, 0) AS earning_referral
FROM earnings
LEFT JOIN (SELECT DATE_FORMAT(earning_created, '%c/%e/%Y') AS day,
SUM(earning_amount) AS earning_standard
FROM earnings
WHERE earning_account_id = ?
AND earning_referral_id = 0
AND (earning_created > DATE_SUB(now(), INTERVAL 90 DAY))
GROUP BY DATE(earning_created)) AS es
ON (day = es.day)
LEFT JOIN (SELECT DATE_FORMAT(e.earning_created, '%c/%e/%Y') AS day,
SUM(e.earning_amount) AS earning_referral
FROM earnings AS e
INNER JOIN referrals AS r
ON r.referral_id = e.earning_referral_id
WHERE e.earning_account_id = ?
AND e.earning_referral_id > 0
AND (e.earning_created > DATE_SUB(now(), INTERVAL 90 DAY))
AND r.referral_type = 0
GROUP BY DATE(e.earning_created)) AS er
ON (day = er.day)
WHERE earnings.earning_account_id = ?
ORDER BY day
where I'm assuming earning_account_id = ? is intended to be with a question mark because the language you're using to run the query is replacing it with the actual id before running the query.

SELECT
COALESCE(t1.amount,0) AS link_earnings,
COALESCE(t2.amount,0) AS publisher_referral_earnings,
COALESCE(t3.amount,0) AS advertiser_referral_earnings,
t1.day AS day
FROM
(
SELECT DATE_FORMAT(earning_created, '%c/%e/%Y') AS day, SUM(earning_amount) AS amount
FROM earnings
WHERE earning_referral_id = 0
AND (earning_created > DATE_SUB(now(), INTERVAL 90 DAY))
AND earning_account_id = ?
GROUP BY DATE(earning_created)
) t1
LEFT JOIN
(
SELECT DATE_FORMAT(ep.earning_created, '%c/%e/%Y') AS day, (SUM(ep.earning_amount) * rp.referral_share) AS amount
FROM earnings AS ep
INNER JOIN referrals AS rp
ON ep.earning_referral_id = rp.referral_id
WHERE ep.earning_referral_id > 0
AND (ep.earning_created > DATE_SUB(now(), INTERVAL 90 DAY))
AND ep.earning_account_id = ?
AND rp.referral_type = 0
GROUP BY DATE(ep.earning_created)
) t2
ON t1.day = t2.day
LEFT JOIN
(
SELECT DATE_FORMAT(ea.earning_created, '%c/%e/%Y') AS day, (SUM(ea.earning_amount) * ra.referral_share) AS amount
FROM earnings AS ea
INNER JOIN referrals AS ra
ON ea.earning_referral_id = ra.referral_id
WHERE ea.earning_referral_id > 0
AND (ea.earning_created > DATE_SUB(now(), INTERVAL 90 DAY))
AND ea.earning_account_id = ?
AND ra.referral_type = 1
GROUP BY DATE(ea.earning_created)
) t3
ON t1.day = t3.day
ORDER BY day
Seems to run ok....

You can simply use an outer join to retain earnings even when there is no matching referral, and then conditionally sum depending on whether a referral exists or not:
SELECT DATE_FORMAT(e.earning_created, '%c/%e/%Y') AS day,
SUM(IF(r.referral_id IS NULL, e.earning_amount, 0)) earning_standard,
SUM(IF(r.referral_id IS NULL, 0, e.earning_amount)) earning_referral
FROM earnings e LEFT JOIN referrals r ON r.referral_id = e.earning_referral_id
WHERE e.earning_account_id = ?
AND e.earning_created > CURRENT_DATE - INTERVAL 90 DAY
AND (r.referral_id IS NULL OR r.referral_type = 0)
GROUP BY 1
ORDER BY 1
I've assumed here that earnings.earning_referral_id is never negative, though you can add an explicit test to filter such records if so desired.
I've also changed the filter on earnings.earning_created to base from CURRENT_DATE rather than NOW() to ensure that any earnings created earlier than the current time on the first day of the series are still included—this would typically be what one actually wants, but feel free to change back if not.

Related

How sum values in days intervals MySQL 5.7?

I have a server with MySQL 5.7.
I have two tables. First one t contains creating dates for each id. Second table t0 contains profit records day by day for each id.
I want to get columns with sums of profit for first and second 30 days for each id as well as for the first day.
SELECT t.created_at,
t.id,
sum(t1.profit) profit_1_week,
sum(t2.profit) profit_2_week,
sum(t3.profit) profit_1_day
FROM t
LEFT JOIN t0 t1 ON t.id = t.id
AND t1.inport_date BETWEEN t.created_at AND DATE_ADD(t.created_at, INTERVAL 30 DAY)
LEFT JOIN t0 t2 ON t.id = t.id
AND t2.inport_date BETWEEN DATE_ADD(t.created_at, INTERVAL 30 DAY) AND DATE_ADD(t.created_at, INTERVAL 60 DAY)
LEFT JOIN t0 t3 ON t.id = t.id
AND t3.inport_date BETWEEN t.created_at AND DATE_ADD(t.created_at, INTERVAL 1 DAY)
GROUP BY t.created_at,
t.id
ORDER BY t.created_at
This code runs but sums are wrong because sum of the first day much more then monthly. Where I'm wrong and how to fix it?
Your problem statement is not that clear, but based on your attempt, I suspect that you can do conditional aggregation:
select t.created_at, t.id,
sum(case when t0.inport_date >= t.created_at and t0.inport_date < t.created_at + interval 30 day then t0.profit else 0 end) profit_1,
sum(case when t0.inport_date >= t.created_at + interval 30 day and t0.inport_date < t.created_at + interval 60 day then t0.profit else 0 end) profit_2,
sum(case when t0.inport_date >= t.created_at and t0.inport_date < t.created_at + interval 1 day then t0.profit else 0 end) profit_3
from t
left join t0 on t0.id = t.id
group by t.created_at, t.id
order by t.created_at
The logic is to join just once, and then to use case expressions within the sum()s to choose which values should be taken into account.
I changed the date filtering logic to use half-open intervals rather than between, because it seems more relevant to me. You can adapt that as you wish according to your actual use case.

Getting the results of a query for each day in the past 30 days

Below is a query I run to extract some data in the past 24 hours.
SELECT
s.symbol,
count(cs.symbol_id) AS mentions
FROM symbols s
LEFT JOIN comments_symbols cs ON cs.symbol_id = s.id
LEFT JOIN comments c ON c.id = cs.comment_id
WHERE c.`date` > DATE_SUB(NOW(), INTERVAL 1 DAY)
GROUP BY (s.symbol)
ORDER BY mentions
DESC LIMIT 15
However, I need 24 hour intervals of data for the past 30 days in order to show a 30-day chart.
Instead of executing this query 30 times for the each day in the past 30 days, is there an approach I can take to do it with just one query execution?
It seems executing this query 30 times per page load may not be the best way to do this, no?
I hope I explained clearly, please let me know if any details are fuzzy.
Let me assume you have a list of dates. If you don't want to list them out, you can generate them:
with recursive dates as (
select curdate() - interval 30 day as dte
union all
select dte + interval 1 day
from dates
where dte < curdate()
)
Second, the LEFT JOIN seems superfluous, because you are filtering the results using LIMIT. However, I'll leave it in. Use a cross join to generate a row for each day and symbol . . . then aggregate:
SELECT s.symbol, COUNT(cs.symbol_id) AS mentions
FROM dates d CROSS JOIN
symbols s LEFT JOIN
comments_symbols cs
ON cs.symbol_id = s.id LEFT JOIN
comments c
ON c.id = cs.comment_id AND
c.date >= d.dte AND
c.date < d.date + interval 1 day
GROUP BY d.dte, s.symbol
ORDER BY d.dte, mentions DESC
Finally, to get 15 per day, let's put that into a CTE and use window functions:
WITH sm as (
SELECT d.dte, s.symbol, COUNT(cs.symbol_id) AS mentions
FROM dates d CROSS JOIN
symbols s LEFT JOIN
comments_symbols cs
ON cs.symbol_id = s.id LEFT JOIN
comments c
ON c.id = cs.comment_id AND
c.date >= d.dte AND
c.date < d.date + interval 1 day
GROUP BY d.dte, s.symbol
)
SELECT cs.*
FROM (SELECT cs.*,
ROW_NUMBER() OVER (PARTITION BY dte ORDER BY mentions DESC) as seqnum
FROM cs
) cs
WHERE seqnum <= 15;
ORDER BY dte, mentions DESC;

How to query a hotel database to return the query for a single room available for three consecutive nights?

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)

Sum over defined period of time in months

Got a good one here...
I currently have the following bit of code that basically sums the total of active accounts by month.
That is simple, however what I'm wanting to do the same thing on a per category basis and over a defined period and not just one month Ie. over 12 months.... Any ideas / assistance please....My brain is flat :(
SELECT
DATE_FORMAT(s.`DateCreated`,'%Y-%M') AS `Date Created`
, DATE_FORMAT(s.`DateEnd`,'%Y-%M') AS `Date End`
,(#csum := #csum + COUNT(DISTINCT(acc.`AccountId`))) AS Active
FROM (SELECT #csum := 0) AS csums, xx_accountdetails acc
INNER JOIN xx_services s USING(accountid)
LEFT JOIN xx_category cat ON(CategoryId)
INNER JOIN xx_products prod USING(productid)
LEFT JOIN xx_subcategory sc ON(prod.Subcategoryid = sc.SubCategoryId)
LEFT JOIN xx_invoiceline il USING(serviceid)
LEFT JOIN xx_invoices i USING(invoiceid)
WHERE DATE_FORMAT(s.DateStart,'%Y-%m') <= DATE_SUB(NOW(), INTERVAL 1 MONTH)
AND
s.`ProductId` NOT IN (4001,4002)
AND cat.`CategoryId` = '1'
AND
(DATE_FORMAT(s.`DateEnd`,'%Y-%m') >= DATE_SUB(NOW(), INTERVAL 1 MONTH)
OR
(s.`DateEnd` IS NULL
AND s.`IsActive` = 1
AND (s.`SuspendReasonId` != 3 OR s.`SuspendReasonId` IS NULL)))
GROUP BY DATE_FORMAT(s.DateCreated,'%Y-%m')
For the date range, simply change the date intervals, e.g.:
date_sub(now(), interval 12 months)
For the per categories, you need to add an extra group by statement:
group by cat.CategoryId, ...
Remove the where clause on it, too, else you'll only have CategoryId = 1.

Mysql subtracting values using row selected from min timestamp, grouping by id

I've been at this for a few hours now to no avail, pulling my hair out.
Edit: Im wanting to calculate the difference between the overall_exp column by using the same data from 1 day ago to calculate the greatest 'gain' for each user
Currently I'm take a row, then select a row from 1 day ago based on the first rows timestamp then subtract the overall_exp column from the 2 rows and order by that result whilst grouping by user_id
SQL Fiddle: http://sqlfiddle.com/#!2/501c8
Here is what i currently have, however the logic is completely wrong so im pulling 0 results
SELECT rsn, ts.timestamp, #original_ts := SUBDATE( ts.timestamp, INTERVAL 1 DAY), ts.overall_exp, ts.overall_exp - previous.overall_exp AS gained_exp
FROM tracker AS ts
INNER JOIN (
SELECT user_id, MIN( TIMESTAMP ) , overall_exp
FROM tracker
WHERE TIMESTAMP >= #original_ts
GROUP BY user_id
) previous
ON ts.user_id = previous.user_id
JOIN users
ON ts.user_id = users.id
GROUP BY ts.user_id
ORDER BY gained_exp DESC
You can do this with a self-join:
select t.user_id, max(t.overall_exp - tprev.overall_exp)
from tracker t join
tracker tprev
on tprev.user_id = t.user_id and
date(tprev.timestamp) = date(SUBDATE(t.timestamp, INTERVAL 1 DAY))
group by t.user_id
A key here is converting the timestamps to dates, so the comparison is exact.
Try:
select u.*, max(t.`timestamp`)-min(t.`timestamp`) gain
from users u
left join tracker t
on u.id = t.user_id and
t.`timestamp` >= date_sub(date(now()), interval 1 day) and
t.`timestamp` < date_add(date(now()), interval 1 day)
group by u.id
order by gain desc
SQLFiddle here.