简体   繁体   中英

Getting rows from one table that do not have related rows in other tables meeting specific criteria without subquery

Here's SQL fiddle .

I have a table with a list of names. I want to get a list of all the names that do not have an entry in a related table with a particular status ('S') or a field in a 3rd table (related to the second) that meet a certain status ('A').

I've been able to accomplish this with the subquery shown in the SQL fiddle and below, but I'd like to do this without a subquery if possible.

SELECT * FROM name_table LEFT JOIN 
  (SELECT b.name_id FROM b LEFT JOIN c ON (b.c_id = c.id) WHERE (c.c_status = 'A') OR (b.b_status='S'))
 results ON (name_table.id=results.name_id)
WHERE results.name_id IS NULL;

In my example, I'd like rows "Ted Andrews" and "Jack Johnson"

"John Doe" is not included because table C has a row with status 'A' "Bill Smith" is not included because table B has status 'S' "Jim Scott" is not included because table C has a row with status 'A' (even though there is another row without status 'A' in table C)

 SELECT * FROM name_table 
  LEFT JOIN b ON (name_table.id = b.name_id)
 LEFT JOIN c ON (b.c_id = c.id) AND (c_status = 'A');
  WHERE (b.b_status IS NULL) OR ((b.b_status <> 'S') AND (c.id IS NULL));

was an attempt that incorrectly includes "Jim Scott"

I'd do it like this ( sqlfiddle ):

  SELECT nt.first, nt.last
    FROM name_table nt
         LEFT JOIN b ON nt.id  = b.name_id
         LEFT JOIN c ON nt.id  = c.a_id
GROUP BY first, last
  HAVING SUM(IF(b.b_status = 'S',1,0)) = 0
     AND SUM(IF(c.c_status = 'A',1,0)) = 0;

The having bit just counts instances of the status symbols that exclude people from your desired result set and requires none.

SELECT name_table.id, min(first) as first, min(last) as last
FROM name_table
   LEFT JOIN b ON name_table.id = b.name_id
   LEFT JOIN c ON b.c_id = c.id
GROUP BY name_table.id
HAVING
        count(case when b.b_status = 'S' then 1 end) = 0
    and count(case when c.c_status = 'A' then 1 end) = 0;

Since you have the requirement that you must look at multiple rows in c to determine whether you have a match you'll find yourself using some kind of aggregation. Personally I think the query isn't so horrible.

Even though you said Jim Scott should not be included, it looked to me like he does not have a b_status of 'S' or a c_status of 'A'. Is this what you're looking for?

select * from name_table
join b on name_table.id = b.name_id and b.b_status <> 'S'
join c on b.c_id = c.id and c.c_status <> 'A';

Here's a way that doesn't use subqueries, although I'd want to check the performance of this against a large data set:

SELECT a.*
FROM name_table AS a
LEFT JOIN b ON a.id = b.name_id
LEFT JOIN c ON b.c_id = c.id
GROUP BY a.id
HAVING MIN((b.b_status IS NULL OR b.b_status <> 'S') AND (c.c_status IS NULL OR c.c_status <> 'A'));

The HAVING clause evaluates to 0 or 1 for each row to indicate if it meets your criteria, and then takes the minimum of this across all names to ensure that Jim Scott is excluded - only names where all joined rows match your criteria are included.

not exists is helpful in these situations

select
      *
from name_table t
where not exists (select null from B
                  where t.id = b.name_id and b.b_status = 'S')
and not exists (select null from C
                where t.id = c.a_id and c.c_status = 'A')

Result from Demo :

| id | first |     last |
|----|-------|----------|
|  3 |   Ted | Anderson |
|  5 |  Jack |  Johnson |

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM