简体   繁体   中英

SQL - identifying rows for a value in one table, where all joined rows only has a specific value

IN SQL Server, I have a result set from a joined many:many relationship.

Considering Products linked to Orders via a link table ,

Table - Products
ID
ProductName

Table - Orders
ID
OrderCountry

LinkTable OrderLines (columns not shown)

I'd like to be able to filter these results to show only the results where for an entity from one table, all the values in the other table only have a given value in a particular column. In terms of my example, for each product, I want to return only the joined rows when all the orders they're linked to are for country 'uk'

So if my linked result set is

productid, product, orderid, ordercountry
1, Chocolate, 1, uk
2, Banana, 2, uk
2, Banana, 3, usa
3, Strawberry, 4, usa

I want to filter so that only those products that have only been ordered in the UK are shown (ie Chocolate). I'm sure this should be straight-forward, but its Friday afternoon and the SQL part of my brain has given up for the day...

You could do something like this, where first you get all products only sold in one country, then you proceed to get all orders for those products

with distinctProducts as
(
    select LinkTable.ProductID
    from Orders 
    inner join LinkTable on LinkTable.OrderID = Orders.ID
    group by LinkTable.ProductID
    having count(distinct Orders.OrderCountry) = 1
)
select pr.ID as ProductID
        ,pr.ProductName
        ,o.ID as OrderID
        ,o.OrderCountry
from Products pr
inner join LinkTable lt on lt.ProductID = pr.ID
inner join Orders o on o.ID = lt.OrderID
inner join distinctProducts dp on dp.ProductID = pr.ID
where o.OrderCountry = 'UK'

Hmm. Based on Philip's earlier approach, try adding something like this to exclude rows where there's been the same product ordered in another country:

    SELECT pr.Id, pr.ProductName, od.Id, od.OrderCountry
     from Products pr
      inner join LinkTable lt
       on lt.ProductId = pr.ID
      inner join Orders od
       on od.ID = lt.OrderId
     where 
      od.OrderCountry = 'UK' 
      AND NOT EXISTS
      (
        SELECT 
          *
        FROM
          Products MatchingProducts
            inner join LinkTable lt
              on lt.ProductId = MatchingProducts.ID
            inner join Orders OrdersFromOtherCountries 
              on OrdersFromOtherCountries.ID = lt.OrderId
        WHERE
          MatchingProducts.ID = Pr.ID AND
          OrdersFromOtherCountries.OrderCountry != od.OrderCountry
      )

In the hope that some of this may be generally reusable:

;with startingRS (productid, product, orderid, ordercountry) as (
    select 1, 'Chocolate', 1, 'uk' union all
    select 2, 'Banana', 2, 'uk' union all
    select 2, 'Banana', 3, 'usa' union all
    select 3, 'Strawberry', 4, 'usa'
), countryRankings as (
select productid,product,orderid,ordercountry,
    RANK() over (PARTITION by productid ORDER by ordercountry) as FirstCountry,
    RANK() over (PARTITION by productid ORDER by ordercountry desc) as LastCountry
from
    startingRS
), singleCountry as (
    select productid,product,orderid,ordercountry
    from countryRankings
    where FirstCountry = 1 and LastCountry = 1
)
select * from singleCountry where ordercountry='uk'

In the startingRS, you put whatever query you currently have to generate the intermediate results you've shown. The countryRankings CTE adds two new columns, that ranks the countries within each productid.

The singleCountry CTE reduces the result set back down to those results where country ranks as both the first and last country within the productid (ie there's only a single country for this productid). Finally, we query for those results which are just from the uk.

If you want, for example, all productid rows with a single country of origin, you just skip this last where clause (and you'd get 3,strawberry,4,usa in your results also)


So is you've got a current query that looks like:

select p.productid,p.product,o.orderid,o.ordercountry
from product p inner join order o on p.productid = o.productid --(or however these joins work for your tables)

Then you'd rewrite the first CTE as:

;with startingRS (productid, product, orderid, ordercountry) as (
    select p.productid,p.product,o.orderid,o.ordercountry
    from product p inner join order o on p.productid = o.productid
), /* rest of query */
;WITH mytable (productid,ordercountry)
AS
(SELECT productid, ordercountry
 FROM Orders od INNER JOIN LinkTable lt ON od.orderid = lt.OrderId) 

SELECT * FROM mytable
INNER JOIN dbo.Products pr ON pr.productid = mytable.productid
WHERE pr.productid NOT IN (SELECT productid FROM mytable
                           GROUP BY productid
                           HAVING COUNT(ordercountry) > 1)
AND ordercountry = 'uk'
SELECT pr.Id, pr.ProductName, od.Id, od.OrderCountry
 from Products pr
  inner join LinkTable lt
   on lt.ProductId = pr.ID
  inner join Orders od
   on od.ID = lt.OrderId
 where od.OrderCountry = 'UK'

This probably isn't the most efficient way to do this, but ...

SELECT  p.ProductName
FROM Product p
WHERE p.ProductId IN
(
    SELECT DISTINCT ol.ProductId
    FROM OrderLines ol
    INNER JOIN [Order] o
    ON ol.OrderId = o.OrderId
    WHERE o.OrderCountry = 'uk'
)
AND p.ProductId NOT IN
(
    SELECT DISTINCT ol.ProductId
    FROM OrderLines ol
    INNER JOIN [Order] o
    ON ol.OrderId = o.OrderId
    WHERE o.OrderCountry != 'uk'
)

TestData

create table product
(
ProductId int,
ProductName nvarchar(50)
)
go

create table [order]
(
OrderId int,
OrderCountry nvarchar(50)
)
go

create table OrderLines
(
OrderId int,
ProductId int
)
go


insert into Product VALUES (1, 'Chocolate')
insert into Product VALUES (2, 'Banana')
insert into Product VALUES (3, 'Strawberry')

insert into [order] values (1, 'uk')
insert into [order] values (2, 'uk')
insert into [order] values (3, 'usa')
insert into [order] values (4, 'usa')

insert into [orderlines] values (1, 1)
insert into [orderlines] values (2, 2)

insert into [orderlines] values (3, 2)
insert into [orderlines] values (4, 3)

insert into [orderlines] values (3, 2)
insert into [orderlines] values (3, 3)

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