Use inner join if record exists otherwise use left join

Viewed 5573

I have the following table structure:

dbo.Owner

OwnerID   OwnerName  
  1        John   
  2        Marie
  3        Alex

and dbo.Pet

PetID PetTag Status OwnerID
  1    A341  Active    1  
  2    A342  Inactive  1  
  3    A343  Active    2
  4    A345  Active    2

I need to return all owners who have only Active pets or no pets.

So in this example above I need to Return Owner 2 (All pets are active) and Owner 3 (No pets)

I will be pulling data in C# using Entity Framework but plain SQL will be sufficient.

Here's what I came up with so far:

select mi.* from Owner o
join Pet p
on o.OwnerID= p.OwnerID
where o.Status='Active'
union select * from Owner
where OwnerID not in (select OwnerID from Pet)

Now, this query above works but it includes OwnerID = 1. and Also I was wondering if there's a way to do this in 1 query without union.

7 Answers

If your only values for Status are "Active" and "Inactive", you can actually simplify your query. When you say:

I need to return all owners who have only Active pets or no pets.

This would then actually translate to:

I need to return all owners who have no Inactive pets.

Then your query becomes much easier.

In an Entity Framework query:

owners = context.Owners
    .Where(o => !o.Pets.Any(p => p.Status == "Inactive"))
    .ToList();

The SQL query generated by this is:

SELECT 
    [Extent1].[OwnerID] AS [OwnerID], 
    [Extent1].[OwnerName] AS [OwnerName]
    FROM [dbo].[Owners] AS [Extent1]
    WHERE  NOT EXISTS (SELECT 
        1 AS [C1]
        FROM [dbo].[Pets] AS [Extent2]
        WHERE ([Extent1].[OwnerID] = [Extent2].[OwnerID]) AND (N'Inactive' = [Extent2].[Status])
    )

Or to remove the clutter:

SELECT 
    OwnerID,
    OwnerName
    FROM Owners o
    WHERE  NOT EXISTS (SELECT 
        1
        FROM Pets p
        WHERE (o.OwnerID = p.OwnerID AND p.Status = 'Inactive')
    )

If you have more values for Status, you could use (Entity Framework):

owners = context.Owners
    .Where(o => o.Pets.Any(p => p.Status == "Active") || !o.Pets.Any())
    .Where(o => !o.Pets.Any(p => p.Status == "Inactive" /* || p.Status == "Lost" and any other values */))
    .ToList();

which would generate the SQL query:

SELECT 
    [Extent1].[OwnerID] AS [OwnerID], 
    [Extent1].[OwnerName] AS [OwnerName]
    FROM [dbo].[Owners] AS [Extent1]
    WHERE (( EXISTS (SELECT 
        1 AS [C1]
        FROM [dbo].[Pets] AS [Extent2]
        WHERE ([Extent1].[OwnerID] = [Extent2].[OwnerID]) AND (N'Active' = [Extent2].[Status])
    )) OR ( NOT EXISTS (SELECT 
        1 AS [C1]
        FROM [dbo].[Pets] AS [Extent3]
        WHERE [Extent1].[OwnerID] = [Extent3].[OwnerID]
    ))) AND ( NOT EXISTS (SELECT 
        1 AS [C1]
        FROM [dbo].[Pets] AS [Extent4]
        WHERE ([Extent1].[OwnerID] = [Extent4].[OwnerID]) AND (N'Inactive' = [Extent4].[Status])
    ))

You'd want to test that for performance and there may well be better ways, but it gives the desired result. It does assume you have foreign key/navigation property though.

Try the following query:

