oooo - a challenge.
Yes, you just need your product table left-joined into the mix at the appropriate place...
Assuming that each product on tblProducts has a single record with a field ProductName that matches the ProductName on the log records...
Code:
Query1:
SELECT
TP2.ProductName,
TP2.DayTime,
TP2.HourTime,
TL.UserName
FROM
(SELECT
TP1.ProductName,
TD.DayTime,
TH.HourTime
FROM
tblProducts AS TP1,
tblDays AS TD,
tblHours AS TH) AS TP2
LEFT JOIN
tblLogs AS TL
ON TP2.ProductName = TL.ProductName
WHERE (TP2.DayTime + TP2.HourTime) BETWEEN TL.StartTime AND TL.EndTime
GROUP BY TP2.ProductName, TP2.DayTime, TP2.HourTime, TL.UserName;
Note that the inner select - aliased TP2 - is now a true cross-join, resulting in one record with each possible combination of product name, day, and hour. When you left-join that against the tblLog, you'll get either one Query1 record for an hour with no log data, or one Query1 record per log record in that hour.
I don't think Query2 needs updated. The null record in the inner (Q2) select of Query2 should result in a Count(0) for that hour, and if no users logged on that day, that gives you a MaxLoggedUsers of zero for that product for that day.
No, actually it wasn't right in the first place. I'm sure that syntax would have thrown an error. Here's the corrected version.
Code:
Query2:
SELECT Q2.ProductName, Q2.DayTime, MAX(Q2.HourCount) AS MaxLoggedUsers
FROM
(SELECT Q1.ProductName, Q1.DayTime, Q1.HourTime, Count(Q1.UserName) As HourCount
FROM Query1 AS Q1
GROUP BY Q1.ProductName, Q1.DayTime, Q1.HourTime) AS Q2
GROUP BY Q2.ProductName, Q2.DayTime;