17
Update
I've run four different ways to do this through SQL Server 2005 and included the execution plan.
-- 269 reads, 16 CPU
SELECT *
FROM Groups
WHERE NOT EXISTS (
SELECT *
FROM People
WHERE People.GroupId = Groups.GroupId
);
-- 249 reads, 15 CPU
SELECT *
FROM Groups
WHERE (
SELECT COUNT(*)
FROM People
WHERE People.GroupId = Groups.GroupId
) = 0
-- 249 reads, 14 CPU
SELECT *
FROM Groups
WHERE GroupId NOT IN (
SELECT DISTINCT GroupId
FROM Users
)
-- 10 reads, 12 CPU
SELECT *
FROM Groups
LEFT JOIN Users ON Users.GroupId = Groups.GroupId
WHERE Users.GroupId IS NULL
So the last one, while arguably the least readable of the four, performs the best.
That comes as something as a surprise to me, and honestly I still prefer the WHERE NOT EXISTS syntax because I think it's more explicit - it reads exactly like what you're trying to do.