MySQL Query - data not showing as expected, problem in code - mysql

I am trying to query a database for the number of individuals who did not arrive for their booking on a given date. However, the results given are not as expected.
From manual checking, the results for 3rd May 2021 should be displayed as 3. I have a feeling that the customer id's are being added together with the result being displayed rather than just the count of individual customer id's.
select
count(c.CUSTOMER_ID) AS 'No Shows',
date(checkins.POSTDATE) as date
from
customers c, checkins
where
checkins.postdate >= date_sub(curdate(), interval 7 day)
and
(
c.archived = 0
and (
(
(
(
(
(
c.GUID in (
select
sb1.customer_guid
from
schedule_bookings sb1
join schedule_events se1 on sb1.course_guid = se1.course_guid
and sb1.OFFERING_ID in (
'2915911', '3022748', '3020740', '2915949',
'2914398', '2916147', '3022701',
'3020699', '2916185', '2915168',
'2916711', '3022403', '3020455',
'2916785', '2916478', '2915508',
'3022538', '3020582', '2915994',
'2914547', '2916069', '3022648',
'3020658', '2916107', '2915290',
'2928786', '2914729', '3022854',
'3020812', '2914694', '2914659',
'3041801', '2920756', '2920834',
'2920795', '2916223', '3022788',
'3020783', '2916239', '2915013'
)
and sb1.CANCELLED in ('0')
)
)
or (
c.GUID in (
select
sp.customer_guid
from
schedule_participants sp
join schedule_bookings sb2 on sp.BOOKING_ID = sb2.BOOKING_ID
join schedule_events se2 on sb2.course_guid = se2.course_guid
and sb2.OFFERING_ID in (
'2915911', '3022748', '3020740', '2915949',
'2914398', '2916147', '3022701',
'3020699', '2916185', '2915168',
'2916711', '3022403', '3020455',
'2916785', '2916478', '2915508',
'3022538', '3020582', '2915994',
'2914547', '2916069', '3022648',
'3020658', '2916107', '2915290',
'2928786', '2914729', '3022854',
'3020812', '2914694', '2914659',
'3041801', '2920756', '2920834',
'2920795', '2916223', '3022788',
'3020783', '2916239', '2915013'
)
and sb2.CANCELLED in ('0')
)
)
)
)
)
and (
(
(
not (
(
(
select
count(CHECKIN_ID)
from
checkins
where
checkins.CUSTOMER_ID = c.CUSTOMER_ID
) between 1
and 9999
)
)
)
)
)
)
)
and not c.customer_id in (1008, 283429, 2507795)
)
group by date(checkins.POSTDATE)
Here are the results:
+----------+------------+
| No Shows | date |
+----------+------------+
| 30627 | 2021-04-27 |
| 37638 | 2021-04-28 |
| 34071 | 2021-04-29 |
| 33579 | 2021-04-30 |
| 29274 | 2021-05-01 |
| 30135 | 2021-05-02 |
| 48339 | 2021-05-03 |
| 8979 | 2021-05-04 |
+----------+------------+
8 rows in set (8.71 sec)
As you can see, the count is nowhere near as intended.
The query parameters are:
Customer is a participant/bookee on the listed specific offerings (offering_id)
Customer's 'Check-in' count was not between 1 and 9999.
Display these results by count per date.
Can anyone see why this query would be not displaying the results as intended?
Kind Regards
Tom

