views:

355

answers:

4

I have the correlated subquery below. Each id (147,148,149) in table1 has many records in table2. The id 148 however has no records that match the time condition in the statement. Therefore it is not included in the output. I want it to be included with a 0 for the count column. What needs to be changed?

SELECT b.fkid, COUNT(DISTINCT username)
FROM table2 AS b
WHERE
b.fkid IN ( 147,148,149 )
AND time > (SELECT SUBTIME(a.endTime, SEC_TO_TIME( 60*60 )) FROM table1 AS a WHERE a.id = b.fkid)
GROUP BY b.fkid

This statement returns:

b.fkid   COUNT(DISTINCT username)
147      41
149      26

I want it to return:

b.fkid   COUNT(DISTINCT username)
147      41
148       0
149      26

Okay I got the solution. It is a modified version of rexem's answer:

SELECT t.fkid,
   IFNULL(nu.num_users, 0)
FROM TABLE_2 t
LEFT JOIN (SELECT t.fkid,
                  COUNT(DISTINCT t.username) 'num_users'
           FROM TABLE_2 t
           JOIN TABLE_1 a ON a.id = t.fkid
                          AND SUBTIME(a.endTime, SEC_TO_TIME( 60*60 )) < t.time
           GROUP BY t.fkid) nu ON nu.fkid = t.fkid
WHERE t.fkid IN (147, 148, 149)
GROUP BY t.fkid, nu.num_users

Changes from rexem's answer:
"SEC_TO_TIME( 60*60 )) = t.time" to "SEC_TO_TIME( 60*60 )) < t.time"
Removed "t.time" in GROUP BY clause of subquery
+1  A: 

Try using COALESCE to handle the scenarios where time is compared to the empty set (e.g., because there are no matches between table1 and table2):

SELECT b.fkid, COUNT(DISTINCT username)
FROM table2 AS b
WHEREb.fkid IN ( 147,148,149 )
AND time > COALESCE((SELECT SUBTIME(a.endTime, SEC_TO_TIME( 60*60 )) FROM table1 AS a WHERE a.id = b.fkid), 0)
GROUP BY b.fkid

COALESCE takes an unbounded set of parameters and, from this group, will return the first non-null parameter.

David Andres
I tried COALESCE, but it still gives me the same output.
hekevintran
What are the values of the time field?
David Andres
The time comparison isn't the issue, and adding COALESCE to surround the COUNT(DISTINCT username) won't make the records with no counts appear. This is a standard LEFT JOIN affair.
OMG Ponies
The time field holds datetime values that are before the endTime values.
hekevintran
I also don't recommend using COALESCE when IFNULL is sufficient. COALESCE is meant for 2+ things being null, making queries less readable. Use API properly.
OMG Ponies
@rexem: I believe the empty set is treatable as NULL by COALESCE, and so that's why I placed it within the WHERE clause. Left joins work as well.
David Andres
@rexem: Are you sure you don't mean ISNULL rather than IFNULL?
David Andres
@David: No, I do mean IFNULL: http://dev.mysql.com/doc/refman/5.0/en/control-flow-functions.html
OMG Ponies
@rexem: I agree with your statement about API usage, though prefer ANSI-compliant queries over flavor-specific usage, which is partially what motivated me to use COALESCE (other than lack of awareness of IFNULL, of course). It's subjective as to whether COALESCE is less appropriate than IFNULL. Is the latter more performant (I assume it is)?
David Andres
@David: Dunno about performance, just saying it can be a red herring to someone else reading your code - they'd expect 2+ arguments. Every DB has an null check: NVL in Oracle; ISNULL in SQL Server...
OMG Ponies
@rexem: Fair enough, I don't know the extent of one (IFNULL) is used over the other (COALESCE) in typical MySQL database code. However, I don't think I'd ever be throw off by its usage, or ISNULL/IFNULL, or even the CASE statements COALESCE is essentially shorthand for.
David Andres
+1  A: 

Try:

   SELECT t.fkid,
          IFNULL(nu.num_users, 0)
     FROM TABLE_2 t
LEFT JOIN (SELECT t.fkid,
                  COUNT(DISTINCT t.username) 'num_users'
             FROM TABLE_2 t
             JOIN TABLE_1 a ON a.id = t.fkid
                           AND SUBTIME(a.endTime, SEC_TO_TIME( 60*60 )) = t.time
         GROUP BY t.fkid) nu ON nu.fkid = t.fkid
   WHERE t.fkid IN (147, 148, 149)
GROUP BY t.fkid, nu.num_users
OMG Ponies
You forgot a comma after "JOIN (SELECT a.id". After making that change the statement runs but it gives me rows of (147, 0) many times followed by many rows of (149, 0).
hekevintran
Thx for the heads up, updated answer to add missing comma and GROUP BY to get rid of the duplicates.
OMG Ponies
It doesn't work though. The statement returns 0 for everything and doesn't return a row for id 148.
hekevintran
@hekevintran: My bet then is the time comparison is filtering out the fkid 148
OMG Ponies
@rexem: So there's no way to do a count on an empty result set? To me it makes sense for the count to equal 0.
hekevintran
@hekevintran: That's why there's the IFNULL, because no related record in TABLE_1 will turn up as a NULL due to the LEFT JOIN.
OMG Ponies
@rexem: This one works better. All the ids are in the result, but the counts are somewhat weird. The counts are not regarding the time condition; it's just a total count from the table. Switching the < to a > does has no effect.
hekevintran
Remember: COUNT never returns NULL.
Jonathan Leffler
@rexem: I got it to work using the first version modified slightly (see above). Thanks for the help!
hekevintran
@hekevintran: Cool, updated mine to match.
OMG Ponies
A: 
SELECT a.ID, COUNT(DISTINCT username)
FROM table1 AS a LEFT JOIN table2 AS b
on a.ID = b.fkID
WHERE a.ID IN ( 147,148,149 )
AND b.time > SUBTIME(a.endTime, SEC_TO_TIME( 60*60 )) 
GROUP BY a.ID

Does this help in anyway?

EDIT: I am not sure if this can work in mysql. But, I hope you get the idea to build the query using this.

shahkalpesh
It's fine for MySQL, but it looks like the time comparison is what's responsible for 148 not turning up in the results.
OMG Ponies
@rexem, shahkalpesh: the time comparison needs to be moved to the LEFT JOIN condition for this to work.
David Andres
@David: Thx, waiting on hekevintran for confirmation
OMG Ponies
@rexem: That is the reason, I am adding table1 on LEFT so that 148 gets selected irresp. of there are records in table2.
shahkalpesh
@David: I am not able to understand of moving TIME comparison to LEFT. IMO, my query picks records from table1 and doing a LEFT JOIN. So, 148 should get picked up even if there are no records in table2.
shahkalpesh
A: 

I think your best chance of getting the zero into the result is using a (disjoint) UNION. The first part would be your current query; the second clause would find the values where there are no relevant entries, selecting the constant 0 for the count column.

Often, an outer join would do the trick. The difficulty here is that the outer join would produce a row of data for ID = 148, and therefore COUNT would give an answer of 1, not 0.

Jonathan Leffler
Can you give me an example of the statement that looks for the non-existence of entries?
hekevintran
SELECT * FROM ReferencingTable AS S WHERE NOT EXISTS (SELECT * FROM ReferencedTable AS T WHERE S.IDCol1 = T.IDCol1 AND S.IDCol2 = T.IDCol2);
Jonathan Leffler
Thanks! This answer is the simplest and fastest.
hekevintran