I have the below query that takes a TagId list from table variable and returns the list.
But I need to add that CategoryId WHERE condition only if #Tags has the records.
Is it possible to add a WHERE Condition only if my table variable has records otherwise run the same query with 1=1(Always true) and skip the category filter?
DECLARE #TagIdList NVARCHAR(100) = '22,25,47'
DECLARE #Tags TABLE (TagId INT);
WITH CSVtoTable
AS (
SELECT CAST('<XMLRoot><RowData>' + REPLACE(t.val, ',', '</RowData><RowData>') + '</RowData></XMLRoot>' AS XML) AS x
FROM (
SELECT #TagIdList
) AS t(val)
)
INSERT INTO #Tags (TagId)
SELECT m.n.value('.[1]', 'varchar(8000)') AS TagId
FROM CSVtoTable
CROSS APPLY x.nodes('/XMLRoot/RowData') m(n)
SELECT BookingId
,C.CategoryName
FROM Booking B
INNER JOIN Category C ON C.CategoryId = B.CategoryId
WHERE (
b.IsDeleted = 0
OR b.IsDeleted IS NULL
)
-- Add the below where condition only if #Tags has records, else use 1=1
AND C.CategoryId IN (
SELECT DISTINCT CategoryId
FROM CategoryXTag con
WHERE TagId IN (
SELECT TagId
FROM #Tags
)
)
Ultimately you only need to change the end of your query. If performance is an issue you might want to consider using two branches of an if block for each of the two cases even though it's technically possible to squeeze the logic into a single query that doesn't generally optimize as well.
AND
(
C.CategoryId IN (
SELECT CategoryId
FROM CategotryXTag
WHERE TagId IN (
SELECT TagId
FROM #Tags
)
)
OR
(SELECT COUNT(*) FROM #Tags) = 0
)
declare int #tagcount = (select count(*) from #Tags);
SELECT BookingId, C.CategoryName
FROM Booking B
INNER JOIN Category C
ON C.CategoryId = B.CategoryId
AND isnull(b.IsDeleted, 0) = 0
INNER JOIN CategoryXTag con
ON C.CategoryId = con.CategoryId
INNER JOIN #Tags tags
ON tags.TagID = con.TagID
OR #tagcount = 0;
if #tags is empty you might need to put one record in it with a value that would never by used and then or that value
if(#tagcount = 0) insert into #tags values (-100);
or tags.TagID = -100;
You don't need to modify your where clause. Instead, you achieve the same logic by filling #Tags with every TagId from CategoryXTag before running your final query if #Tags is empty after the initial insert:
if ((select count(*) from #Tags) = 0)
insert into #Tags
select distinct TagId
from CategoryXTag;
I'd declare a variable for the #Tags table:
declare #needTagsFilter bit
set #needTagsFilter = case when exists(select 1 from #Tags) then 1 else 0 end
and change the where clause like
AND (
(#needTagsFilter = 0) OR
(C.CategoryId IN (
SELECT DISTINCT CategoryId
FROM CategoryXTag con
WHERE TagId IN (
SELECT TagId
FROM #Tags
)
)
)
COUNT(*) is slower then exists. The downside of adding the count/exists directly to your original query is that SQL server might execute it for all rows.
Related
Using following query i'm going to filter results based on selected tags or categories:
DECLARE #categories NVARCHAR(MAX),
#tags NVARCHAR(MAX);
SELECT #categories = '1,2,4', -- comma separated category ids
#tags = '2,3' -- comma separated tag ids
SELECT p.id,
p.title,
p.price
FROM tbl_products p
LEFT JOIN tbl_product_categories pc ON #categories IS NOT NULL AND pc.product_FK = p.id
LEFT JOIN tbl_product_tags pt ON #tags IS NOT NULL AND pt.product_FK = p.id
WHERE ( p.price >= #min_price OR #min_price IS NULL )
AND ( p.price <= #max_price OR #max_price IS NULL )
AND ( pc.category_FK IN (SELECT value FROM STRING_SPLIT(#categories, ',')) OR #categories IS NULL )
AND ( pt.tag_FK IN (SELECT value FROM STRING_SPLIT(#tags, ',')) OR #tags IS NULL)
GROUP BY p.id
HAVING COUNT(p.id) = ( (SELECT COUNT(*) FROM STRING_SPLIT(#categories, ',')) + (SELECT COUNT(*) FROM STRING_SPLIT(#tags, ',')) )
But it does not produce expected results! I am suspicious that HAVING part does not employed correctly as it does not produce right count every time based on passed tags and category ids.
Does anyone know how we could implement such situations, apply relational division to extract products which have all these passed #categories and #tags in common??? Any better way?
-- update:
for example use the following sample date:
tbl_products:
id title price
===================
1 mouse 10
2 keyboard 18
3 iphone 8 100
4 note 8 90
tbl_product_categories:
product_FK category_FK
======================
1 1
2 1
3 2
4 2
tbl_product_tags:
product_FK tag_FK
=================
1 1
3 1
3 2
4 2
so if we pass #categories = '2' and #tags = '1,2' and min_price = 50 then we should get an iphone 8
Instead of trying to add counts from your variables, you can use count(distinct [tags|categories]) equals the count for the respective parameter like so:
declare #categories nvarchar(max), #tags nvarchar(max), #min_price int, #max_price int;
select
#categories = '2' -- comma separated category ids
, #tags = '1,2' -- comma separated tag ids
, #min_price = 0
, #max_price = power(2,30)
select
p.id
, p.title
, p.price
from tbl_products p
left join tbl_product_categories pc
on #categories is not null and pc.product_fk = p.id
left join tbl_product_tags pt
on #tags is not null and pt.product_fk = p.id
where ( p.price >= #min_price or #min_price is null )
and ( p.price <= #max_price or #max_price is null )
and ( pc.category_fk in (select value from string_split(#categories, ',')) or #categories is null )
and ( pt.tag_fk in (select value from string_split(#tags, ',')) or #tags is null)
group by p.id, p.title, p.price
having (count(distinct pc.category_fk) = (select count(*) from string_split(#categories, ',')) or #categories is null)
and (count(distinct pt.tag_fk) = (select count(*) from string_split(#tags, ',')) or #tags is null)
demo: dbfiddle.uk demo
returns:
+----+----------+-------+
| id | title | price |
+----+----------+-------+
| 3 | iphone 8 | 100 |
+----+----------+-------+
When it comes to performance, you will benefit from rewriting this as a procedure with dynamic sql execution, or at least option (recompile) as shown in these references:
An Updated "Kitchen Sink" Example - Aaron Bertand
Parameter Sniffing, Embedding, and the RECOMPILE Options - Paul White
Dynamic Search Conditions - Erland Sommarskog
Catch-all queries - Gail Shaw
Here is an example of a dynamic sql search procedure for your query that uses exists ...having count()... instead of left join... where... having count(distinct ...) that simplifies the plan a bit (plan comparison demo):
create procedure product_search (
#categories nvarchar(max)
, #tags nvarchar(max)
, #min_price int
, #max_price int
) as
begin;
set nocount, xact_abort on;
declare #sql nvarchar(max);
declare #params nvarchar(256);
set #params = '#categories nvarchar(max), #tags nvarchar(max), #min_price int, #max_price int';
set #sql = ';
select
p.id
, p.title
, p.price
from tbl_products p
where 1=1'
if #min_price is not null
set #sql = #sql + '
and p.price >= #min_price';
if #max_price is not null
set #sql = #sql + '
and p.price <= #max_price';
if #categories is not null
set #sql = #sql + '
and exists (
select 1
from tbl_product_categories ic
where ic.product_fk = p.id
and ic.category_fk in (select value from string_split(#categories, '',''))
having count(*) = (select count(*) from string_split(#categories, '',''))
)';
if #tags is not null
set #sql = #sql + '
and exists (
select 1
from tbl_product_tags it
where it.product_fk = p.id
and it.tag_fk in (select value from string_split(#tags, '',''))
having count(*) = (select count(*) from string_split(#tags, '',''))
)';
exec sp_executesql #sql, #params, #categories, #tags, #min_price, #max_price;
end;
executed like so:
declare #categories nvarchar(max), #tags nvarchar(max), #min_price int, #max_price int;
select
#categories = null -- comma separated category ids
, #tags = '1,2' -- comma separated tag ids
, #min_price = null
, #max_price = power(2,30)
exec product_search #categories, #tags, #min_price, #max_price
demo: dbfiddle.uk demo
returns:
+----+----------+-------+
| id | title | price |
+----+----------+-------+
| 3 | iphone 8 | 100 |
+----+----------+-------+
From your sample data I think you're joining on the wrong column tag_FK instead of product_FK, so the LEFT JOIN on the table tbl_product_tags should be:
LEFT JOIN tbl_product_tags pt ON #tags IS NOT NULL AND pt.product_FK = p.id
Also, I don't think there is a need to use HAVING statement in your query, it seems to me that you're using it as an extra check; since your query is already doing the filtering job. However, the condition after HAVING statement is not correct, and the best example to prove that is your example itself:
1. count of p.Id = 1 (p.Id = 3 ... iPhone 8)
2. count of categories = 1 (category: 2)
3. count of tags = 2 (tags: 1, 2)
then in this case the count of p.Id is not equal to the count of the passed categories and tags.
UPDATE
: based on #dtNiro the query should be as follows:
DECLARE #categories NVARCHAR(MAX),
#tags NVARCHAR(MAX);
SELECT #categories = '1,2,4', -- comma separated category ids
#tags = '2,3' -- comma separated tag ids
SELECT p.id,
p.title,
p.price
FROM tbl_products p
LEFT JOIN tbl_product_categories pc ON #categories IS NOT NULL AND pc.product_FK = p.id
LEFT JOIN tbl_product_tags pt ON #tags IS NOT NULL AND pt.product_FK = p.id
WHERE ( p.price >= #min_price OR #min_price IS NULL )
AND ( p.price <= #max_price OR #max_price IS NULL )
AND ( pc.category_FK IN (SELECT value FROM STRING_SPLIT(#categories, ',')) OR #categories IS NULL )
AND ( pt.tag_FK IN (SELECT value FROM STRING_SPLIT(#tags, ',')) OR #tags IS NULL)
GROUP BY p.id
HAVING (#tags IS NULL OR (COUNT(p.id) = (SELECT COUNT(*) FROM STRING_SPLIT(#tags, ','))))
I have this SQL query:
IF NOT EXISTS (SELECT TOP 1 RowId
FROM dbo.Cache AS C
WHERE StringSearched = #pcpnpi
AND colName = 'pcpnpi'
AND ModifiedAt > (SELECT ModifiedAt
FROM dbo.Patients AS p
WHERE P.RowID = C.RowID))
BEGIN
SELECT #constVal = FunctionWeight
FROM dbo.FunctionWeights
WHERE FunctionWeights.FunctionId = 33;
INSERT INTO #Temp2
(RowNumber,ValFromUser,ColumnName,ValFromFunc,
FuncWeight,percentage)
SELECT RowNumber,#pcpnpi,'pcpnpi',PercentMatch,
#constVal,PercentMatch * #constVal
FROM dbo.Matchpcpnpi (#pcpnpi);
END
ELSE
BEGIN
INSERT INTO #Temp2
(RowNumber,ValFromUser,ColumnName,Percentage)
SELECT RowId,StringSearched,ColName,PercentMatch
FROM dbo.Cache AS C
WHERE StringSearched = #pcpnpi
AND colName = 'pcpnpi'
AND ModifiedAt > (SELECT ModifiedAt
FROM dbo.Patients AS p
WHERE P.RowID = C.RowID)
END
The above if statement is meant to avoid unnecessary look ups for strings that have already been searched earlier and MatchPercent has been calculated. In that case, it is directly retrieved from Cache table.
Above sql query is basically for one particular column and this same kind of query with just columnName and its value changing is repeated for many other columns in the procedure.
The if Exists check was obviously meant so that query performance could improve however the performance has gone down, probably because of extra checks.
Cache table which is actually meant to improve the performance, extra checks have ruined it.
Is there a way to simplify above query,please ? Any directions on same will help.
Thanks
First insert into #temp2 based on exists condition. If the Insert record count is zero then do the another insert. Try this.
INSERT INTO #Temp2
(RowNumber,ValFromUser,ColumnName,Percentage)
SELECT RowId,StringSearched,ColName,PercentMatch
FROM dbo.Cache AS C
WHERE StringSearched = #pcpnpi
AND colName = 'pcpnpi'
AND ModifiedAt > (SELECT ModifiedAt
FROM dbo.Patients AS p
WHERE P.RowID = C.RowID)
IF ##ROWCOUNT = 0
BEGIN
SELECT #constVal = FunctionWeight
FROM dbo.FunctionWeights
WHERE FunctionWeights.FunctionId = 33;
INSERT INTO #Temp2
(RowNumber,ValFromUser,ColumnName,ValFromFunc,
FuncWeight,percentage)
SELECT RowNumber,#pcpnpi,'pcpnpi',PercentMatch,
#constVal,PercentMatch * #constVal
FROM dbo.Matchpcpnpi (#pcpnpi)
END
First, consider this query in the exists:
select Top 1 RowId
from dbo.Cache as C
where StringSearched = #pcpnpi and
colName = 'pcpnpi' and
ModifiedAt > ( Select ModifiedAt FROM dbo.Patients p WHERE P.RowID = C.RowID))
For performance, you want indexes on cache(StringSearched, colName, ModifiedAt, RowId) and Patients(RowId).
However, you are running this query twice. I would suggest a structure more like:
declare #RowId . . . ; -- I don't know the type
select Top 1 #RowId = RowId
from dbo.Cache as C
where StringSearched = #pcpnpi and
colName = 'pcpnpi' and
ModifiedAt > ( Select ModifiedAt FROM dbo.Patients p WHERE P.RowID = C.RowID));
if (#RowId) is null . ..
else . . .
I have a weird situation and not too sure how to approach it.
I have 2 separate tables:
Table A is submissions
id
submitterQID
nomineeQID
story
Table B is employees
QID
Name
Department
I am trying to get the total number of submissions grouped by department as well as the total number of nominations.
This is what my Stored procedure looks like:
BEGIN
SELECT TOP 50 count(A.[nomineeQID]) AS totalNominations,
count(A.[subQID]) AS totalSubmissions,
B.[DepartmentDesc] AS department
FROM empowermentSubmissions AS A
JOIN empTable AS B
ON B.[qid] = A.[nomineeQID]
WHERE A.[statusID] = 3
AND A.[locationID] = #locale
GROUP BY B.[Department]
ORDER BY totalNominations DESC
FOR XML PATH ('data'), TYPE, ELEMENTS, ROOT ('root');
END
This issue with this is that the JOIN is joining by the nomineeQID only and not the subQID as well.
My end result I am looking for is:
Department Customer Service has 25 submissions and 90 nominations
ORDERED BY the SUM of both counts...
I tried to just JOIN again on the subQID but was told I cant join on the same table twice.
Is there an easier way to accomplish this?
This is a situaton where you'll need to gather your counts independently of each other. Using two left joins will cause some rows to be counted twice in the first left join when the join condition is met for both. Your scenario can be solved using either correlated subqueries or an outer apply gathering the counts on different criteria. I did not present a COUNT(CASE ... ) option here, because you don't have an either-or scenario in the data, you have two foreign keys to the employees table. So, setting up sample data:
declare #empowermentSubmissions table (submissionID int primary key identity(1,1), submissionDate datetime, nomineeQID INT, submitterQID INT, statusID INT, locationID INT)
declare #empTable table (QID int primary key identity(1,1), AreaDesc varchar(10), DepartmentDesc varchar(20))
declare #locale INT = 0
declare #n int = 1
while #n < 50
begin
insert into #empTable (AreaDesc, DepartmentDesc) values ('Area ' + cast((#n % 2)+1 as varchar(1)), 'Department ' + cast((#n % 4)+1 as varchar(1)))
set #n = #n + 1
end
set #n = 1
while #n < 500
begin
insert into #empowermentSubmissions (submissionDate, nomineeQID, submitterQID, StatusID, locationID) values (dateadd(dd,-(cast(rand()*600 as int)),getdate()), (select top 1 QID from #empTable order by newid()), (select top 1 QID from #empTable order by newid()), 3 + (#n % 2) - (#n % 3), (#n % 2) )
set #n = #n + 1
end
And now the OUTER APPLY option:
SELECT TOP 50 E.DepartmentDesc, SUM(N.Nominations) Nominations, SUM(S.TotalSubmissions) TotalSubmissions
FROM #empTable E
OUTER APPLY (
SELECT COUNT(submissionID) Nominations
FROM #empowermentSubmissions A
WHERE A.statusID = 3
AND A.nomineeQID = E.QID
AND A.locationID = #locale
) N
OUTER APPLY (
SELECT COUNT(submissionID) TotalSubmissions
FROM #empowermentSubmissions A
WHERE A.statusID = 3
AND A.submitterQID = E.QID
AND A.locationID = #locale
) S
GROUP BY E.DepartmentDesc
ORDER BY SUM(Nominations) + SUM(TotalSubmissions) DESC
I have a situation when I use IF ELSE statement in SQL.
#searchString nvarchar(50),
#languageId int,
#count int,
#id int
IF(#count IS NOT NULL)
SELECT TOP (#count) Id, value
FROM TABLE1
WHERE (Id IN
(SELECT Id
FROM TABLE2
WHERE (Id = #id))) and languageId = #languageId AND value LIKE '%' + #searchString + '%'
ORDER BY value
ELSE
SELECT Id, value
FROM TABLE1
WHERE (Id IN
(SELECT Id
FROM TABLE2
WHERE (Id = #id))) and languageId = #languageId AND value LIKE '%' + #searchString + '%'
ORDER BY value
I would like to return number of all rows using
count(*) over() (or something similar)
(as I return only TOP count records for now), like it is answered here:
How to return total number of records with TOP * select
BUT: I wouldn't return this value for every instance, but I would like to return count just once.
Is there a way to do this with one query, or I have to write a separate query for this?
Any hint would be greatly appreciated!
EDIT: using SQL server 2008 r2.
I suggest you to use INNER JOIN, for Exemple with scripts:
{
SCRIPT
SELECT
l.localite_id,
p.patient_id,
p.patient_date_naissance,
p.patient_gsm, p.patient_tel
FROM
patient p
INNER JOIN localite l
ON l.localite_id = p.localite_id
INNER JOIN ville v
ON v.ville_id = l.ville_id
INNER JOIN gouvernorat g
ON v.gouv_id = g.gouv_id
INNER JOIN pays pays
ON pays.pays_id = g.pays_id
IF NEQ #patient_gsm# -1
WHERE p.patient_gsm like #patient_gsm#
AND p.patient_code like #patient_code#
ENDIF
END
}
In SQL Server 2005, I have an order details table with an order id and a product id. I want to write a sql statement that finds all orders that have all the items within a particular order. So, if order 5 has items 1, 2, and 3, I would want all other orders that also have 1, 2, and 3. Also, if order 5 had 2 twice and 3 once, I'd want all other orders with two 2s and a 3.
My preference is that it return orders that match exactly, but orders that are a superset are acceptable if that's much easier / performs much better.
I tried a self-join like the following, but that found orders with any of the items rather than all of the items.
SELECT * FROM Order O1
JOIN Order O2 ON (O1.ProductId = O2.ProductId)
WHERE O2.OrderId = 5
This also gave me duplicates if order 5 contained the same item twice.
If the OrderDetails table contains a unique constraint on OrderId and ProductId, then you can do something like this:
Select ...
From Orders As O
Where Exists (
Select 1
From OrderDetails As OD1
Where OD1.ProductId In(1,2,3)
And OD1.OrderId = O.Id
Group By OD1.OrderId
Having Count(*) = 3
)
If it is possible to have the same ProductId on the same Order multiple times, then you could change the Having clause to Count(Distinct ProductId) = 3
Now, given the above, if you want the situation where each order has the same signature with duplicate product entries, that is trickier. To do that you would need the signature of order in question over the products in question and then query for that signature:
With OrderSignatures As
(
Select O1.Id
, (
Select '|' + Cast(OD1.ProductId As varchar(10))
From OrderDetails As OD1
Where OD1.OrderId = O1.Id
Order By OD1.ProductId
For Xml Path('')
) As Signature
From Orders As O1
)
Select ...
From OrderSignatures As O
Join OrderSignatures As O2
On O2.Signature = O.Signature
And O2.Id <> O.Id
Where O.Id = 5
This sort of thing is very difficult to do in SQL, as SQL is designed to generate its result set by, at the most basic level, comparing a set of column values on a single row each to another value. What you're trying to do is compare a single column value (or set of column values) on multiple rows to another set of multiple rows.
In order to do this, you'll have to create some kind of order signature. Strictly speaking, this isn't possible to do using query syntax alone; you'll have to use some T-SQL.
declare #Orders table
(
idx int identity(1, 1),
OrderID int,
Signature varchar(MAX)
)
declare #Items table
(
idx int identity(1, 1),
ItemID int,
Quantity int
)
insert into #Orders (OrderID) select OrderID from [Order]
declare #i int
declare #cnt int
declare #j int
declare #cnt2 int
select #i = 0, #cnt = max(idx) from #Orders
while #i < #cnt
begin
select #i = #i + 1
declare #temp varchar(MAX)
delete #Items
insert into #Items (ItemID, Quantity)
select
ItemID,
Count(ItemID)
from OrderItem oi
join #Orders o on o.idx = #i and o.OrderID = oi.OrderID
group by oi.ItemID
order by oi.ItemID
select #j = min(idx) - 1, #cnt2 = max(idx) from #Items
while #j < #cnt2
begin
select #j = #j + 1
select #temp = isnull(#temp + ', ','') +
'(' +
convert(varchar,i.ItemID) +
',' +
convert(varchar, i.Quantity) +
')'
from #Items i where idx = #j
end
update #Orders set Signature = #temp where idx = #i
select #temp = null
end
select
o_other.OrderID
from #Orders o
join #Orders o_other on
o_other.Signature = o.Signature
and o_other.OrderID <> o.OrderID
where o.OrderID = #OrderID
This assumes (based on the wording of your question) that ordering multiple of the same item in an order will result in multiple rows, rather than using a Quantity column. If the latter is the case, just remove the group by from the #Items population query and replace Count(ItemID) with Quantity.
I think this should work. I'm using 108 as an example OrderID, so you'll have to replace that twice below or use a variable.
WITH TempProducts(ProductID) AS
(
SELECT DISTINCT ProductID FROM CompMarket
WHERE OrderID = 108
)
SELECT OrderID FROM CompMarket
WHERE ProductID IN (SELECT ProductID FROM TempProducts)
AND OrderID != 108
GROUP BY OrderID
HAVING COUNT(DISTINCT ProductID) >= (SELECT COUNT(ProductID) FROM TempProducts)
This uses a CTE to get a list of an Order's Products, then selects all order IDs that have products that are all in this list. To make sure that the Orders returned have all the products, this compares the Count of the CTE to the Counts of the returned Order's Products.