Linq results different than the sql result - sql-server

I am having an MVC4 web application where I am using LINQ.
I have the below query which results 53 rows in SQL.
select * from table1 t join
[table2] tpf on t.TestID=tpf.TestID
join
table3 pf on tpf.Test2ID =pf.Test2ID
join table4 pfp on
pf.Test3ID = pfp.Test3ID
join table5 p on pfp.Test5ID = p.Test5ID where t.testtypeid=1
order by pfp.Test3ID,pf.Test2ID
If I convert the same query as below it returns more records.
trvm.MyTestVMs = (
from tt in db.table1s
join ttpf in db.table2s on tt.TestID equals ttpf.TestID
join pf in db.table3s on ttpf.Test2ID equals pf.Test2ID
join pfp in db.table4s on pf.Test3ID equals pfp.Test3ID
join p in table5s on pfp.Test5ID equals p.Test5ID
where tt.testtypeid == 1
orderby pfp.Test3ID
orderby pf.Test2ID
select new MyTestVM
{
FamilyID = pf.Test2ID,
ProductID = p.Test3ID,
Desc = p.Description
}
).ToList();
The result which is getting from SQL and the above LINQ varies. Actually, there are some duplicate result I am getting from the LINQ query. What is causing this difference?

It turns out that the LINQ query is not equivalent to the SQL query posted due to the usage of a products query variable (not shown in the post) which causes one of the many-to-many link table to be included twice, thus producing more records.
One way to fix the problem is to replace products with db.Products and apply the same filters as in the query variables you were trying to reuse.
But if you want to reuse query variables, then here is the correct way to do that:
// Eliminate the need of DbFunctions.TruncateTime(dt) inside the queries
dt = dt.Date;
// Queries
var productFamilys = (
from tt in db.TestTypes
join ttpf in db.TestTypeProductFamilys on tt.TestTypeID equals ttpf.TestTypeID
join pf in db.ProductFamilys on ttpf.ProductFamilyID equals pf.ProductFamilyID
where tt.TestTypeID == TestTypeID
where DbFunctions.TruncateTime(pf.StartDate) <= dt
where DbFunctions.TruncateTime(pf.EndDate) > dt
select pf
);
var productFamilyProducts = (
from pf in productFamilys
join pfp in db.ProductFamilyProducts on pf.ProductFamilyID equals pfp.ProductFamilyID
join p in db.Products on pfp.ProductID equals p.ProductID
where DbFunctions.TruncateTime(p.StartDate) <= dt
where DbFunctions.TruncateTime(p.EndDate) > dt
select new { Family = pf, Product = p }
);
var products = (
from pfp in productFamilyProducts
select pfp.Product
);
var productFamilyProductVMs = (
from pfp in productFamilyProducts
orderby pfp.Product.ProductID, pfp.Family.ProductFamilyID
select new ProductFamilyProductVM
{
ProductFamilyID = pfp.Family.ProductFamilyID,
ProductID = pfp.Product.ProductID,
ProdDesc = pfp.Product.Description
}
);
// Results
trvm.ProductFamilys = productFamilys.ToList();
trvm.Products = products.ToList();
trvm.ProductFamilyProductVMs = productFamilyProductVMs.ToList();
Now the SQL for the last query (the one in question) looks like this
SELECT
[Project1].[ProductFamilyID] AS [ProductFamilyID],
[Project1].[ProductID] AS [ProductID],
[Project1].[Description] AS [Description]
FROM ( SELECT
[Extent2].[ProductFamilyID] AS [ProductFamilyID],
[Extent4].[ProductID] AS [ProductID],
[Extent4].[Description] AS [Description]
FROM [dbo].[TestTypeProductFamilies] AS [Extent1]
INNER JOIN [dbo].[ProductFamilies] AS [Extent2] ON [Extent1].[ProductFamilyID] = [Extent2].[ProductFamilyID]
INNER JOIN [dbo].[ProductFamilyProducts] AS [Extent3] ON [Extent2].[ProductFamilyID] = [Extent3].[ProductFamilyID]
INNER JOIN [dbo].[Products] AS [Extent4] ON [Extent3].[ProductID] = [Extent4].[ProductID]
WHERE ([Extent1].[TestTypeID] = #p__linq__0) AND ((convert (datetime2, convert(varchar(255), [Extent2].[StartDate], 102) , 102)) <= #p__linq__1) AND ((convert (datetime2, convert(varchar(255), [Extent2].[EndDate], 102) , 102)) > #p__linq__2) AND ((convert (datetime2, convert(varchar(255), [Extent4].[StartDate], 102) , 102)) <= #p__linq__3) AND ((convert (datetime2, convert(varchar(255), [Extent4].[EndDate], 102) , 102)) > #p__linq__4)
) AS [Project1]
ORDER BY [Project1].[ProductID] ASC, [Project1].[ProductFamilyID] ASC
i.e. pretty similar to the sample SQL query and should produce the same results.

Result is different because in your second query , you have two "OrderBy" and because of that, the second OrderBy it's works over the collection which is result of first "OrderBy" and is reordering the items.
Change
orderby pfp.ProductID
orderby pf.ProductFamilyID
from your second query in
orderby pfp.ProductID, pf.ProductFamilyID
to get same results

Related

EF LINQ Count by Grouped field

I have the following data schema:
With the following LINQ query:
var profiles = (
from p in context.BusinessProfiles
join u in context.Users on p.UserId equals u.Id
join addr in context.BusinessAddress on p.ProfileId equals addr.ProfileId into addrj
from addr in addrj.DefaultIfEmpty()
join pa in context.BusinessProfileActivities on p.ProfileId equals pa.ProfileId into paj
from paIfNull in paj.DefaultIfEmpty()
where p.ProfileId >= 137 && p.ProfileId <= 139
group new { p, u, addr, paIfNull }
by new {
p.ProfileId,
p.CompanyName,
p.Email,
UserEmail = u.Email,
addr.City, addr.Region,
addr.Country,
ActivityProfileId = paIfNull.ProfileId }
into pg
select new {
pg.Key.ProfileId,
pg.Key.CompanyName,
Email = pg.Key.Email ?? pg.Key.UserEmail,
pg.Key.City,
pg.Key.Region,
pg.Key.Country,
MatchingActivities = pg.Key.ActivityProfileId > 0 ? pg.Count() : 0
} into result
orderby result.MatchingActivities descending
select result
);
Which results with:
This result is corrent (ProfileId 137 has 0 activities, 138 has 1 and 139 has 2), but it produces the following SQL:
SELECT [b].[ProfileId], [b].[CompanyName], COALESCE([b].[Email], [a].[Email]) AS [Email], [b0].[City], [b0].[Region], [b0].[Country],
CASE WHEN [b1].[ProfileId] > CAST(0 AS bigint) THEN COUNT(*)
ELSE 0
END AS [MatchingActivities]
FROM [BusinessProfiles] AS [b]
INNER JOIN [AspNetUsers] AS [a] ON [b].[UserId] = [a].[Id]
LEFT JOIN [BusinessAddress] AS [b0] ON [b].[ProfileId] = [b0].[ProfileId]
LEFT JOIN [BusinessProfileActivities] AS [b1] ON [b].[ProfileId] = [b1].[ProfileId]
WHERE ([b].[ProfileId] >= CAST(137 AS bigint)) AND ([b].[ProfileId] <= CAST(139 AS bigint))
GROUP BY [b].[ProfileId], [b].[CompanyName], [b].[Email], [a].[Email], [b0].[City], [b0].[Region], [b0].[Country], [b1].[ProfileId]
ORDER BY CASE
WHEN [b1].[ProfileId] > CAST(0 AS bigint) THEN COUNT(*)
ELSE 0
END DESC
In SQL, I can avoid both CASE WHEN if I use COUNT([b1].[ProfileId]) like this:
SELECT [b].[ProfileId], [b].[CompanyName], COALESCE([b].[Email], [a].[Email]) AS [Email], [b0].[City], [b0].[Region], [b0].[Country],
COUNT([b1].[ProfileId]) AS [MatchingActivities]
FROM [BusinessProfiles] AS [b]
INNER JOIN [AspNetUsers] AS [a] ON [b].[UserId] = [a].[Id]
LEFT JOIN [BusinessAddress] AS [b0] ON [b].[ProfileId] = [b0].[ProfileId]
LEFT JOIN [BusinessProfileActivities] AS [b1] ON [b].[ProfileId] = [b1].[ProfileId]
WHERE ([b].[ProfileId] >= CAST(137 AS bigint)) AND ([b].[ProfileId] <= CAST(139 AS bigint))
GROUP BY [b].[ProfileId], [b].[CompanyName], [b].[Email], [a].[Email], [b0].[City], [b0].[Region], [b0].[Country], [b1].[ProfileId]
ORDER BY [MatchingActivities] DESC
My question is, how can I count by grouped ActivityProfileId = paIfNull.ProfileId using LINQ and get EF to generate the above SQL?
I have tried so many variations resulting mostly in EF to SQL errors.
MatchingActivities = pg.Count(t => t.ActivityProfileId!= 0)
MatchingActivities = pg.Select(t => t.paIfNull.ProfileId).Distinct().Count(),
MatchingActivities = pg.Count(t => t.paIfNull != null),
All result in errors like System.InvalidOperationException: The LINQ expression ... could not be translated. or getting MatchingActivities as 1 instead of 0.
Related Q/A:
LINQ Count returning 1 instead of zero for an empty group
Group by in LINQ
How to write left join, group by and average in c# entity framework Linq
In short you can't! EF Core still doesn't support that.
See this:
https://github.com/dotnet/efcore/issues/17376
And also See:
https://stackoverflow.com/a/61878332/9212040

Ridding this query of Hash Match Join?

I have the following query that takes around 26 rows to return 8700 rows of data.
SELECT
R.ClientReferralID,
R.ClientID,
C.FirstName,
C.LastName,
C.FullName,
dbo.fnGetLocalDate(R.ReferralDate) as ReferralDate,
RT.ReferralTypeName,
R.ReferralTypeOther,
RT2.ReferredToName,
R.ReferredToOther,
R.ReferredByID,
U.FullName as ReferredBy,
TS.TimeSpentName,
R.Notes,
L.ReferralLocationID,
L.ReferralLocationName as Location,
R.ReferralLetterSentID,
R.ReferralLetterOnFileID,
dbo.fnGetLocalDate(R.DateCreated) as DateCreated,
U2.FullName as UserCreated,
dbo.fnGetLocalDate(R.DateModified) as DateModified,
U3.FullName as UserModified
FROM
ClientReferral R
INNER JOIN Client C on
R.ClientID = C.ClientID
INNER JOIN LookUp.ReferralType RT on
R.ReferralTypeID = RT.ReferralTypeID
INNER JOIN LookUp.ReferredTo RT2 on
R.ReferredToID = RT2.ReferredToID
INNER JOIN UserAccount U on
R.ReferredByID = U.UserAccountID
INNER JOIN LookUp.TimeSpent TS on
R.TimeSpentID = TS.TimeSpentID
INNER JOIN LookUp.ReferralLocation L on
R.ReferralLocationID = L.ReferralLocationID
INNER JOIN UserAccount U2 on
R.UserCreated = U2.UserAccountID
LEFT JOIN UserAccount U3 on
R.UserModified = U3.UserAccountID
WHERE
(R.ReferralDate >= #StartDate or #StartDate is null) and
(R.ReferralDate <= #EndDate or #EndDate is null)
ORDER BY
R.DateCreated DESC
The execution plan can be viewed here:
https://www.brentozar.com/pastetheplan/?id=B1A5ji7tf
I see the most costly operation is 65% on a Hash Match Join. I was expecting the following index to improve that but no:
CREATE NONCLUSTERED INDEX [Name] ON [dbo].[ClientReferral]
(
[ClientID] ASC
)
Anyone see off hand what I can do here? Please let me know if some sample data is needed.
Try adding an index
ClientReferral ReferralDate
Or
ClientReferral ReferralDate, ClientID
ClientReferral ClientID, ReferralDate
First change
WHERE (R.ReferralDate >= #StartDate or #StartDate is null) and (R.ReferralDate <= #EndDate or #EndDate is null)
To
WHERE R.ReferralDate BETWEEN ISNULL(#StartDate,CAST(0 AS datetime2)) AND ISNULL(#EndDate,CAST(999999 AS datetime2))
After that create an index on
ClientReferral(ReferralDate, ClientID, ReferralTypeID, ReferredToID, ReferredByID, TmeSpentID, ReferralLocationID, UserCreated, UserModified, DateCreated, DateModified) INCLUDE(ClientReferralID, ReferralTypeOther, ReferredToOther, Notes, ReferralLetterSentID, ReferralLetterOnFileID)
Also show the code for
fnGetLocalDate

Combining multiple SQL's into a single SQL

Hi have the following queries
(SELECT COUNT(DISTINCT KUNDNR) CHECKED_CUSTOMER from CLNT0001.TCM_CHECK_SUMMARY
where '20170322000000000' <= HISTVON and HISTVON < '20170323000000000' and INSTITUTSNR='0001')
and
SELECT clientNumber
,creationDate
,customerNumber
,checkedCustomer
,CLNT0001.TCM_CHECK_SUMMARY.COUNTRY_CODE countryCode
,CLNT0001.TCM_CHECK_SUMMARY.PST_KURZTEXT personStatus
,CLNT0001.TCM_CASE_COUNTRY_GROUP.COUNTRY_CODE homeCountryCode
,CLNT0001.TCM_CASE_COUNTRY_GROUP.PST_LFD_NR personStatusId
,CLNT0001.TCM_CASE_COUNTRY_GROUP.REGULATION regulation
,caseStatus
,COC_SCORE_COUNT cocCaseCount
FROM (
SELECT GEPRUEFT_JN checkedCustomer
,INSTITUTSNR clientNumber
,KUNDNR customerNumber
,CASE_STATUS caseStatus
,MAX(CREATION_DATE) creationDate
FROM CLNT0001.TAXACTCASE
WHERE GEPRUEFT_JN = 'J' AND CREATION_DATE>='20170322000000000' AND
CREATION_DATE<='20170323000000000'
GROUP BY KUNDNR
,INSTITUTSNR
,GEPRUEFT_JN
,CASE_STATUS
) T1
INNER JOIN CLNT0001.TCM_CHECK_SUMMARY ON T1.customerNumber = CLNT0001.TCM_CHECK_SUMMARY.KUNDNR
INNER JOIN CLNT0001.TCM_CASE_COUNTRY_GROUP ON T1.customerNumber = CLNT0001.TCM_CASE_COUNTRY_GROUP.KUNDNR
WHERE T1.creationDate <= CLNT0001.TCM_CHECK_SUMMARY.HISTBIS
AND T1.creationDate >= CLNT0001.TCM_CHECK_SUMMARY.HISTVON
I need the CHECKED_CUSTOMER column as a part of the second query's result set, i am not able to figure out a way to do this, is this possible ?
SELECT clientNumber,creationDate,customerNumber,checkedCustomer
,CLNT0001.TCM_CHECK_SUMMARY.COUNTRY_CODE countryCode
,CLNT0001.TCM_CHECK_SUMMARY.PST_KURZTEXT personStatus
,CLNT0001.TCM_CASE_COUNTRY_GROUP.COUNTRY_CODE homeCountryCode
,CLNT0001.TCM_CASE_COUNTRY_GROUP.PST_LFD_NR personStatusId
,CLNT0001.TCM_CASE_COUNTRY_GROUP.REGULATION regulation
,caseStatus,COC_SCORE_COUNT cocCaseCount ,CHECKED_CUSTOMER
FROM (
SELECT GEPRUEFT_JN checkedCustomer,INSTITUTSNR clientNumber ,KUNDNR customerNumber ,CASE_STATUS caseStatus,MAX(CREATION_DATE) creationDate,COUNT(DISTINCT b.KUNDNR) CHECKED_CUSTOMER
FROM CLNT0001.TAXACTCASE
LEFT JOIN CLNT0001.TCM_CHECK_SUMMARY b ON CLNT0001.TAXACTCASE.KUNDNR=b.KUNDNR
WHERE GEPRUEFT_JN = 'J' AND CREATION_DATE>='20170322000000000' AND
CREATION_DATE<='20170323000000000'
GROUP BY KUNDNR,INSTITUTSNR ,GEPRUEFT_JN,CASE_STATUS
) T1 INNER JOIN CLNT0001.TCM_CHECK_SUMMARY ON T1.customerNumber = CLNT0001.TCM_CHECK_SUMMARY.KUNDNR
INNER JOIN CLNT0001.TCM_CASE_COUNTRY_GROUP ON T1.customerNumber = CLNT0001.TCM_CASE_COUNTRY_GROUP.KUNDNR
WHERE T1.creationDate <= CLNT0001.TCM_CHECK_SUMMARY.HISTBIS
AND T1.creationDate >= CLNT0001.TCM_CHECK_SUMMARY.HISTVON

Turn Date into Column with Pivot

Im not good at using Pivot but i think that's the only way to solve my Problem.
I have this SQL
SELECT DISTINCT ADR_Adressen.AdressNrADR
, LEFT(ADR_Adressen.Name, 3) AS Name
, LEFT(ADR_Adressen.Vorname, 3) AS Vorname
, CRM_Aufgaben.TerminVon
, LAG_Artikel.ArtikelNrLAG
, CRM_AufgabenLink.MitNrPRO
FROM ADR_Adressen
INNER JOIN PRO_Auftraege ON ADR_Adressen.AdressNrADR = PRO_Auftraege.Kunde
INNER JOIN CRM_Aufgaben ON PRO_Auftraege.AuftragNrPRO = CRM_Aufgaben.AuftragNrPRO
INNER JOIN CRM_Status ON CRM_Aufgaben.StatusCRM = CRM_Status.StatusCRM
INNER JOIN LAG_Artikel ON CRM_Aufgaben.ArtikelNrLAG = LAG_Artikel.ArtikelNrLAG
INNER JOIN ADR_GruppenLink ON ADR_Adressen.AdressNrADR = ADR_GruppenLink.AdressNrADR
INNER JOIN ADR_Gruppen ON ADR_GruppenLink.GruppeADR = ADR_Gruppen.GruppeADR
INNER JOIN CRM_AufgabenLink ON CRM_Aufgaben.AufgabenNrCRM = CRM_AufgabenLink.AufgabenNrCRM
WHERE { d '2016-03-07'} <= CRM_Aufgaben.TerminVon
AND { d '2016-03-11'} + 1 >= CRM_Aufgaben.TerminBis
AND CRM_AufgabenLink.MitNrPRO != 0
AND ADR_Gruppen.GruppeADR IN ( 'KIND' )
This is my result:
My wish is to get a Output like this:
The different Dates in TerminVon has to be Columns with the Values from ArtikelNrLAG+MitNrPRO. If the same AdressNrADR has more then one TerminVon on the same Date i have to make more rows. (Example where Name = Boc,Alt)
Can someone help me please =)
To PIVOT what you have, you can use a query similar to this.
SELECT AdressNrADR,
Name,
Vorname,
[2016-03-07],
[2016-03-08],
[2016-03-09],
[2016-03-10],
[2016-03-11]
FROM (
SELECT DISTINCT
ADR_Adressen.AdressNrADR,
LEFT(ADR_Adressen.Name,3) AS Name,
LEFT(ADR_Adressen.Vorname,3) AS Vorname,
CONVERT(VARCHAR(10), CRM_Aufgaben.TerminVon, 120) AS TerminVon, -- Convert date to yyyy-mm-dd format
LAG_Artikel.ArtikelNrLAG + '+' + CRM_AufgabenLink.MitNrPRO AS [Value], -- Combine column values
ROW_NUMBER() OVER
(PARTITION BY AdressNrADR,
LEFT(ADR_Adressen.Name,3),
LEFT(ADR_Adressen.Vorname,3),
CAST(CRM_Aufgaben.TerminVon AS DATE)
ORDER BY CRM_Aufgaben.TerminVon) Rn -- So we can get 1 row per time value
FROM ADR_Adressen
INNER JOIN PRO_Auftraege ON ADR_Adressen.AdressNrADR = PRO_Auftraege.Kunde
INNER JOIN CRM_Aufgaben ON PRO_Auftraege.AuftragNrPRO = CRM_Aufgaben.AuftragNrPRO
INNER JOIN CRM_Status ON CRM_Aufgaben.StatusCRM = CRM_Status.StatusCRM
INNER JOIN LAG_Artikel ON CRM_Aufgaben.ArtikelNrLAG = LAG_Artikel.ArtikelNrLAG
INNER JOIN ADR_GruppenLink ON ADR_Adressen.AdressNrADR = ADR_GruppenLink.AdressNrADR
INNER JOIN ADR_Gruppen ON ADR_GruppenLink.GruppeADR = ADR_Gruppen.GruppeADR
INNER JOIN CRM_AufgabenLink ON CRM_Aufgaben.AufgabenNrCRM = CRM_AufgabenLink.AufgabenNrCRM
WHERE { d '2016-03-07'} <= CRM_Aufgaben.TerminVon
AND { d '2016-03-11'} + 1 >= CRM_Aufgaben.TerminBis
AND CRM_AufgabenLink.MitNrPRO != 0
AND ADR_Gruppen.GruppeADR IN ('KIND')
) t
PIVOT (
MAX([Value])
FOR TerminVon IN ([2016-03-07],[2016-03-08],[2016-03-09],[2016-03-10],[2016-03-11])
) p
If you get that query to work. Your next step would be to make it Dynamic.
The difficult part of using t-sql's pivot functionality is that the output column names have to be hard coded. In your example we would need to know the value of each date and use that in the query in order to get the matching values by date. Fortunately other fine developers have experienced this frustration for us and have created scripts that will generate a dynamic pivot. I have included two links that will help you on your way.
https://www.mssqltips.com/sqlservertip/2783/script-to-create-dynamic-pivot-queries-in-sql-server/
http://sqlhints.com/2014/03/18/dynamic-pivot-in-sql-server/

paging over SELECT UNION super slow and killing my server

I have an SP that returns paged data from a query that contains a UNION. This is killing my DB and taking 30 seconds to run sometimes, am I missing something obvious here? What can I do to improve it's performance?
Tables Involved: Products, Categories, CategoryProducts
Goal:
Any Products that are not in a Category or have been deleted from a category UNION all Products currently in a category and page over them for a web service.
I have Indexes on all columns that I am joining on and there are 427,996 Products, 6148 Categories and 409,691 CategoryProducts in the database.
Here is my query that is taking between 6, and 30 seconds to run:
SELECT * FROM (
SELECT ROW_NUMBER() OVER(ORDER BY Products.ItemID, Products.ManufacturerID) AS RowNum, *
FROM
(
SELECT Products.*,
CategoryID = NULL, CategoryName = NULL,
CategoryProductID = NULL,
ContainerMinimumQuantity =
CASE COALESCE(Products.ContainerMinQty, 0)
WHEN 0 THEN Products.OrderMinimumQuantity
ELSE Products.ContainerMinQty
END
Products.IsDeleted,
SortOrder = NULL
FROM CategoryProducts RIGHT OUTER JOIN Products
ON CategoryProducts.ManufacturerID = Products.ManufacturerID
AND CategoryProducts.ItemID = Products.ItemID
WHERE (Products.ManufacturerID = #ManufacturerID)
AND (Products.ModifiedOn > #tStamp )
AND ((CategoryProducts.IsDeleted = 1) OR (CategoryProducts.IsDeleted IS NULL))
UNION
SELECT Products.*,
CategoryProducts.CategoryID , CategoryProducts.CategoryName,
CategoryProducts.CategoryProductID ,
ContainerMinimumQuantity =
CASE COALESCE(Products.ContainerMinQty, 0)
WHEN 0 THEN Products.OrderMinimumQuantity
ELSE Products.ContainerMinQty
END
CategoryProducts.IsDeleted,
CategoryProducts.SortOrder
FROM Categories INNER JOIN
CategoryProducts ON Categories.CategoryID = CategoryProducts.CategoryID INNER JOIN
Products ON CategoryProducts.ManufacturerID = Products.ManufacturerID
AND CategoryProducts.ItemID = Products.ItemID
WHERE (Products.ManufacturerID = #ManufacturerID)
AND (Products.ModifiedOn > #tStamp OR CategoryProducts.ModifiedOn > #tStamp))
AS Products) AS C
WHERE RowNum >= #StartRow AND RowNum <= #EndRow
Any insight would be greatly appreciated.
If I read your situation correctly, the only reason for having two distinct queries is treatment of missing/deleted CategoryProducts. I tried to address this issue by left join with IsDeleted = 0 to bring all deleted CategoryProducts to nulls, so I don't have to test them again. ModifiedOn part got another test for null for missing/deleted Categoryproducts you wish to retrieve.
select *
from (
SELECT
Products.*,
-- Following three columns will be null for deleted/missing categories
CategoryProducts.CategoryID,
CategoryProducts.CategoryName,
CategoryProducts.CategoryProductID ,
ContainerMinimumQuantity = COALESCE(nullif(Products.ContainerMinQty, 0),
Products.OrderMinimumQuantity),
CategoryProducts.IsDeleted,
CategoryProducts.SortOrder,
ROW_NUMBER() OVER(ORDER BY Products.ItemID,
Products.ManufacturerID) AS RowNum
FROM Products
LEFT JOIN CategoryProducts
ON CategoryProducts.ManufacturerID = Products.ManufacturerID
AND CategoryProducts.ItemID = Products.ItemID
-- Filter IsDeleted in join so we get nulls for deleted categories
-- And treat them the same as missing ones
AND CategoryProducts.IsDeleted = 0
LEFT JOIN Categories
ON Categories.CategoryID = CategoryProducts.CategoryID
WHERE Products.ManufacturerID = #ManufacturerID
AND (Products.ModifiedOn > #tStamp
-- Deleted/missing categories
OR CategoryProducts.ModifiedOn is null
OR CategoryProducts.ModifiedOn > #tStamp)
) C
WHERE RowNum >= #StartRow AND RowNum <= #EndRow
On a third look I don't see that Category is used at all except as a filter to CategoryProducts. If this is the case second LEFT JOIN should be changed to INNER JOIN and this section should be enclosed in parenthessis.

Resources