selecting between date range and forcing empty rows to be 0? - mysql

I have a table that looks like
expires | value
-------------------
2011-06-15 | 15
2011-06-15 | 15
2011-06-25 | 15
2011-07-15 | 15
2011-07-15 | 15
2011-07-25 | 15
2011-08-15 | 15
2011-08-15 | 15
2011-08-25 | 15
I want to run a query that will spit out
June | 45
July | 45
August | 45
So my query is
SELECT SUM(amount) AS `amount`,
DATE_FORMAT(expires , '%M') AS `month`
FROM dealDollars
WHERE DATE(expires) BETWEEN DATE(NOW())
AND LAST_DAY(DATE(NOW()+INTERVAL 3 MONTH))
GROUP BY MONTH(expires)
Which works fine. But with the result, if there were no rows in say July, July would not show up.
How can I force July to show up with 0 as its value?

There is no easy way to do this. One possible way is to have a table called months:
Which will have 12 rows: (January, February, ..., December)
You can left join the Months table with the query you have to get the desired output.

The general consensus is that you should just create a table of month names. What follows is a silly solution which can serve as a workaround.
You'll have to work on the specifics yourself, but have you looked at sub-queries in the from clause?
Basically, it would be something like this:
SELECT NVL(B.amount, 0) as `amount`, A.month as `month`
FROM (SELECT 'January' as `month`
UNION SELECT 'February' as `month`
UNION SELECT 'March' as `month`...
UNION SELECT 'DECEMBER' as `month`) as A
LEFT JOIN
(SELECT SUM(amount) AS `amount`,
DATE_FORMAT(expires , '%M') AS `month`
FROM dealDollars
WHERE
DATE(expires) BETWEEN
DATE(NOW()) AND
LAST_DAY(DATE(NOW()+INTERVAL 3 MONTH))
GROUP BY MONTH(expires)) as B
ON (A.MONTH = B.MONTH)
Crazy, no?

MySQL doesn't have recursive functionality, so you're left with using the NUMBERS table trick -
Create a table that only holds incrementing numbers - easy to do using an auto_increment:
DROP TABLE IF EXISTS `example`.`numbers`;
CREATE TABLE `example`.`numbers` (
`id` int(10) unsigned NOT NULL auto_increment,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Populate the table using:
INSERT INTO NUMBERS
(id)
VALUES
(NULL)
...for as many values as you need. In this case, the INSERT statement needs to be run at least 3 times.
Use DATE_ADD to construct a list of days, increasing based on the NUMBERS.id value:
SELECT x.dt
FROM (SELECT DATE(DATE_SUB(CURRENT_DATE(), INTERVAL (n.id - 1) MONTH)) AS dt
FROM numbers n
WHERE DATE(DATE_SUB(CURRENT_DATE(), INTERVAL (n.id - 1) MONTH)) BETWEEN CURRENT_DATE()
AND LAST_DAY(CURRENT_DATE() +INTERVAL 3 MONTH)) ) x
Use an OUTER JOIN to get your desired output:
SELECT x.dt,
COUNT(*) AS total
FROM (SELECT DATE(DATE_SUB(CURRENT_DATE(), INTERVAL (n.id - 1) MONTH)) AS dt
FROM numbers n
WHERE DATE(DATE_SUB(CURRENT_DATE(), INTERVAL (n.id - 1) MONTH)) BETWEEN CURRENT_DATE()
AND LAST_DAY(CURRENT_DATE() +INTERVAL 3 MONTH)) ) x
LEFT JOIN YOUR_TABLE y ON y.date = x.dt
GROUP BY x.dt
ORDER BY x.dt
Why Numbers, not Dates?
Simple - dates can be generated based on the number, like in the example I provided. It also means using a single table, vs say one per data type.

select MONTHNAME(expires) as month_name,sum(`value`) from Table1
group by month_name order by null;
fiddle

Related

How to populate number for missing dates?

