I have the following SQL query to return all Customers who have no OrderLines with no Parts assigned – i.e. I only want the customers within which every order line of every order has no parts assigned – (in the actual problem I am dealing with a different domain but have translated to customers/orders to illustrate the problem)
SELECT c.Customer_PK
FROM Customers c
INNER JOIN Orders o
ON c.Customer_PK = o.Customer_FK
LEFT OUTER JOIN OrderLines l
ON o.Order_PK = l.Order_FK
LEFT OUTER JOIN Parts p
ON l.OrderLine_PK = p.OrderLine_FK
GROUP BY c.Customer_PK
HAVING COUNT(p.Part_PK) = 0
The best I have come up with in LINQ is as follows:
Dim qry =
(From c In context.Customers
Select New With { c.Customer_PK,
.CountParts =
(From o In c.Orders
From l In o.OrderLines
Select l.Parts.Count).DefaultIfEmpty.Sum})
qry = (From grp In qry
Where grp.CountParts = 0
Select grp.Customer_PK)
This works but generates less than optimal SQL – it is doing a subquery for Count on each row of the customers query rather than using Group By and Having. I tried making the LINQ Group By syntax work but it kept putting the filter as a WHERE not a HAVING clause.
Any ideas?
Edit in response to Answers below:
I am accepting JamieSee’s answer as it addresses the stated problem, even though it does not produce the GROUP BY HAVING query I originally had.
Thanks Peter and Nick for your input on this. I am a VB developer so I have had a crack translating your code to VB, this is the closest I got to but it does not quite produce the desired output:
Dim qry = From c In context.Customers
Group Join o In context.Orders On c.Customer_PK Equals o.Customer_FK
Into joinedOrders = Group
From jo In joinedOrders.DefaultIfEmpty
Group Join l In context.OrderLines On jo.Order_PK Equals l.Order_FK
Into joinedLines = Group
From jl In joinedLines.DefaultIfEmpty
Group c By Key = New With {c.Customer_PK, jl} Into grp = Group
Where Key.jl Is Nothing OrElse Not Key.jl.Parts.Any
Select c.Customer_PK
The problem I had is that I have to push “jl” into the Group By “Key” so I can reference it from the Where clause, otherwise the compiler cannot see that variable or any of the other variables appearing before the Group By clause.
With the filter as specified I get all customers where at least one order has lines with no parts rather than only customers with no parts in any order.
Given that you don’t care about the counts, only the resulting customers, consider the folllowing restatement of the problem:
Identify all Customers who do not have any Orders that have Lines with Parts.
This yields:
This should yield emitted SQL that is roughly equivalent to the following:
To get the SQL you were trying to reproduce, I’d start by trying the following:
Note that in VB the
joinhere would be replaced byGroup Join.