SQL Server CTE hierarchy keyword search - sql-server

I've run into a tricky issue with recursive searching in an eCommerce shop stored procedure. Basically this single procedure will return all products factoring in basic filters and paging, and using a parent/child category table to perform recursive checks down the hierarchy. This is works beautifully and the CTE's run extremely fast, however the recent addition of a keyword search which needs to search across the Category Name, Product Name, and Style Number has caused dramas.
This seemed quite trivial at first as the 1st CTE already generates a table of all relevant categories in the hierarchy based on the supplied #categoryid and then joins onto the rest of the Product specific tables for all filtering. The Product Name and Style Number search works fine, but I cannot for the life of me get a Category Name search to work because it needs to search the category tree for any matches down the hierarchy tree starting from the top.
EDIT: I'm now thinking it may just be a lot easier to add a "tag" table against Products which stores all keyword related tags such as category name, product name and style etc and search directly against the tags.
For example a subset of the Category hierarchy looks like this:
Mens
- Polos
- Jerseys
- Pants
Womens
- Pants
- Shirts
- Polos
Supporters
- State Of Origin
- Mens
- Womens
- Kids
- Bulldogs
- Jerserys
- Pants
- Shirts
- Caps
- Warratahs
In my sample code below i am passing a search term of "origin mens" which should return all products within the "State of Origin" category that are also within the "Mens" category. The only thing it matches on is Product Names that start with "Origin" and nothing else because the category at the product level is not "State of Origin" as this is the parent. Any help here would be fantastic!
-- Variable Declarations
DECLARE #categoryid int
DECLARE #minprice int
DECLARE #maxprice int
DECLARE #sizefilter int
DECLARE #colourfilter int
DECLARE #searchstring varchar(255)
DECLARE #totalrows int
-- Variables values for testing
SET #categoryid = 0
SET #minprice = 0
SET #maxprice = 0
SET #sizefilter = 0
SET #colourfilter = 0
SET #searchstring = 'origin mens'
-- Setup paging table
DECLARE #indextable table (rownum int identity(1,1), recordid int);
BEGIN
-- First run CTE recursively over all categories in hierarchy
;WITH categoryCTE AS (
SELECT cat.id as CategoryId, cat.name as CategoryName
FROM dbo.shopcategory AS cat
WHERE (#categoryid = 0 OR cat.id = #categoryid)
AND cat.isenabled = 1
UNION ALL
SELECT child.id as CategoryId, child.name as CategoryName
FROM dbo.ShopCategory AS child
INNER JOIN categoryCTE AS parent
ON child.parentid = parent.CategoryId
WHERE child.isenabled = 1
),
-- Now join CTE onto products tables via linker product_shopcategory
productsCTE AS (
SELECT p.id, ppc.shopcategoryid, ppc.listorder as catlistorder
FROM categoryCTE as cat
INNER JOIN product_shopcategory ppc ON ppc.shopcategoryid = cat.CategoryId
INNER JOIN product p ON ppc.productid = p.id
INNER JOIN productlocality pl ON pl.productid = p.id
-- ** SEARCH - Join List to Table function of keywords
INNER JOIN dbo.udf_parseList(#searchString, ' ') s
ON (cat.CategoryName + p.Name + p.stylenumber LIKE '%' + s.array_Value + '%')
LEFT JOIN product_quantity pq ON pq.productid = p.id AND pq.localityid = #localityid
LEFT JOIN productcolour pc ON pc.productid = p.id
LEFT JOIN productcolourswatch pcs ON pc.productcolourswatchid = pcs.id
LEFT JOIN product_productsize pps ON pps.productid = p.id
LEFT JOIN productsize ps ON pps.productsizeid = ps.id
WHERE p.isenabled = 1
AND pq.quantity > 1
AND (pc.isenabled IS NULL OR pc.isenabled = 1)
AND (#minprice = 0 OR pl.price >= #minprice)
AND (#maxprice = 0 OR pl.price <= #maxprice)
-- Colour Group Filters
AND (#colourfilter = 0
OR
(pcs.swatchgroupid = #colourfilter AND (pq.productcolourid = pc.id AND pq.quantity > 0))
)
-- Size Group Filters
AND (#sizefilter = 0
OR
(ps.sizegroupid = #sizefilter AND (pq.productsizeid = pps.productsizeid AND pq.quantity > 0))
)
)
-- Create Paging table of results and strip out duplicates with group by
INSERT INTO #indextable (recordid)
SELECT DISTINCT id
FROM productsCTE
GROUP BY id
ORDER BY id;

Finally solved it! I almost went down the path of creating a full tag table structure so that i could search directly against keyword tags rather than the direct data, however in trying to script a product tags table containing a nesting of the category hierarchy I found the solution which was quite simple.
In the solution procedure below i've created a new column in the CategoryCTE to hold a comma delimited list of category names that is built recursively and this then tracks the full tree for the supplied CategoryId. Now that i have a comma delimited list of Category names, I can then factor this into my 2nd CTE and perform a standard LIKE clause factoring in Product Name, Style Number, and Category Names. Finally in order to make this search a little smarter i made the keyword search inclusive of all keywords so that "mens origin" will only return products matching both of these keywords as oppose to any matches, and this was done using the NOT EXISTS clause.
Hope this helps someone else it performs very fast as well!
-- Variable Declarations
DECLARE #categoryid int
DECLARE #minprice int
DECLARE #maxprice int
DECLARE #sizefilter int
DECLARE #colourfilter int
DECLARE #searchstring varchar(255)
DECLARE #totalrows int
-- Variables values for testing
SET #categoryid = 0
SET #minprice = 0
SET #maxprice = 0
SET #sizefilter = 0
SET #colourfilter = 0
SET #searchstring = 'origin mens'
-- Setup paging table
DECLARE #indextable table (rownum int identity(1,1), recordid int);
BEGIN
-- First run CTE recursively over all categories in hierarchy inclusive of supplied categoryId
;WITH categoryCTE AS (
SELECT cat.id as CategoryId, cat.name as CategoryName,
CONVERT(varchar(255),cat.name) AS Tags
FROM dbo.shopcategory AS cat
WHERE (#categoryid = 0 OR cat.id = #categoryid)
AND cat.isenabled = 1
UNION ALL
SELECT child.id as CategoryId, child.name as CategoryName, CONVERT(varchar(255),
parent.Tags + CONVERT(varchar(32),',' + child.name)) AS Tags
FROM dbo.ShopCategory AS child
INNER JOIN categoryCTE AS parent
ON child.parentid = parent.CategoryId
WHERE child.isenabled = 1
),
-- Now join CTE onto products tables via linker product_shopcategory
productsCTE AS (
SELECT p.id, ppc.shopcategoryid, ppc.listorder as catlistorder
FROM categoryCTE as cat
INNER JOIN product_shopcategory ppc ON ppc.shopcategoryid = cat.CategoryId
INNER JOIN product p ON ppc.productid = p.id
INNER JOIN productlocality pl ON pl.productid = p.id
LEFT JOIN product_quantity pq ON pq.productid = p.id AND pq.localityid = #localityid
LEFT JOIN productcolour pc ON pc.productid = p.id
LEFT JOIN productcolourswatch pcs ON pc.productcolourswatchid = pcs.id
LEFT JOIN product_productsize pps ON pps.productid = p.id
LEFT JOIN productsize ps ON pps.productsizeid = ps.id
WHERE p.isenabled = 1
AND pq.quantity > 1
AND (pc.isenabled IS NULL OR pc.isenabled = 1)
AND pl.localityid = #localityid
AND (#minprice = 0 OR pl.price >= #minprice)
AND (#maxprice = 0 OR pl.price <= #maxprice)
-- Keyword Search filter
AND (#searchstring = '' OR NOT EXISTS
(
SELECT NULL
FROM dbo.udf_parseList(#searchString, ' ')
WHERE cat.Tags + p.Name + p.stylenumber + pc.stylenumber NOT LIKE '%' + array_Value + '%'
)
)
-- Colour Group Filters
AND (#colourfilter = 0
OR
(pcs.swatchgroupid = #colourfilter AND (pq.productcolourid = pc.id AND pq.quantity > 0))
)
-- Size Group Filters
AND (#sizefilter = 0
OR
(ps.sizegroupid = #sizefilter AND (pq.productsizeid = pps.productsizeid AND pq.quantity > 0))
)
)
-- Create Paging table of results and strip out duplicates with group by
INSERT INTO #indextable (recordid)
SELECT DISTINCT id
FROM productsCTE
GROUP BY id
ORDER BY id;

Related

Optimizing query with huge amount of data

How can I optimize the query. I looked at the execution plan and created all the index. Every table has huge data. And this query execution time is very large. By looking at the query could you please suggest where can I optimize more.
If I give little background of the query the structure like:
There are many companies
Each company can have multiple managers
Data is in pagination format
Filter on #parent_manager so another temp table created parent_manager_filter just to use for the filtering purpose as #parent_manager has name in "," separated format
CREATE TABLE #parent_manager
(
cid NUMERIC(18) PRIMARY KEY,
name NVARCHAR(MAX),
code NVARCHAR(MAX)
);
CREATE INDEX cte_parent_manager ON #parent_manager(cid);
CREATE TABLE #parent_manager_filter
(
cid NUMERIC(18),
name NVARCHAR(1000),
code NVARCHAR(1000)
);
CREATE INDEX cte_parent_manager_filter_idx ON #parent_manager_filter(cid);
INSERT INTO #parent_manager
SELECT DISTINCT
mgrc.cid,
name = CAST (STUFF ((SELECT ', ' + CAST(c.company_name AS varchar(2000))
FROM manager_company mc
INNER JOIN company c ON (mc.mgr_cid = c.cid )
WHERE mc.cid = mgrc.cid
AND c.company_name IS NOT NULL
FOR XML PATH ('')), 1, 1, '') AS VARCHAR(2000)),
code = CAST (STUFF ((SELECT ', ' + CAST(c.code AS varchar(2000))
FROM manager_company mc
INNER JOIN company c ON (mc.mgr_cid = c.cid )
WHERE mc.cid = mgrc.cid
AND c.company_name IS NOT NULL
FOR XML PATH ('')), 1, 1, '') AS VARCHAR(2000))
FROM
manager_company mgrc
INNER JOIN
company c ON (mgrc.mgr_cid = c.cid )
JOIN
handler h ON (c.handlerId = h.handlerid )
WHERE
h.handlerid = 5800657002370
INSERT INTO #parent_manager_filter
SELECT DISTINCT
mc.cid,
c.company_name as name,
c.code as code
FROM
manager_company mc
INNER JOIN
company c ON (mc.mgr_cid = c.cid )
JOIN
handler h ON (h.handlerid = c.handlerid)
WHERE
h.handlerid = 5800657002370 ;
WITH company AS
(
SELECT DISTINCT
c.cid AS cid,
parentManager.name AS MANAGER_NAME,
parentManager.code AS code
FROM
company c
LEFT JOIN
#parent_manager parentManager ON (parentManager.cid = c.cid)
LEFT JOIN
# parent_manager_filter parentManagerFilter ON (parentManagerFilter.cid = c.cid)
WHERE
parentManagerFilter.name IN (:managerList)
),
total_rows AS
(
SELECT
COUNT(*) OVER () AS TOTALCOUNT,
ROW_NUMBER() OVER (ORDER BY company_name ASC) AS rnum,
grid.*
FROM
company grid
)
SELECT *
FROM total_rows rnum
WHERE rnum >= 1
AND rnum <= 10
DROP TABLE #parent_manager;
DROP TABLE #parent_manager_filter;
If you are building up temp tables then I would make sure you don't miss a clustered index, else your temp table is simply a heap. You don't have one covering the filter table.
INSERT INTO #parent_manager_filter ...
CREATE CLUSTERED INDEX cte_parent_manager_filter On #parent_manager_filter(cid);

Query works as expected, SSRS finds error?

This question was closed because someone thought it was the same issue as SSRS multi-value parameter using a stored procedure
But it is not. My report is not a stored procedure and thus, behaves differently. Also, this issue describes a result of getting no results if multi-valued params are used and that too is inaccurate for this scenario. So I'll try posting this again.
My report for the most part works. It is when I select more than one value from either of 2 specific params (#global, #manual) that I get this error:
Here is the SQL:
DECLARE #STATE VARCHAR(2) = 'mn'
,#START DATE = '6/1/2020'
,#END DATE = '7/1/2020'
,#GLOBAL VARCHAR(50) = 'indigent fee'
,#MANUAL VARCHAR(100) = '''misc charges'',''discount'''
DROP TABLE
IF EXISTS #customers
,#test
SELECT DISTINCT ch.amount
,ch.vehicle_program_id
,c.customer_id
,ch.customer_charge_id
,ch.charge_type
INTO #customers
FROM customer c
JOIN customer_charge ch(NOLOCK) ON c.customer_id = ch.customer_id
JOIN service_history sh(NOLOCK) ON sh.customer_id = c.customer_id
JOIN header h(NOLOCK) ON h.service_history_id = sh.service_history_id
WHERE ch.entry_date BETWEEN #START
AND #END
AND ch.price_trigger_id IN (
16
,15
)
AND ch.source_type = 1
AND sh.service_type = 5
AND h.is_duplicate = 0;
WITH CTE_global
AS (
SELECT DISTINCT ch.charge_type
,'global' AS type
FROM customer_charge ch
JOIN store s ON ch.store_id = s.store_id
JOIN address a ON a.id = s.address_id
JOIN locality l ON a.locality_id = l.id
WHERE l.region = #state
AND ch.price_trigger_id = 16
UNION ALL
SELECT 'None'
,'global'
)
,CTE_manual
AS (
SELECT DISTINCT ch.charge_type
,'manual' AS type
FROM customer_charge ch
JOIN store s ON ch.store_id = s.store_id
JOIN address a ON a.id = s.address_id
JOIN locality l ON a.locality_id = l.id
WHERE l.region = #state
AND ch.price_trigger_id = 15
UNION ALL
SELECT 'None'
,'manual'
)
SELECT DISTINCT c.last_name
,c.first_name
,vp.account_no
,cust.charge_type
,cust.amount
,sh.service_date
,s.store_name_short
,GLOBAL = g.charge_type
,manual = m.charge_type
INTO #test
FROM vehicle_program vp(NOLOCK)
JOIN vehicle v(NOLOCK) ON v.vehicle_id = vp.vehicle_id
JOIN service_history sh(NOLOCK) ON sh.vehicle_program_id = vp.program_id
AND service_type = 5
JOIN customer c(NOLOCK) ON v.customer_id = c.customer_id
AND c.customer_id = sh.customer_id
JOIN store s(NOLOCK) ON vp.current_store_id = s.store_id
JOIN #customers cust ON cust.customer_id = c.customer_id
AND cust.vehicle_program_id = sh.vehicle_program_id
JOIN customer_condition cc(NOLOCK) ON c.customer_id = cc.customer_id
JOIN customer_charge ch(NOLOCK) ON ch.customer_id = c.customer_id
JOIN service_charge sc ON sc.service_history_id = sh.service_history_id
AND sc.customer_charge_id = cust.customer_charge_id
JOIN header h(NOLOCK) ON h.service_history_id = sh.service_history_id
JOIN CTE_global g ON g.charge_type = ch.charge_type
JOIN CTE_manual m ON m.charge_type = ch.charge_type
WHERE cc.state_of_conviction = #state
AND sh.service_date BETWEEN #START
AND #END
AND h.is_duplicate = 0
SELECT *
FROM #test
WHERE GLOBAL IN (
CASE
WHEN #global IN ('None')
THEN charge_type
WHEN #global NOT IN ('None')
THEN #global
END
)
OR manual IN (
CASE
WHEN #manual IN ('None')
THEN charge_type
WHEN #manual NOT IN ('None')
THEN #manual
END
)
For clarity, the last bit in the query there is some logic to allow for these two params to be optional: so by selecting 'None' that param is rendered useless basically. It seems clear that the issue is with this last bit, specifically my WHERE clause using the CASE expression. When I remove that, I don't get the error, but I of course lose my logic. What's most confusing is that the error indicates an issue with a comma, but there's no comma in that part of the SQL?? Any help is going to be greatly appreciated.
Assuming users will only select 'None' from the list on it's own and never with another value then the following should work.
WHERE (GLOBAL IN (#Global) OR #Global = 'None')
AND
(manual IN (#manual) OR #manual = 'None')
this question was closed because someone thought it was the same issue
It is a dupe, but you kind of have to read between the lines in the other answers to apply it to this scenario. The point is that SSRS replaces multi-select parameters with delimited strings in the query body itself, and this transformation can lead either to unexpectedly getting no results, or in an illegal SQL query, depending on where the parameter marker appears in the original query.
I'll make it a bit clearer exactly what's going on. You can repro this behavior with this as your Data Set query:
drop table if exists #foo
create table #foo(charge_type varchar(200) , global varchar(200))
select *
from #foo
WHERE GLOBAL IN (
CASE
WHEN #global IN ('None')
THEN charge_type
WHEN #global NOT IN ('None')
THEN #global
END
)
And configure #global as a parameter that allows multi-select. When the user selects multiple values SSRS transforms the query into:
drop table if exists #foo
create table #foo(charge_type varchar(200) , global varchar(200))
select *
from #foo
WHERE GLOBAL IN (
CASE
WHEN N'a',N'b' IN ('None')
THEN charge_type
WHEN N'a',N'b' NOT IN ('None')
THEN N'a',N'b'
END
)
Which fails with An expression of non-boolean type specified in a context where a condition is expected, near ','.

MS SQL, outer column in sub-query

giving this table definitions:
CREATE TABLE Attivita_T041(Kint_TP013_IdAttivita INT)
CREATE TABLE TP004_LinkPersonaArea
(int_T013_IdArea int,
dte_T013_DataInizio date,
dte_T013_DataFine date,
int_T013_IdPersona int,
int_T013_IdRuoloArea int)
go
CREATE TABLE [dbo].[Q_TP013_Pianificazione] (
[Kint_TP013_IdAttivita] INT ,
[Ksin_TP013_IdArea] SMALLINT,
[Ksin_TP013_AnnoRifPian] SMALLINT,
[sin_TP013_Giorni] SMALLINT
);
i need a table function with a query that join Attivita_T041 table with many others and now i have to add a subquery to get two information: total of [sin_TP013_Giorni] and total of [sin_TP013_Giorni] for [Ksin_TP013_IdArea] where current user actualy work in (reading from TP004_LinkPersonaArea)
that is this part of the query:
SELECT ....
FROM Attivita_T041
INNER JOIN ---- many other tables here
LEFT OUTER JOIN PersonaArea_T013
ON int_T013_IdPersona = #idUtente
AND dte_T013_DataInizio <= #dataRiferimento
AND (dte_T013_DataFine > #dataRiferimento OR dte_T013_DataFine IS NULL)
LEFT OUTER JOIN (
SELECT
Kint_TP013_IdAttivita
, SUM(sin_TP013_Giorni) AS GiorniPianificatiAnno
, SUM(CASE
WHEN Ksin_TP013_IdArea = int_T013_IdArea THEN sin_TP013_Giorni
ELSE 0
END) AS GiorniPianificatiAnnoArea
FROM
Q_TP013_Pianificazione
INNER JOIN dbo.FN_DettaglioPianificazione(YEAR(#dataRiferimento), 0)
ON Progressivo = Ktyi_TP013_ProgressivoPian
WHERE
Ksin_TP013_AnnoRifPian = YEAR(#dataRiferimento)
GROUP BY Kint_TP013_IdAttivita
) AS Pianificazione
ON Kint_TP013_IdAttivita = Kint_T041_IdAttivita
but i can't do that because int_T013_IdArea column i nedd is outer of subquery
searching on SO i find the suggestion to use OUTER APPLY so i switch to this:
OUTER APPLY (
SELECT TOP 1
SUM(sin_TP013_Giorni) AS GiorniPianificatiAnno
, SUM(CASE
WHEN Ksin_TP013_IdArea = int_T013_IdArea THEN sin_TP013_Giorni
ELSE 0
END) AS GiorniPianificatiAnnoArea
FROM
Q_TP013_Pianificazione
INNER JOIN dbo.FN_DettaglioPianificazione(YEAR(#dataRiferimento), 0)
ON Progressivo = Ktyi_TP013_ProgressivoPian
WHERE
Ksin_TP013_AnnoRifPian = YEAR(#dataRiferimento)
AND Kint_TP013_IdAttivita = Kint_T041_IdAttivita
) AS Pianificazione
but now i get this error:
Multiple columns are specified in an aggregated expression containing
an outer reference. If an expression being aggregated contains an
outer reference, then that outer reference must be the only column
referenced in the expression.
is there a way to do that?

Sub query in Store Procedure

I have a query in sql stored procedure. I want to get record from other query from its id how I do that.
SELECT t.Name ,t.CreatedDate ,t.CreatedBy , t.Amount
,t.Margin ,t.Probability ,t.Id
FROM (SELECT a = 1) a
CROSS JOIN
(SELECT
Name = HirschInternational_MSCRM.dbo.SalesOrderBase.Name
,CreatedDate=HirschInternational_MSCRM.dbo.SalesOrderBase.CreatedOn
,CreatedBy=HirschInternational_MSCRM.dbo.SystemUserBase.FullName
,Amount = totalamount
,Probability=CloseProbability
,Id=SalesOrderId
,Margin=(SELECT ( ISNULL( ((Sum(Price)-Sum(CurrentCost)) / NULLIF( Sum(Price), 0 ))*100, 0 ) )
FROM HirschInternational_MSCRM.dbo.ProductBase
JOIN HirschInternational_MSCRM.dbo.SalesOrderDetailBase
ON HirschInternational_MSCRM.dbo.SalesOrderDetailBase.ProductId = HirschInternational_MSCRM.dbo.ProductBase.ProductId
JOIN HirschInternational_MSCRM.dbo.SalesOrderBase
ON HirschInternational_MSCRM.dbo.SalesOrderBase.SalesOrderId = HirschInternational_MSCRM.dbo.SalesOrderDetailBase.SalesOrderId)
FROM HirschInternational_MSCRM.dbo.SalesOrderBase
JOIN HirschInternational_MSCRM.dbo.OpportunityBase
ON HirschInternational_MSCRM.dbo.SalesOrderBase.Opportunityid = HirschInternational_MSCRM.dbo.OpportunityBase.Opportunityid
JOIN HirschInternational_MSCRM.dbo.SystemUserBase
ON HirschInternational_MSCRM.dbo.SystemUserBase.SystemUserId = HirschInternational_MSCRM.dbo.SalesOrderBase.CreatedBy
WHERE YEAR(HirschInternational_MSCRM.dbo.SalesOrderBase.CreatedOn)=YEAR(GETDATE())
I want Margin from every record I want Output like
It's not entirely clear what you want, but you might be looking for something like
select *
from (your SQL SELECT statement goes here) t1
where id = ?;
I want to get margin of every record how I filter margin query for SalesOrderId
like
Margin=(SELECT ( ISNULL( ((Sum(Price)-Sum(CurrentCost)) / NULLIF( Sum(Price), 0 ))*100, 0 ) )
FROM HirschInternational_MSCRM.dbo.ProductBase
JOIN HirschInternational_MSCRM.dbo.SalesOrderDetailBase
ON HirschInternational_MSCRM.dbo.SalesOrderDetailBase.ProductId = HirschInternational_MSCRM.dbo.ProductBase.ProductId
JOIN HirschInternational_MSCRM.dbo.SalesOrderBase
ON HirschInternational_MSCRM.dbo.SalesOrderBase.SalesOrderId = HirschInternational_MSCRM.dbo.SalesOrderDetailBase.SalesOrderId
Where HirschInternational_MSCRM.dbo.SalesOrderBase.SalesOrderId= //SalesOrderId that I get in main query)
how I pass that SalesOrderId in this query

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