Query returns null instead of 0 [duplicate] - mysql

This question already has answers here:
How to generate data in MySQL?
(4 answers)
Closed 8 years ago.
I have a query that counts all Work ID Numbers by month and year and then creates a chart for the past 13 months using jpgraph. It works great except there are no Work ID Numbers in July so the chart totally skips July.
Query Results:
5
16
15
11
3
12
4
8
2
9
13
12
Desired Results:
5
16
15
11
3
12
0
4
8
2
9
13
12
As you can see I need the (0) zero in order for my chart to work, however since there are no Work ID Number in July my query simply skips it. Here is my query:
SELECT COUNT( WORK_ID_NUM ) AS count,
DATE FROM SERVICE_JOBS
WHERE (DATE BETWEEN '$lastyear' AND '$date')
AND JOB_TYPE LIKE 'Street Light'
GROUP BY YEAR( DATE ), MONTH( DATE )
ORDER BY DATE

sqlFiddle Demo
SELECT IFNULL(count,0) as count,theDate as Date
FROM
(SELECT #month := #month+INTERVAL 1 MONTH as theDate
FROM service_jobs,(SELECT #month:='$lastyear' - INTERVAL 1 MONTH)as T1
LIMIT 13)as T2
LEFT JOIN
(SELECT COUNT(WORK_ID_NUM)as count,DATE
FROM service_jobs
WHERE (DATE BETWEEN '$lastyear' AND '$date')
AND JOB_TYPE LIKE 'Street Light'
GROUP BY YEAR(DATE), MONTH(DATE)) T3
ON (YEAR(theDate) = YEAR(DATE) AND MONTH(theDate) = MONTH(DATE))
ORDER BY theDate;

To get your query to return a row for July, you need to have a row with July in it. You could create a table with all the dates between $lastyear and $date in it and then outer join from that to SERVICE_JOB.
SELECT COUNT( WORK_ID_NUM ) AS count,
allDates.DATE
FROM AllDates
Left outer join SERVICE_JOB
on AllDates.DATE = SERVICE_JOB.DATE
WHERE (AllDates.DATE BETWEEN '$lastyear' AND '$date') AND
(SERVICE_JOB.WORK_ID_NUM is NULL OR JOB_TYPE LIKE 'Street Light')
GROUP BY YEAR( AllDates.DATE ), MONTH( AllDates.DATE )
ORDER BY AllDates.DATE
In SQL Server it would be pretty easy to make a Common Table Expression that could fill AllDates for you based on $lastyear and $date. Not sure about MySql.

Related

MySQL_Retrieving stock price based on max and min_date

I am trying to retrieve closed and opened stock prices from the first and last date per month.
For some reason, the output of the 'end_date_closed_stock_price' is NULL.
Do you know any idea why it is giving this result?
Also, could you tell me the appropriate queries to retrieve the last date of the month?
The followings are my queries and output.
Thanks in advance!
SELECT YEAR(date) AS years
, MONTH(date) AS months
, CASE WHEN date = MAX(date) THEN close END end_date_closed_stock_price
, CASE WHEN date = MIN(date) THEN open END first_date_opened_stock_price
FROM nasdaq_feb_25_1971_feb_25_2021
GROUP
BY 1,2
ORDER
BY 1 DESC;
---OUTPUT---
2020 5 NULL 9382.349609
2019 1 NULL 6947.459961
2019 2 NULL 7266.279785
2019 3 NULL 7582.290039
There is probably a more efficient solution, but this should work:
WITH dates as
(SELECT YEAR(date) as years
,MONTH(date) AS months
,MAX(date) as end_date
,MIN(date) as first_date
FROM nasdaq_feb_25_1971_feb_25_2021
GROUP BY 1, 2)
SELECT dates.years
, dates.months
, price1.close as end_date_closed_stock_price
, price2.open as first_date_opened_stock_price
FROM dates
JOIN nasdaq_feb_25_1971_feb_25_2021 price1
ON price1.date = dates.end_date
JOIN nasdaq_feb_25_1971_feb_25_2021 price2
ON price2.date = dates.first_date
ORDER
BY 1 DESC, 2 DESC;

MYSQL SUM until last day of Each month for last 12 months

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.

How to get the counts of last 3 months with month names, if no record for that month need to get 0 with month name [duplicate]

This question already has answers here:
MySQL monthly Sale of last 12 months including months with no Sale
(3 answers)
Get Record Per Month But Also Get Zero If No Records That Month
(2 answers)
Mysql to select month-wise record even if data not exist
(2 answers)
MySQL query to get last 12 months data grouped by month including zero counts
(1 answer)
Closed 4 years ago.
I want get the last 3 months data from current date then i will get four month names in that i have only 3 month data but i do not have one month data in middle.
SELECT
count(v.visit_id) as count,
MONTHNAME(v.updated) AS Month_name
FROM patient_status as p, visit_history_details as v
WHERE v.visit_id = p.visit_id and v.hospital_code = 'id'
and p.doctor_id = '2' and v.updated >= now()-interval 3 month
GROUP by Month_name
My Result are coming like this:
January 10
December 12
October 10
But I want this result:
January 10
December 12
November 0
October 10
please help me how to solve this issue
For the month you should use a subquery for get all the month you need:
select monthname(my_date), ifnull(count, 0)
from (
select str_to_date('2018-01-01', '%Y-%m-%d') as my_date
union
select str_to_date('2018-10-01', '%Y-%m-%d')
union
select str_to_date('2018-11-01', '%Y-%m-%d')
union
select str_to_date('2018-12-01', '%Y-%m-%d')
) t
left join (
SELECT
count(v.visit_id) as count,
MONTHNAME(v.updated) AS Month_name,
MONTH(v.updated) as my_month
FROM patient_status as p
INNER JOIN visit_history_details as v
ON v.visit_id = p.visit_id
WHERE v.hospital_code = 'id'
and p.doctor_id = '2'
and v.updated >= now()-interval 3 month
GROUP by Month_name
) r on r.my_month = t.my_month
And you should avoid old implicit join syntax and use the explicit syntax.
Or as suggested by Salman A for a more general solution:
select monthname(my_date), ifnull(count, 0)
from (
select LAST_DAY(CURRENT_TIMESTAMP) + INTERVAL 1 DAY - INTERVAL 1 MONTH as my_date
union
select LAST_DAY(CURRENT_TIMESTAMP) + INTERVAL 1 DAY - INTERVAL 2 MONTH
union
select LAST_DAY(CURRENT_TIMESTAMP) + INTERVAL 1 DAY - INTERVAL 3 MONTH
union
select LAST_DAY(CURRENT_TIMESTAMP) + INTERVAL 1 DAY - INTERVAL 4 MONTH
) t
left join (
SELECT
count(v.visit_id) as count,
MONTHNAME(v.updated) AS Month_name,
MONTH(v.updated) as my_month
FROM patient_status as p
INNER JOIN visit_history_details as v
ON v.visit_id = p.visit_id
WHERE v.hospital_code = 'id'
and p.doctor_id = '2'
and v.updated >= now()-interval 3 month
WHERE v.visit_id = p.visit_id and v.hospital_code = 'id'
and p.doctor_id = '2' and v.updated >= now()-interval 3 month
GROUP by Month_name

Aggregating table data in MySQL, is there an easier way to do this?

I'm trying to write a query that aggregates data from a table.
Essentially I have a long list of devices that have been inventoried and eventually installed over the last couple of years.
I want to find the average amount of time between when the device was received and when it was installed, and then have that data sorted by the month the device was installed. BUT in each month's row, I also want to include the data from the previous months.
So essentially what I want to see is: (sorry for terrible formatting)
MonthInstalled | TimeToInstall | Total#Devices
-----------------+---------------+----------------------------
Jan | 10 Days | 5
Feb(=Jan+Feb) | 15 Days | 18 (5 in Jan + 13 in Feb)
Mar(=Jan+Feb+Mar)| 13 Days | 25 (5 + 13 + 7)
...
The query I currently have written looks like this:
INSERT INTO DevicesInstall
SELECT ROUND(AVG(DATEDIFF(dvc.dt_install , dvc.dt_receive)), 1) AS 'Install',
COUNT(dvc.dvc_model) AS 'Total Devices',
MAX(dvc.dt_install) AS 'Date',
loc.loc_campus AS 'Campus'
FROM dvc_info dvc, location loc
WHERE dvc.dvc_loc_bin = loc.loc_bin
AND dvc.dt_install < '20160201'
;
Although this is functional, I have to iterate this for each month manually, so it is not scale-able. Is there a way to condense this at all?
We can return the dates using an inline view (derived table), and then join to the dvc_info table, so we can get the "cumulative" results.
To get the results for:
Jan
Jan+Feb
Jan+Feb+Mar
We need to return three copies of the rows for Jan, and two copies of the rows for Feb, and then collapse the those rows into an appropriate group.
The loc_campus is being included in the SELECT list... not clear why that is needed. If we want results "by campus", then we need to include that expression in the GROUP BY clause. Otherwise, the value returned for that non-aggregate is indeterminate... we will get a value for some row "in the group", but it could be any row.
Something like this:
SELECT d.dt AS `before_date`
, loc.loc_campus AS `Campus`
, ROUND(AVG(DATEDIFF(dvc.dt_install,dvc.dt_receive)),1) AS `Install`
, COUNT(dvc.dvc_model) AS `Total Devices`
, MAX(dvc.dt_install) AS `latest_dt_install`
FROM ( SELECT '2016-01-01' + INTERVAL 1 MONTH AS dt
UNION ALL SELECT '2016-01-01' + INTERVAL 2 MONTH
UNION ALL SELECT '2016-01-01' + INTERVAL 3 MONTH
UNION ALL SELECT '2016-01-01' + INTERVAL 4 MONTH
UNION ALL SELECT '2016-01-01' + INTERVAL 5 MONTH
UNION ALL SELECT '2016-01-01' + INTERVAL 6 MONTH
UNION ALL SELECT '2016-01-01' + INTERVAL 7 MONTH
UNION ALL SELECT '2016-01-01' + INTERVAL 8 MONTH
UNION ALL SELECT '2016-01-01' + INTERVAL 9 MONTH
UNION ALL SELECT '2016-01-01' + INTERVAL 10 MONTH
UNION ALL SELECT '2016-01-01' + INTERVAL 11 MONTH
UNION ALL SELECT '2016-01-01' + INTERVAL 12 MONTH
) d
CROSS
JOIN location loc
LEFT
JOIN dvc_info dvc
ON dvc.dvc_loc_bin = loc.loc_bin
AND dvc.dt_install < d.dt
GROUP
BY d.dt
, loc.loc_campus
ORDER
BY d.dt
, loc.loc_campus
Note that the value returned for d.dt will be the "up until" date. We're going to get '2016-02-01' returned for the January results. If we want to return a value of January date, we can use an expression in the SELECT list...
SELECT DATE_FORMAT(d.dt + INTERVAL -1 MONTH,'%Y-%m') AS `month`
Lots of options on query alternatives.
But it looks like the "big hump" is that to get cumulative results, we need to return multiple copies of the dvc_info rows, so the rows can be collapsed into each "grouping".
I recommend working on just the SELECT first. And get that tested working, before monkeying around to turn it into an INSERT ... SELECT.
FOLLOWUP
We can use any query as an inline view (derived table d) that returns a set of dates we want.
e.g.
FROM ( SELECT DATE_FORMAT(m.install_dt,'%Y-%m-01') + INTERVAL 1 MONTH AS dt
FROM dvc_install m
WHERE m.install_dt >= '2016-01-01'
GROUP BY DATE_FORMAT(m.install_dt,'%Y-%m-01') + INTERVAL 1 MONTH
) d
Note that with this approach, if there are no install_dt in February, we won't get back a row for February. Using the static UNION ALL SELECT approach allows us to get back "zero" counts, i.e. to return rows for months where there isn't an install_dt in that month. (But that's the answer to a different question... how do I get back a "zero" count for February when there aren't any rows for Februrary?)
Alternatively, if we have a calendar table e.g. cal that contains a list of the dates we want, we could just reference the table in place of the inline view, or the inline view query could get rows from that.
FROM ( SELECT cal.dt
FROM cal cal
WHERE cal.dt >= '2016-01-01'
AND cal.dt <= NOW()
AND DATE_FORMAT(cal.dt,'%d') = '01'
) d

How to get the next 12 future dates

I have a bit of code that tells you when a contact would not have been contacted and highlights that day on a calender. i.e. If you last contacted the person yesterday, then a highlighted day will appear one month from that on next months page.
What I would like to do is do that for every month up to a year. So in the previous example, if I moved one month beyond the highlighted day, it would be 'not contacted for two months', three months, four months and so fourth up to 12 months.
Here is what I'm using now for the 'not contacted for one month' query:
SELECT DATE_FORMAT(DATE(DATE_ADD(date, INTERVAL 1 MONTH)), '%Y-%c-%d') AS overDate
FROM contact_method_history
WHERE DATE(DATE_ADD(date, INTERVAL 1 MONTH)) = '$SQLDate'
AND entityRef = ".$this->entityId."
ORDER BY date DESC
LIMIT 1
$this->entityId could be something like 153 or 9045, its just a reference to the contact.
$SQLDate is in the form of '2012-05-09'.
I don't agree with comments: if you do a loop then you will run 12 queries instead of 1, which is decreasing performances. In the other hand, the statement for a "all in one" query is not trivial:
SELECT date
, i.m
, DATE_FORMAT(DATE(DATE_ADD(cmh.date, INTERVAL i.m MONTH)), '%Y-%c-%d') AS overDate
FROM contact_method_history AS cmh
, (
SELECT 1 as m
UNION ALL SELECT 2 as m
UNION ALL SELECT 3 as m
UNION ALL SELECT 4 as m
UNION ALL SELECT 5 as m
UNION ALL SELECT 6 as m
UNION ALL SELECT 7 as m
UNION ALL SELECT 8 as m
UNION ALL SELECT 9 as m
UNION ALL SELECT 10 as m
UNION ALL SELECT 11 as m
UNION ALL SELECT 12 as m
) AS i
WHERE ( DATE(DATE_ADD(cmh.date, INTERVAL i.m MONTH)) = '$SQLDate' )
AND (entityRef = ".$this->entityId.")
GROUP BY i.m, DATE(DATE_ADD(cmh.date, INTERVAL i.m MONTH))