Lets try to reverse this out some. You are dealing with a very finite set of Offering IDs. How about something like starting with the finite list of offerings you are concerned with and join on from that. Additionally, there does not appear to be any need for the join to the schedule events table. If something is booked, its booked. You are never getting any additional context from the event itself.
So, lets start with a very simplified union. You are looking at the bookings table for the possible customer IDs. Then from the actual participants for those same bookings. My GUESS is not every person doing the actual booking may be a participant, likewise, all participants may not be the booking party.
None of this has to do with the actual final customer, archive status or even the events for the booking. We are just getting people - period. Once you have the people and dates, then get the counts.
select
date(CI.POSTDATE) as date,
count( JustCustomers.customer_guid ) AS 'No Shows'
from
(
select
sb1.customer_guid
from
schedule_bookings sb1
where
sb1.CANCELLED = 0
-- if "ID" are numeric, dont use quotes to imply character
and sb1.OFFERING_ID in
( 2915911, 3022748, 3020740, 2915949,
2914398, 2916147, 3022701, 3020699,
2916185, 2915168, 2916711, 3022403,
3020455, 2916785, 2916478, 2915508,
3022538, 3020582, 2915994, 2914547,
2916069, 3022648, 3020658, 2916107,
2915290, 2928786, 2914729, 3022854,
3020812, 2914694, 2914659, 3041801,
2920756, 2920834, 2920795, 2916223,
3022788, 3020783, 2916239, 2915013
)
UNION
select
sp.customer_guid
from
schedule_bookings sb2
JOIN schedule_participants sp
on sb2.BOOKING_ID = sp.BOOKING_ID
where
sb2.CANCELLED = 0
and sb2.OFFERING_ID in
( 2915911, 3022748, 3020740, 2915949,
2914398, 2916147, 3022701, 3020699,
2916185, 2915168, 2916711, 3022403,
3020455, 2916785, 2916478, 2915508,
3022538, 3020582, 2915994, 2914547,
2916069, 3022648, 3020658, 2916107,
2915290, 2928786, 2914729, 3022854,
3020812, 2914694, 2914659, 3041801,
2920756, 2920834, 2920795, 2916223,
3022788, 3020783, 2916239, 2915013
)
) JustCustomers
JOIN customers c
on JustCustomers.customer_guid = c.customer_id
AND c.archived = 0
AND NOT c.customer_id IN (1008, 283429, 2507795)
JOIN checkins CI
on c.CUSTOMER_ID = CI.CUSTOMER_ID
AND CI.postdate >= date_sub(curdate(), interval 7 day)
group by
date(ci.POSTDATE)
The strange thing I notice though is that you are looking for "No shows", but explicitly looking for those people who DID check in. Now, if you are looking for all people who WERE SUPPOSED to be at a given event, then you are probably looking for where the customer DID NOT check in. If that is the intended case, there would be no check-in date to be associated. If that is the case, I would expect a date in some table such as the EVENT Date... such as going on a cruise, the event is when the cruise is, regardless of who makes it to the ship.
If I am way off, I would suggest you edit your existing post, provide additional detail / clarification.

Related

Mysql time spent at work by specyfic user

I have a MySQL table like this:
+-----+----------+------------+--------------+-------------+
| id | user_id | added_on | status_enter | status_exit |
+-----+----------+------------+--------------+-------------+
Is it possible to count the time if the data is in other rows?
12:16:16 - 10:44:1
User Date Enter Exit
----------- -------------------- ------ ------
John 2021-06-25 10:44:15 1 0
John 2021-06-25 12:16:16 0 1
Not tested, but SHOULD get what you are looking for. The outer query is only looking for those where a person clocked IN. The 3rd column-based select is a correlated query to whatever the current user is and the ID is greater than the check-in, AND it is the check-out. So its possible a null value here if the person is still clocked-in. I would have an index on this table by (enter, user, exit, id) to help optimize the query.
select
tc.id,
tc.user,
tc.date,
( select min( tc2.date )
from TimeClockTable tc2
where tc.User = tc2.User
and tc.id < tc2.id
and tc2.enter = 0
and tc2.exit = 1 ) EndTime,
( select min( tc2.id )
from TimeClockTable tc2
where tc.User = tc2.User
and tc.id < tc2.id
and tc2.enter = 0
and tc2.exit = 1 ) EndTimeID
from
TimeClockTable tc
where
tc.enter = 1
FEEDBACK
If the date/time stamp is always going to be sequential with the ID as it is added, ie: ID #1234 on July 5 at 10:00am will ALWAYS be before #1235 on July 5 at 10:01am (you would never have an ID 1235 or higher that was BEFORE the date/time of ID #1234), then the above modification to the query should work for you. You are already getting the lowest date/time for the given user in comparison to the first, then calling it a second time to get the minimum ID would correlate to the same end time.
There you go:
SELECT T.user_id AS User,
CAST(T.added_on AS DATE) AS Date,
DATEDIFF(
HOUR,
MIN(T.added_on),
MAX(T.added_on)
) AS TotalWorkTime
FROM WorkTable AS T
GROUP BY T.user_id,
CAST(T.added_on AS DATE)

SQL consecutive occurrences for availability based query