select o.*
from dbo.owner o
where not exists(
  select *
  from dbo.pet p
  where p.ownerid=o.ownerid and
    p.status='Inactive'
);
SELECT  OwnerID, OwnerName 
FROM Owner 
WHERE OwnerID NOT IN (
SELECT OwnerID from Pet
WHERE Status='Inactive'

This simple query will do the thing.

OwnerId        OwnerName 
2              Marie
3              Alex

And if you want to select owner with atleast one ACTIVE or NO PET then use the below query.

SELECT o.OwnerID o.OwnerName
FROM Owner o 
LEFT JOIN Pet p 
ON o.OwnerID= p.OwnerID 
AND (p.Status='Active' 
OR p.OwnerID is NULL)

OwnerId        OwnerName
1              John
2              Marie
3              Alex

This query will return OWNER name until that Owner's all pets are INACTIVE

Now for another case..

If there is a chance for your table to have OwnerId as NULL in Pets Table. Kindly use the below Query. (Mysql)

SELECT OwnerID, OwnerName 
FROM Owner 
WHERE OwnerID NOT IN (
   SELECT IFNULL(OwnerID,0) from Pet
   WHERE Status='Inactive');

ADDED IFNULL() in subquery.

SQLFIDDLE

Interestingly it is possible to do this with a LEFT JOIN. I've no idea about whether this performs differently to the NOT EXISTs queries suggested by other answers.

CREATE TABLE [Owner] (
    OwnerID int PRIMARY KEY,
    OwnerName nvarchar(50)
);

INSERT INTO [Owner]
VALUES
  (1, 'John'), 
  (2, 'Marie'),
  (3, 'Alex');

CREATE TABLE Pet (
    PetID int PRIMARY KEY, 
    PetTag nvarchar(10), 
    Status nvarchar(30), 
    OwnerID int FOREIGN KEY REFERENCES [Owner](OwnerID)
);

INSERT INTO Pet
VALUES
  (1,'A341','Active', 1),  
  (2,'A342','Inactive', 1),  
  (3,'A343','Active', 2),
  (4,'A345','Active', 2);

SELECT * FROM [Owner];
SELECT * FROM Pet;

SELECT
    o.*
FROM
    [Owner] o
    LEFT JOIN Pet p
        ON o.OwnerID = p.OwnerID
        AND p.Status <> 'Active'
WHERE
    p.OwnerID IS NULL;

DROP TABLE Pet, [Owner];
select DISTINCT 
     o.Id 
FROM Owner o
LEFT JOIN Pet p ON o.OwnerID= p.OwnerID
where p.Status='Active' OR p.OwnerID IS NULL
SELECT DISTINCT RESULT FROM (
                            SELECT    CASE WHEN POID is NULL 
                                           THEN OID
                                           WHEN OID NOT IN (SELECT DISTINCT 
                                                            OwnerID from Pet 
                                                            WHERE Status='Inactive')
                                           THEN OID
                                      END AS RESULT
                            FROM (
                                     SELECT O.OwnerID as OID, P.OwnerID as POID
                                     FROM Owner o 
                                     LEFT JOIN Pet p 
                                     ON o.OwnerID= p.OwnerID 
                                  ) T
                            )T2 WHERE  RESULT IS NOT NULL

SQL Fiddle

Interesting that although you tagged it entity-framework, most answers don't come up with the simplifications that entity-framework offers you.

There is a one-to-many relationship between Owners and Pets. Every Owner has zero or more Pets, every Pet belongs to exactly one Owner.

If you have configured your entity framework classes correctly for a one-to-many relationship, they will be like:

class Owner
{
    public int Id {get; set;}
    // every Owner has zero or more Pets:
    public virtual ICollection<Pet> Pets {get; set;}

    ... // other properties
}
class Pet
{
    public int Id {get; set;}
    // every Pet belongs to exactly one Owner, using foreign key:
    public int OwnerId {get; set;}
    public Owner Owner {get; set;}
}
class MyDbConnection : DbConnection
{
    public DbSet<Owner> Owners {get; set;}
    public DbSet<Pet> Pets {get; set;}
}

This is enough for entity framework to recognize that you designed the one-to-many relationship. Whenever needed, entity framework will do the correct join for you.

I need to return all owners who have only Active pets or no pets.

This is the same collections as:

I need to return all owners who don't have Inactive pets

(note to self: Owners without Pets, surely don't have Inactive Pets!)

If you've set up the classes correctly, the queries will be much more readable. You can think of collections, instead of tables that are related towards each other using IDs

using (var dbConnection = new MyDbConnection())
{
    var requestedOwners = dbConnection.Owners              // Give me all Owners
        .Where(owner => !owner.Pets.Any()                  // that have no Pets at all
          || owner.Pets.All(pet => pet.Status == Active)); // or have only active Pets
}

Entity Framework will recognize that this needs a join and will translate this into the correct inner joins for you.

The second query is even simpler, and probably faster because you can continue to the next Owner as soon as an Inactive Pet is found

var ownersWithoutInactivePets = dbContext.Owners   // give me all Owners
    .Where(owner => !owner.Pets                    // that don't have
        .Any(pet => pet.Status == Inactive);       // any inactive Pets

Again, entity framework will do the join for you

Related