I have a table that look like this
date counts
Sep-01 1
Sep-05 5
Sep-10 30
As you can see the dates are jumping across and is not continuous. I want the final results to be populated for all calendar dates, and the value for the missing dates should be equal to the previous date. I played around with several SQL analytics functions but it doesn't seem to solve the problem. Any tips?
date counts
Sep-01 1
Sep-02 1
Sep-03 1
Sep-04 1
Sep-05 5
Sep-06 5
Sep-07 5
Sep-08 5
Sep-09 5
Sep-10 30
Sep-11 30
...
Here is solution:
CREATE TABLE datadate(startdate DATETIME, cnt INT);
INSERT INTO datadate VALUES('2020-05-25',1);
INSERT INTO datadate VALUES('2020-05-27',2);
INSERT INTO datadate VALUES('2020-05-30',5);
SELECT * FROM datadate;
WITH RECURSIVE cte_name (cnt,ddate) AS (
SELECT cnt,MIN(startdate) AS ddate FROM datadate
UNION ALL
SELECT
CASE
WHEN (SELECT cnt FROM datadate WHERE startdate=DATE_ADD(ddate, INTERVAL 1 DAY)) IS NULL THEN cnt
ELSE (SELECT cnt FROM datadate WHERE startdate=DATE_ADD(ddate, INTERVAL 1 DAY))
END AS cnt,
DATE_ADD(ddate, INTERVAL 1 DAY)
FROM cte_name
WHERE ddate < (SELECT MAX(startdate) FROM datadate)
)
SELECT * FROM cte_name;
Input:
Output:

Combining data from multiple rows into one

First question on here, so i will try my best to be clear.
I have 2 tables:
"TABLE1" which contains a record for each stock code and a list of attributes.
In TABLE 1 there is just one record for each stock_code
"TABLE2" which contains a log of changes to attributes of products, over time.
"TABLE2" contains the following fields:.
stock_code
stock_attribute
old_value
new_value
change_date
change_time
TABLE 2 has multiple entries ofr each stock_code.
Every time a stock item is change, another entry is made in Table2, with the attribute that has changed, the change date, time, old value and new value.
I want to create a query which will result in a table that has one record for each stock_code (from TABLE 1), and a column for each week over past year, with the value in each field being the last recorded "new_val" for that week (From TABLE 2)
I have tried
SELECT a.`stcode`, b.`week1`, b.`week2`, b.`week3`, b.`week4` etc. etc.
from (SELECT stcode, )as a
LEFT JOIN (SELECT stcode,
(CASE WHEN chngdate BETWEEN DATE_SUB(CURDATE(),INTERVAL 363 DAY) AND DATE_SUB(CURDATE(),INTERVAL 357 DAY) THEN newval END)week1,
(CASE WHEN chngdate BETWEEN DATE_SUB(CURDATE(),INTERVAL 356 DAY) AND DATE_SUB(CURDATE(),INTERVAL 350 DAY) THEN newval END)week2,
(CASE WHEN chngdate BETWEEN DATE_SUB(CURDATE(),INTERVAL 349 DAY) AND DATE_SUB(CURDATE(),INTERVAL 343 DAY) THEN newval END)week3,
(CASE WHEN chngdate BETWEEN DATE_SUB(CURDATE(),INTERVAL 342 DAY) AND DATE_SUB(CURDATE(),INTERVAL 336 DAY) THEN newval END)week4,
(etc
etc
etc
FROM (SELECT * from TABLE 2 ORDER BY "chngdate" DESC, "chngtime" DESC )as sub) as b ON b.stcode = s.stcode
ORDER BY stcode ASC
The trouble is with this, i get multiple lines for a stock_code which has mutliple entries....
for example, for stock_code abc123 the result i get is
STCODE WEEK1 WEEK2 WEEK3 WEEK4 WEEK5 WEEK6
abc123 null null 4 null null null
abc123 2 null null null null null
abc123 null null null null 3 null
what i WANT is this:
STCODE WEEK1 WEEK2 WEEK3 WEEK4 WEEK5 WEEK6
abc123 2 null 4 null 3 null
I have also tried the following, but teh query took so long, it never finished (there were 52 derived tables!)
SELECT a.`stcode`, w1.`new_value`, w2.`new_value`, w3.`new_value`, w4.`new_value` etc. etc.
from (SELECT stcode, )as a
LEFT JOIN (SELECT stcode,
LEFT JOIN (SELECT stcode, depot, fieldname, chngdate, chngtime, newval from STDepotAmendmentsLog WHERE chngdate BETWEEN DATE_SUB(CURDATE(),INTERVAL 363 DAY) AND DATE_SUB(CURDATE(),INTERVAL 357 DAY) ORDER BY "chngdate" DESC, "chngtime" DESC)as w1 on s.stcode = w1.stcode
etc for week 2, 3, 4 etc etc
You could do the following:
Find the greatest date for each "week"
Find the rows corresponding to those dates
Use conditional aggregation to convert rows into columns
Here is a rough outline of the code. It assumes that e.g. if today is 2020-03-03 then week 52 is from 2020-02-26 to 2020-03-03. Adjust if necessary:
SELECT t.stock_code
, MAX(CASE WHEN weeknum = 51 THEN new_value END) AS week01
, MAX(CASE WHEN weeknum = 50 THEN new_value END) AS week02
, MAX(CASE WHEN weeknum = 1 THEN new_value END) AS week51
, MAX(CASE WHEN weeknum = 0 THEN new_value END) AS week52
FROM table2 AS t
JOIN (
SELECT stock_code
, DATEDIFF(CURRENT_DATE, change_date) div 7 AS weeknum -- count multiples of 7
, MAX(change_date) AS greatest_date
GROUP BY stock_code, weeknum
FROM table2
) AS a ON t.stock_code = a.stock_code AND t.change_date = a.greatest_date
GROUP BY t.stock_code

MySQL 5.7 Group by specific 30 minute interval

I have a MySQL 5.7 table, with time stamps on each row. My goal is to calculate the values ​​of the some_id column and group them according to the specified 30-minute interval. And display only those intervals where count more than 0.
input:
timestamp some_id
-------------- ------
2019-01-19 05:30:12 4
2019-01-19 05:40:12 8
2019-01-19 15:37:40 2
2019-01-20 01:57:38 2
2019-01-20 07:10:07 4
2019-01-20 22:10:38 2
2019-01-21 08:35:55 4
expected:
interval COUNT(some_id)
------------- -----------
05:30:00 - 06:00:00 2
07:00:00 - 07:30:00 1
08:30:00 - 09:00:00 1
22:00:00 - 22:30:00 1
etc..........
I have tried implementing the solution presented here MySQL Group by specific 24 hour interval - but without success.
my try
I'm not sure that this is the right solution
SELECT CONCAT(
DATE_FORMAT(table.timestamp, "%H:"),
IF("30">MINUTE(table.timestamp),
(CONCAT("00-",DATE_FORMAT(DATE_ADD(table.timestamp, INTERVAL 60 MINUTE), "%H:"),"30")),
(CONCAT("30-",DATE_FORMAT(DATE_ADD(table.timestamp, INTERVAL 30 MINUTE), "%H:"),"00")))) AS time_period,
COUNT(*)
FROM table
GROUP BY time_period;
The link you provided has a very esoteric solution. Try:
SELECT CONCAT(
DATE_FORMAT(mydate, "%Y-%m-%d %H:"),
IF("30">MINUTE(mydate), "00", "30")
) AS time_bin,
COUNT(*)
FROM mytable
GROUP BY CONCAT(
DATE_FORMAT(mydate, "%Y-%m-%d %H:"),
IF("30">MINUTE(mydate), "00", "30")
)
The fastest and simplest solution would be this:
CREATE TABLE IF NOT EXISTS `events` (
`id` int unsigned NOT NULL,
`timestamp` DATETIME NOT NULL,
PRIMARY KEY (`timestamp`)
);
INSERT INTO `events` (`timestamp`, `id`) VALUES
('2019-01-19 05:30:12', 4),
('2019-01-19 06:20:12', 4),
('2019-01-19 15:37:40', 2),
('2019-01-20 01:57:38', 2),
('2019-01-20 07:10:07', 4),
('2019-01-20 22:10:38', 2),
('2019-01-21 08:35:55', 4);
And the query:
SELECT
DATE(timestamp), HOUR(timestamp), SUM(id)
FROM
events
GROUP BY 1,2 ORDER BY 1,2;
which produced
http://sqlfiddle.com/#!9/fadd28/5
EDIT: Whoops, missed the 30-minute interval requirement. But I think you get the point. You can play with my solution at the link above. :)
But if your database supports WINDOW functions, even better.
Also, for such purpose I'd personally create an aggregation table which will contain hourly counters and is being updated during INSERT using TRIGGER.
I took what #symcbean said but tunned it a bit in order to give what you wished:
SELECT CONCAT(
DATE_FORMAT(timestamp, "%H:00 - "),
IF("00">MINUTE(timestamp), CONCAT(HOUR(timestamp), ":00"), CONCAT(HOUR(timestamp), ":30"))
) AS Time,
COUNT(*)
FROM mysql_24h_cycle
GROUP BY CONCAT(
DATE_FORMAT(timestamp, "%H:"),
IF("30">MINUTE(timestamp), CONCAT(HOUR(timestamp), ":00"), CONCAT(HOUR(timestamp), ":30"))
)
Here you can see the query and the output
This is the data used

Mysql : Finding empty time blocks between two dates and times?

I wanted to find out user's availability from database table:
primary id | UserId | startdate | enddate
1 | 42 | 2014-05-18 09:00 | 2014-05-18 10:00
2 | 42 | 2014-05-18 11:00 | 2014-05-18 12:00
3 | 42 | 2014-05-18 14:00 | 2014-05-18 16:00
4 | 42 | 2014-05-18 18:00 | 2014-05-18 19:00
Let's consider above inserted data is user's busy time, I want to find out free time gap blocks from table between start time and end time.
BETWEEN 2014-05-18 11:00 AND 2014-05-18 19:00;
Let me add here schema of table for avoiding confusion:
Create Table availability (
pid int not null,
userId int not null,
StartDate datetime,
EndDate datetime
);
Insert Into availability values
(1, 42, '2013-10-18 09:00', '2013-10-18 10:00'),
(2, 42, '2013-10-18 11:00', '2013-10-18 12:00'),
(3, 42, '2013-10-18 14:00', '2013-11-18 16:00'),
(4, 42, '2013-10-18 18:00', '2013-11-18 19:00');
REQUIREMENT:
I wanted to find out free gap records like:
'2013-10-27 10:00' to '2013-10-28 11:00' - User is available for 1 hours and
'2013-10-27 12:00' to '2013-10-28 14:00' - User is available for 2 hours and
available start time is '2013-10-27 10:00' and '2013-10-27 12:00' respectively.
Here you go
SELECT t1.userId,
t1.enddate, MIN(t2.startdate),
MIN(TIMESTAMPDIFF(HOUR, t1.enddate, t2.startdate))
FROM user t1
JOIN user t2 ON t1.UserId=t2.UserId
AND t2.startdate > t1.enddate AND t2.pid > t1.pid
WHERE
t1.endDate >= '2013-10-18 09:00'
AND t2.startDate <= '2013-11-18 19:00'
GROUP BY t1.UserId, t1.endDate
http://sqlfiddle.com/#!2/50d693/1
Using your data, the easiest way is to list the hours when someone is free. The following gets a list of hours when someone is available:
select (StartTime + interval n.n hour) as FreeHour
from (select cast('2014-05-18 11:00' as datetime) as StartTime,
cast('2014-05-18 19:00' as datetime) as EndTime
) var join
(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
) n
on StartTime + interval n.n hour <= EndTime
where not exists (select 1
from availability a
where StartTime + interval n.n hour < a.EndDate and
StartTime + interval n.n hour >= a.StartDate
);
EDIT:
The general solution to your problem requires denormalizing the data. The basic query is:
select thedate, sum(isstart) as isstart, #cum := #cum + sum(isstart) as cum
from ((select StartDate as thedate, 1 as isstart
from availability a
where userid = 42
) union all
(select EndDate as thedate, -1 as isstart
from availability a
where userid = 42
) union all
(select cast('2014-05-18 11:00' as datetime), 0 as isstart
) union all
(select cast('2014-05-18 19:00' as datetime), 0 as isstart
)
) t
group by thedate cross join
(select #cum := 0) var
order by thedate
You then just choose the values where cum = 0. The challenge is getting the next date from this list. In MySQL that is a real pain, because you cannot use a CTE or view or window function, so you have to repeat the query. This is why I think the first approach is probably better for your situation.
The core query can be this. You can dress it up as you like, but I'd handle all that stuff in the presentation layer...
SELECT a.enddate 'Available From'
, MIN(b.startdate) 'To'
FROM user a
JOIN user b
ON b.userid = a.user
AND b.startdate > a.enddate
GROUP
BY a.enddate
HAVING a.enddate < MIN(b.startdate)
For times outside the 'booked' range, you have to extend this a little with a UNION, or again handle the logic at the application level

Query returns null instead of 0 [duplicate]

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.