I am a bit stuck trying to create a pretty complex on SQL, and more specifically MySQL.
The database deals with car rentals, and the main table of what is a snowflake patters looks a bit like:
id | rent_start | rent_duration | rent_end | customerID | carId
-----------------------------------------------------------------------------------
203 | 2016-10-03 | 5 | 2016-11-07 | 16545 | 4543
125 | 2016-10-20 | 9 | 2016-10-28 | 54452 | 5465
405 | 2016-11-01 | 2 | 2016-01-02 | 43565 | 346
My goal is to create a query that allows given
1) A period range like, for example: from 2016-10-03 to 2016-11-03
2) A number of days, for example: 10
allows me to retrieve the cars that are actually available for at least 10 CONSECUTIVE days between the 10th of October and the 11th.
A list of IDs for those cars is more than enough... I just don't really know how to setup a query like that.
If it can help: I do have a list of all the car IDs in another table.
Either way, thanks!
I think it is much simpler to work with availability, rather than rentals, for this purpose.
So:
select r.car_id, r.rent_end as avail_start,
(select min(r2.rent_start
from rentals r2
where r2.car_id = r.car_id and r2.rent_start > r.rent_start
) as avail_end
from rentals r;
Then, for your query, you need at least 10 days. You can use a having clause or subquery for that purpose:
select r.*
from (select r.car_id, r.rent_end as avail_start,
(select min(r2.rent_start
from rentals r2
where r2.car_id = r.car_id and r2.rent_start > r.rent_start
) as avail_end
from rentals r
) r
where datediff(avail_end, avail_start) >= $days;
And finally, you need for that period to be during the dates you specify:
select r.*
from (select r.car_id, r.rent_end as avail_start,
(select min(r2.rent_start
from rentals r2
where r2.car_id = r.car_id and r2.rent_start > r.rent_start
) as avail_end
from rentals r
) r
where datediff(avail_end, avail_start) >= $days and
( (avail_end > $end and avail_start < $start) or
(avail_start <= $start and avail_end >= $start + interval 10 day) or
(avail_start > $start and avail_start + interval 10 day <= $end)
);
This handles the various conditions where the free period covers the entire range or starts/ends during the range.
There are no doubt off-by-one errors in this logic (is a car available the same date it returns). The this should give you a solid approach for solving the problem.
By the way, you should also include cars that have never been rented. But that is not possible with the tables you describe in the question.

Complicated SELECT statement includes multiple joins not returning desired results

I am trying to create a SELECT to return information from a couple of tables. I had it working but then received an additional requirement and now I am having trouble figuring out how to get what I want.
I have a table with information on programs that could be included in the report (based on further requirements)...this file is called milestones.
I have another table with projects in it that relate to the programs - if the IDs match
I have a new table that has a manually entered override end date - this is the new requirement. There is a system end date in the milestones table, but if this override date is entered then it takes precedence over the system end date. If an override date has been entered, the exception file will have the same program ID and two dates which match dates in the milestones table.
dates are yyyy-mm-dd formatted
Example data:
Milestones:
prgId | startDate | endDate
------------------------------
123 | 2014-03-09 | 2014-11-10
123 | 2014-07-10 | 2014-11-10
324 | 2014-05-09 | 2014-11-12
exceptions:
prgId | startDate | overEnd
-------------------------------
123 | 2014-03-09 | 2014-05-31
projects:
prgId | cust
-------------
123 | 12121
123 | 4323
what I currently have being returned is:
prgId prjCnt startDate endDate overEnd
123 2 2014-03-09 2014-11-10 2014-05-31
123 2 2014-07-10 2014-11-10
324 0 2014-05-09 2014-11-12
I do realize that right now the two projects for program 123 will show for both lines - we will be looking for a way to associate them with the right ones but do not have that yet.
We added the override date requirement so that a report of current programs would not show both the '123' lines but only the one that is current (the second one).
My current SELECT is like this (sorry, I can't get this to show easier it is really long):
SELECT milestones.*, newtbl.prjcnt, exceptions.overEnd
FROM milestones
LEFT JOIN ((
SELECT prgGuid, count( prgGuid ) AS prjcnt
FROM projects
GROUP BY prgGuid
) AS newtbl )
ON milestones.prgId = newtbl.prgId
LEFT JOIN exceptions
ON (milestones.prgId = exceptions.prgId
AND milestones.startDate = exceptions.startDate)
WHERE <(milestones.startDate > '2013-00-00')
AND (milestones.startDate <= CURDATE() AND milestones.endDate >= CURDATE())
ORDER BY milestones.endDate, milestones.startDate DESC
Now what I want is to change this to only grab programs, project counts, start and end dates, and the override end date for programs where the start date is anything from 2013 to the current date and that have not ended yet. Now....if a program has an override end date and that end date is current (>= the current date) it should be included but if the override date is NULL or <= the current date, I do not want to include it.
What I want to have returned is:
prgId prjCnt startDate endDate overEnd
123 2 2014-07-10 2014-11-10
324 0 2014-05-09 2014-11-12
The first line before has expired so shouldn't show.
I've tried a few things but I either end up with no results or I get everything that I am currently getting.
Can someone help me figure out what the SELECT should be?
So if I follow you, your DDL might look like this:
CREATE TABLE MILESTONES
(`prgId` int, `startDate` varchar(10), `endDate` varchar(10))
;
INSERT INTO MILESTONES
(`prgId`, `startDate`, `endDate`)
VALUES
(123, '2014-03-09', '2014-11-10'),
(123, '2014-07-10', '2014-11-10'),
(324, '2014-05-09', '2014-11-12')
;
CREATE TABLE EXCEPTIONS
(`prgId` int, `startDate` varchar(10), `overEnd` varchar(10))
;
INSERT INTO EXCEPTIONS
(`prgId`, `startDate`, `overEnd`)
VALUES
(123, '2014-03-09', '2014-05-31')
;
CREATE TABLE PROJECTS
(`prgId` int, `cust` int)
;
INSERT INTO PROJECTS
(`prgId`, `cust`)
VALUES
(123, 12121),
(123, 4323)
;
And your current query which isn't working is this (note I've corrected what I presume are typos in your query from your question):
SELECT milestones.*, newtbl.prjcnt, exceptions.overEnd
FROM milestones
LEFT JOIN ((
SELECT prgId, count( prgId ) AS prjcnt
FROM projects
GROUP BY prgId
) AS newtbl )
ON milestones.prgId = newtbl.prgId
LEFT JOIN exceptions
ON (milestones.prgId = exceptions.prgId
AND milestones.startDate = exceptions.startDate)
WHERE (milestones.startDate > '2013-00-00')
AND (milestones.startDate <= CURDATE() AND milestones.endDate >= CURDATE())
ORDER BY milestones.endDate, milestones.startDate DESC
A working solution looks like this:
SELECT DISTINCT
M.prgId as PRGID
, ( SELECT COUNT(X.prgID)
FROM PROJECTS X
WHERE X.prgID = M.prgID ) as PRJCNT
, M.startDate as STARTDATE
, M.endDate as ENDDATE
, COALESCE(E.overEnd,'') as OVEREND
FROM MILESTONES M
LEFT OUTER JOIN PROJECTS P
ON M.prgId = P.prgId
LEFT JOIN EXCEPTIONS E
ON M.prgId = E.prgId
AND M.startDate = E.startDate
WHERE M.startDate > '2013-01-01'
AND M.startDate <= CURDATE()
AND M.endDate >= CURDATE()
AND ( E.overEnd IS NULL
OR E.overEnd > CURDATE() )
You can see it in action here: SQLFiddle.
Note that the solution relies on the COALESCE function for clean output and more of your business rules being put in place in the WHERE clause.

SQL to get conversations from two tables

I have two tables, one that stores incoming messages and one that stores outgoing messages. What I would like is to be able to have a conversations view of the messages so that all incoming and outgoing messages from/to the same user id are grouped and the conversations are ordered by the most recent message (in or out)
Outgoing
----------
user_id
time
message
Incoming
----------
user_id
time
message
What I would like is to display the results such as
-> User A 9:10 pm Nice ...
<- User A 8:45 pm Our special is pepperoni!
-> User A 8:00 pm What's your special dish?
<- User B 9:00 pm We open at 5
-> User B 6:56 pm Hello What time to you open?
<- User C 8:43 pm Thanks!
-> User C 4:00 pm Loved the pizza today!!
Any idea how to write a query to do this?
EDIT
If user B then texts back in, the result should be:
-> User B 9:15 pm Ok great!
<- User B 9:00 pm We open at 5
-> User B 6:56 pm Hello What time to you open?
-> User A 9:10 pm Nice ...
<- User A 8:45 pm Our special is pepperoni!
-> User A 8:00 pm What's your special dish?
<- User C 8:43 pm Thanks!
-> User C 4:00 pm Loved the pizza today!!
You need to UNION the two tables and sort (ORDER BY) accordingly:
SELECT
'<-' AS direction, user_id, time, message
FROM
Outgoing
UNION ALL
SELECT
'->', user_id, time, message
FROM
Incoming
ORDER BY
user_id ASC,
time DESC ;
After the additional explanations for the complex ordering:
SELECT
CASE WHEN m.d = 1 THEN '<-' ELSE '->' END AS direction,
m.user_id, m.time, m.message
FROM
( SELECT
u.user_id,
GREATEST( COALESCE(mo.time, mi.time),
COALESCE(mi.time, mo.time) ) AS maxtime
FROM
( SELECT user_id FROM Outgoing
UNION
SELECT user_id FROM Incoming
) AS u
LEFT JOIN
( SELECT user_id, MAX(time) AS time FROM Outgoing GROUP BY user_id
) AS mo
ON mo.user_id = u.user_id
LEFT JOIN
( SELECT user_id, MAX(time) AS time FROM Incoming GROUP BY user_id
) AS mi
ON mi.user_id = u.user_id
) AS b
JOIN
( SELECT 1 AS d, user_id, time, message FROM Outgoing
UNION ALL
SELECT 2 AS d, user_id, time, message FROM Incoming
) AS m
ON m.user_id = b.user_id
ORDER BY
b.maxtime ASC,
m.user_id ASC,
m.time DESC ;
Something like this should get the results by user and time. You would need to handle the display at the application level to show messages per user:
select * from (
select '->' as direction, o.* from outgoing o
union
select '<-' as direction, i.* from incoming i
) M
order by user_id asc, time desc
Sample Output:
| DIRECTION | USER_ID | TIME | MESSAGE |
----------------------------------------------------------------------------------------
| -> | 1 | November, 29 2012 21:10:00+0000 | Nice ... |
| <- | 1 | November, 29 2012 20:45:00+0000 | Our special is pepperoni! |
| -> | 1 | November, 29 2012 20:00:00+0000 | What''s your special dish? |
| <- | 2 | November, 29 2012 21:00:00+0000 | We open at 5 |
| -> | 2 | November, 29 2012 18:56:00+0000 | Hello What time to you open? |
| <- | 3 | November, 29 2012 20:43:00+0000 | Thanks! |
| -> | 3 | November, 29 2012 16:00:00+0000 | Loved the pizza today!! |
Demo: http://www.sqlfiddle.com/#!2/602c1/11
Personally, I am not a big fan of how you have your tables structured. What's an incoming message to one user is an outgoing message to another, meaning you need to duplicate every message in the system in each table.
I would probably just have a single messages table with a to and from field. If you had a single table like this:
message_id (primary key)
from_user_id (indexed)
to_user_id (indexed)
message
time (indexed)
Your query would be simple:
SELECT *
FROM messages
WHERE from_user_id = ? OR to_user_id = ?
ORDER BY time DESC
Note this doesn't give you an easy query for purposes of display in the manner you are showing (you would need to do some post-query data manipulation). But it does give you the most efficient lookup query and prevent you from needing to duplicate the messages twice in your storage.
If you need to stick to the concept of grouped conversations (or even to extend to multi-party messages), then perhaps you could look at having a conversations table and modify your schema to be something like this:
conversations (many-to-many join table)
conversation_id (indexed)
user_id (indexed)
(compound primary key across both fields)
messages
message_id (primary key)
conversation_id (indexed)
sending_user_id
message
time (indexed)
With a query like this
SELECT m.sending_user_id, m.message, m.time
FROM conversations AS c
INNER JOIN messages AS m ON c.conversation_id = m.conversation_id
WHERE c.user_id = ?
ORDER BY c.conversation_id, m.time DESC
Obviously from the result query, if the sending_user_id is equal to the id of the current user it is an outgoing message, otherwise it is a message from one of the other conversation participants.
Why separate tables? You could put those in the same table and add a column of type bit with 1 and 0 representing incoming and outgoing. Then your query is as simple as:
select user_id, time, message, inout from message order by user_id, time
To me the direction is telling you something about the message, either way its still a message.
If you still have to do it the other way then you'll have to do a union but expect poorer performance. The best performance tweak you can give you query is through up front table design.
with message as (
select user_id, time, message, 'incoming' from incoming
union all
select user_id, time, message, 'outgoing' from outgoing
) select * from message order by user_id, time
or something like that...
Also, you should be wary of ordering by a time field. From experience you will find you get unexpected results if two messages come in with the same time. This is especially likely as your example is only granular to the minute, rather than second or microsecond. A better way is to have a numeric PK which is auto-assigned in ascending order. That way if the times are not unique you still have a way of determining order.

MySQL - Count Yearly Totals when some Years have nulls

I have 1 table with similar data:
CustomerID | ProjectID | DateListed | DateCompleted
123456 | 045 | 07-29-2010 | 04-03-2011
123456 | 123 | 10-12-2011 | 11-30-2011
123456 | 157 | 12-12-2011 | 02-10-2012
123456 | 258 | 06-07-2011 | NULL
Basically, a customer contacts us, we get a project on our list, and we mark it completed when we're done with it.
What I'm after is a simple (you'd think, at least) count of all projects, with expected output like below:
YEAR | TotalListed | TotalCompleted
2010 | 1 | 0
2011 | 3 | 2
2012 | 0 | 1
However, my query below - because of the join - isn't showing 2012's count, because there's been no listed project for 2012. However, I can't really reverse the query, as then 2010's count wouldn't show up (since nothing was completed in 2010).
I'm open to any suggestions, or tips like how to do this. I've pondered a temp table, is that the best way to go? I'm open to anything that gets me what I need!
(If the code looks familiar, ya'll helped me get the subquery made! MySQL Subquery with main query data variable)
SELECT YEAR(p1.DateListed) AS YearListed, COUNT(p1.ProjectID) As Listed, PreQuery.Completed
FROM(
SELECT YEAR(DateCompleted) AS YearCompleted, COUNT(ProjectID) AS Completed
FROM projects
WHERE CustomerID = 123456 AND DateListed >= DATE_SUB(Now(), INTERVAL 5 YEAR)
GROUP BY YEAR(DateCompleted)
) PreQuery
RIGHT OUTER JOIN projects p1 ON PreQuery.YearCompleted = YEAR(p1.DateListed)
WHERE CustomerID = 123456 AND DateListed >= DATE_SUB(Now(), INTERVAL 5 YEAR)
GROUP BY YearListed
ORDER BY p1.DateListed
After reviewing your table, query, and expected results - I believe I have found a more-revised query to suit your needs. It is a fairly-full rewrite of your existing query though, but I've tested it with your given data and received the same results you want/expect:
SELECT
years.`year`,
SUM(IF(YEAR(DateListed) = years.`year`, 1, 0)) AS TotalListed,
SUM(IF(YEAR(DateCompleted) = years.`year`, 1, 0)) AS TotalCompleted
FROM
projects
LEFT JOIN (
SELECT DISTINCT `year` FROM (
SELECT YEAR(DateListed) AS `year` FROM projects
UNION SELECT YEAR(DateCompleted) AS `year` FROM projects WHERE DateCompleted IS NOT NULL
) as year_inner
) AS years
ON YEAR(DateListed) = `year`
OR YEAR(DateCompleted) = `year`
WHERE
CustomerID = 123456 AND DateListed >= DATE_SUB(Now(), INTERVAL 5 YEAR)
GROUP BY
years.`year`
ORDER BY
years.`year`
To explain, we should start with the inner query (aliased as year_inner). It selects a full list of years in the DateListed and DateCompleted columns and then selects a DISTINCT list of those to create the years alias sub-query. This sub-query is used to get a full list of "years" that we want data for. Doing it this way, opposed to a sub-query with counts and groupings will allow you to only have to define the WHERE clause on the outermost query (though, if efficiency becomes an issue with thousands and thousands of records, you could always add a WHERE clause to the inner query too; or an index to the date columns).
After we've built our inner queries, we join the projects table on the results with a LEFT JOIN for the DateListed or DateCompleted's YEAR() value - which will allow us to bring back null columns too!
For the field selections, we use the year column from our inner query to assure that we get a full list of years to display. Then, we compare the current row's DateListed & DateCompleted YEAR() value to the current year; if they're equal, add 1 - else add 0. When we GROUP BY year, our SUM() will count all of the 1's for that year for each column and give you the output you want (hopefully, of course =P).