Select from table only if all dynamic parameter values match - sql-server

I have the following animal table:
id | action
------------------
duck | cuack
duck | fly
duck | swim
pelican | fly
pelican | swim
I want to create a stored procedure and pass a group of values into a single parameter:
EXEC GuessAnimalName 'cuack,fly,swim'
Result:
duck
So, if it cuacks, it flyes and it swims then it's a duck. But also:
EXEC GuessAnimalName 'fly,swim'
Result:
pelican
---
EXEC GuessAnimalName 'fly'
Result:
No results
Parameter's number is dynamic.
In order to guess the animal's name all actions provided must match or be found in animal table.
This is what I have so far:
DECLARE #animal AS TABLE
(
[id] nvarchar(8),
[action] nvarchar(16)
)
INSERT INTO #animal VALUES('duck','cuack')
INSERT INTO #animal VALUES('duck','fly')
INSERT INTO #animal VALUES('duck','swim')
INSERT INTO #animal VALUES('pelican','fly')
INSERT INTO #animal VALUES('pelican','swim')
-- Parameter simulation
DECLARE #params AS TABLE
(
[action] nvarchar(16)
)
INSERT INTO #params VALUES('cuack')
INSERT INTO #params VALUES('fly')
INSERT INTO #params VALUES('swim')
SELECT
a.[id]
FROM
#animal a
INNER JOIN
#params p
ON
a.[action] = p.[action]
GROUP BY
a.[id]
HAVING COUNT(a.[action]) IN (SELECT COUNT([action]) FROM #animal GROUP BY [id])
Which gives the result:
Result:
--------
duck
--------
pelican
It should return just duck.

convert this to a stored proc using RANK
declare #lookfor varchar(100) = 'swim,fly'
select id from
(select
id, rank() over (order by cnt desc) rank_ -- get rank based on the number of match where should be the same number of rows
from
(
SELECT
a.id, count(1) cnt -- identify how many matches
FROM
#animal a
INNER JOIN
#params p
ON
a.[action] = p.[action]
where charindex(p.action,#lookfor) > 0
group by a.id
having count(1) = (select count(1) from #animal x where a.id = x.id)) -- only get the animal with the same number of matches and rows
y)
z where rank_ = 1 -- display only with the most matches which should be the same number of rows matched
you don't need #params here
select id from
(select
id, rank() over (order by cnt desc) rank_
from
(
SELECT
a.id, count(1) cnt
FROM
#animal a
where charindex(a.action,#lookfor) > 0
group by a.id
having count(1) = (select count(1) from #animal x where a.id = x.id))
y)
z where rank_ = 1

Related

SQL LEFT JOIN SUM One To Many

I am trying to get the SUM from two different but related tables that have a one to many relationship, but when I add a where condition to the second table the first does not properly sum up the total. Can this be done in a single query? I should also note that it is critical that both consider the same set of LocationId's as they come from an outside filter. I also need the Activityname condition to happen after the join if at all possible. If that isn't possible then that is fine.
IF OBJECT_ID('tempdb..#tmpVisits') is not null
begin
drop TABLE #tmpVisits
end
IF OBJECT_ID('tempdb..#tmpVisitsByActivity') is not null
begin
drop TABLE #tmpVisitsByActivity
end
CREATE TABLE #tmpVisits
(
AccountId int,
LocationId int,
Dt DATE,
TotalVisits int
)
CREATE TABLE #tmpVisitsByActivity
(
AccountId int,
LocationId int,
EventDate DATE,
TotalCompleted INT,
ActivityName varchar(20)
)
insert INTO #tmpVisits
SELECT 1,10,'2018-09-12',12
union ALL
SELECT 1,11,'2018-09-12',20
union ALL
SELECT 1,22,'2018-09-12',10
insert INTO #tmpVisitsByActivity
SELECT 1,10,'2018-09-12',55,'ActivityA'
union ALL
SELECT 1,10,'2018-09-12',1,'ActivityA'
union ALL
SELECT 1,10,'2018-09-12',2,'ActivityB'
union ALL
SELECT 1,22,'2018-09-12',3,'ActivityC'
SELECT SUM(v.TotalVisits) --expecting 42 actual 10
, SUM(a.TotalCompleted) --expecting 3 actual 3
FROM #tmpVisits v
left JOIN #tmpVisitsByActivity a
ON v.AccountId = a.AccountId
AND v.dt = a.EventDate
AND v.LocationId = a.locationid
WHERE v.dt='2018-09-12' AND v.AccountId=1
AND a.ActivityName='ActivityC'
You can move the where condition clauses in the join condition like below, if you want to use a single query.
SELECT SUM(v.TotalVisits) --expecting 42 actual 10
, SUM(a.TotalCompleted) --expecting 3 actual 3
FROM #tmpVisits v
left JOIN #tmpVisitsByActivity a
ON v.AccountId = a.AccountId
AND v.dt = a.EventDate
AND v.LocationId = a.locationid
AND v.dt='2018-09-12' AND v.AccountId=1
AND a.ActivityName='ActivityC'
The last criteria makes it so that some in #tmpVisits are excluded when there's no match.
But that's easy to get around.
Move the criteria for a.ActivityName to the ON clause, and remove it from the WHERE clause.
...
LEFT JOIN #tmpVisitsByActivity a
ON a.AccountId = v.AccountId AND
a.EventDate = v.dt AND
a.LocationId = v.locationId AND
a.ActivityName = 'ActivityA'
WHERE v.dt = '2018-09-12'
AND v.AccountId = 1
But it would be better to put the second table in a sub-query. Otherwise the first SUM could be wrong.
Example snippet:
DECLARE #Visits TABLE
(
AccountId INT,
LocationId INT,
Dt DATE,
TotalVisits INT
);
DECLARE #VisitsByActivity TABLE
(
AccountId INT,
LocationId INT,
EventDate DATE,
TotalCompleted INT,
ActivityName VARCHAR(20)
);
INSERT INTO #Visits (AccountId, LocationId, Dt, TotalVisits) VALUES
(1,10,'2018-09-12',12),
(1,11,'2018-09-12',20),
(1,22,'2018-09-12',10);
INSERT INTO #VisitsByActivity (AccountId, LocationId, EventDate, TotalCompleted, ActivityName) VALUES
(1,10,'2018-09-12',55,'ActivityA'),
(1,10,'2018-09-12',1,'ActivityA'),
(1,10,'2018-09-12',2,'ActivityB'),
(1,22,'2018-09-12',1,'ActivityC'),
(1,22,'2018-09-12',2,'ActivityC');
SELECT
SUM(v.TotalVisits) AS TotalVisits,
SUM(ac.TotalCompleted) AS TotalCompleted
FROM #Visits v
LEFT JOIN
(
SELECT AccountId, EventDate, locationid,
SUM(TotalCompleted) AS TotalCompleted
FROM #VisitsByActivity
WHERE ActivityName = 'ActivityC'
GROUP BY AccountId, EventDate, locationid
) AS ac
ON (ac.AccountId = v.AccountId AND ac.EventDate = v.dt AND ac.LocationId = v.locationid)
WHERE v.dt = '2018-09-12'
AND v.AccountId=1

SQL filter rows based on multiple condition and get the matching records

Hello all I have a requirement where I need to filter the rows with multiple conditions and exclude the result if a single entry exists in matching. Here are my sample tables
DECLARE #CUSTOMER TABLE
(
CUSTOMERID INT,
CUSTOMERNAME NVARCHAR(100)
)
DECLARE #ORDER TABLE
(
ORDERID INT,
CUSTOMERID INT,
ISSPECIALORDER INT,
SPECIALORDERID INT
)
DECLARE #SPECIALORDERDTL TABLE
(
SPECIALORDERID INT,
SPECIALORDERDATAID INT
)
DECLARE #SPECIALORDERDATA TABLE
(
SPECIALORDERDATAID INT,
SPECIALORDERMASTERID INT
)
INSERT INTO #CUSTOMER VALUES (100,'CUSTOMER1'),(200,'CUSTOMER2'),(300,'CUSTOMER3'),(400,'CUSTOMER4`enter code here`')
INSERT INTO #ORDER VALUES (1,100,0,1),(2,100,1,1),(3,100,1,2),(4,200,0,1),(5,200,1,1),(6,200,1,4),(7,300,1,5),(8,400,1,6)
INSERT INTO #SPECIALORDERDTL VALUES(1,1),(2,1),(3,2),(4,4)
INSERT INTO #SPECIALORDERDATA VALUES(1,1),(2,1),(3,1),(4,2),(5,2) -- 2 a special order
SELECT C.CUSTOMERID,C.CUSTOMERNAME
FROM #ORDER O
INNER JOIN #CUSTOMER C ON C.CUSTOMERID=O.CUSTOMERID
INNER JOIN #SPECIALORDERDTL SO ON SO.SPECIALORDERID = O.SPECIALORDERID
INNER JOIN #SPECIALORDERDATA SOD ON SO.SPECIALORDERDATAID = SOD.SPECIALORDERDATAID
WHERE SOD.SPECIALORDERID <> 2 AND O.ISSPECIALORDER =0
GROUP BY C.CUSTOMERID,C.CUSTOMERNAME
ORDER BY C.CUSTOMERNAME
When I have an entry in #SPECIALORDERDTL with SPECIALORDERMASTERID as 2 I need to consider them as special entries and exclude those. So my query should return only the customer with 100.
It is not clear from your description or SQL what exactly want. From my understanding:
DECLARE #CUSTOMER TABLE
(
CUSTOMERID INT,
CUSTOMERNAME NVARCHAR(100)
)
DECLARE #ORDER TABLE
(
ORDERID INT,
CUSTOMERID INT,
ISSPECIALORDER INT,
SPECIALORDERID INT
)
DECLARE #SPECIALORDERDTL TABLE
(
SPECIALORDERID INT,
SPECIALORDERDATAID INT
)
DECLARE #SPECIALORDERDATA TABLE
(
SPECIALORDERDATAID INT,
SPECIALORDERMASTERID INT
)
INSERT INTO #CUSTOMER VALUES
(100,'CUSTOMER1'),
(200,'CUSTOMER2'),
(300,'CUSTOMER3'),
(400,'CUSTOMER4')
INSERT INTO #ORDER VALUES
(1,100,0,1),
(2,100,1,1),
(3,100,1,2),
(4,200,0,1),
(5,200,1,1),
(6,200,1,4),
(7,300,1,5),
(8,400,1,6)
INSERT INTO #SPECIALORDERDTL VALUES(1,1),(2,1),(3,2),(4,4)
INSERT INTO #SPECIALORDERDATA VALUES(1,1),(2,1),(3,1),(4,2),(5,2) -- 2 a special order
SELECT C.CUSTOMERID,C.CUSTOMERNAME
from #Customer c
where exists (select * from #ORDER o where o.CustomerId = c.CustomerId)
and not exists (
select *
from #ORDER O
LEFT JOIN #SPECIALORDERDTL SO ON SO.SPECIALORDERID = O.SPECIALORDERID
LEFT JOIN #SPECIALORDERDATA SOD ON SO.SPECIALORDERDATAID = SOD.SPECIALORDERDATAID
WHERE (SO.SPECIALORDERID IS NULL
or SOD.SPECIALORDERMASTERID = 2 --AND O.ISSPECIALORDER =0
) AND O.CustomerId = c.CustomerId
);
GO
CUSTOMERID | CUSTOMERNAME
---------: | :-----------
100 | CUSTOMER1
db<>fiddle here
Assuming I understand the question, I think a conditional aggregation in the having clause is probably the simplest way to get the result you want:
SELECT C.CUSTOMERID, C.CUSTOMERNAME
FROM #CUSTOMER As C
JOIN #ORDER O
ON C.CUSTOMERID = O.CUSTOMERID
JOIN #SPECIALORDERDTL SO
ON O.SPECIALORDERID = SO.SPECIALORDERID
JOIN #SPECIALORDERDATA SOD
ON SO.SPECIALORDERDATAID = SOD.SPECIALORDERDATAID
GROUP BY C.CUSTOMERID, C.CUSTOMERNAME
HAVING COUNT(CASE WHEN SOD.SPECIALORDERMASTERID = 2 THEN 1 END) = 0
The having clause will filter out every customer where at least one of the orders associated with them have a specialordermasterid of 2.
From your description it sounds like not every customer will have an entry in SPECIALORDERDTL or SPECIALORDERDTA so you don't want to inner join to those tables.
What you need is a "not exists" correlated subquery to check that the customers do not have a matching row in those tables.
So you can remove the inner joins to SPECIAL* tables and add something like:-
where not exists (select null from SPECIALORDERDTL SO where
SO.SPECIALORDERID = O.SPECIALORDERID and SO.SPECIALORDERMASTERID = 2)
From your description I'm not quite sure where "SOD.SPECIALORDERID <> 2 AND O.ISSPECIALORDER =0" fit into it, so please give further details of outputs if you can't resolve using subquery.
Following your clarification, please try something like this:-
SELECT distinct C.CUSTOMERID,C.CUSTOMERNAME
FROM #ORDER O
INNER JOIN #CUSTOMER C ON C.CUSTOMERID=O.CUSTOMERID
where not exists
(select null from #SPECIALORDERDTL SO
INNER JOIN #SPECIALORDERDATA SOD ON SO.SPECIALORDERDATAID = SOD.SPECIALORDERDATAID
where SO.SPECIALORDERID = O.SPECIALORDERID and
SOD.SPECIALORDERMASTERID = 2
)
order by C.CUSTOMERNAME

How to merge list from SQL Server stored procedure

ALTER PROCEDURE [dbo].[spGetItemCategories]
AS
BEGIN
SET NOCOUNT ON;
--SELECT * FROM ItemCategories
SELECT
IC.Id, IC.Name ,C.Id AS CompanyId, C.Name AS CompanName
FROM
ItemCategories IC
JOIN
CompanyItems CI ON IC.Id = CI.ItemCategoryId
JOIN
Companies C ON CI.CompanyId = C.Id
--WHERE CI.CompanyId IN (SELECT TOP(100)* FROM Companies C)
END
This displays data like:
4 sdfs 14 Nestle
4 sdfs 15 Unilever
but I want to get like this:
4 sdfs 14 Nestle
15 Unilever
You can check this method but the same data.
declare #mytable table (compid int,compname varchar(20),itemid int, itemdesc varchar(20))
insert into #mytable
values
(1,'Company A',100,'Nestle'),
(1,'Company A',200,'UniLever'),
(2,'Company B',300,'Citrix'),
(2,'Company B',400,'SQL'),
(2,'Company B',500,'Oracle'),
(1,'Company B',600,'Microsoft')
select
iif(left(m1.ord_id,1)>1,NULL,m.compid) [CompID],
iif(left(m1.ord_id,1)>1,NULL,m.compname) [CompName],
m.itemid,
m.itemdesc
from #mytable m
inner join (
select distinct compid,row_number() over (partition by compid order by itemid) [ord_id], itemid
from #mytable) m1
on m.compid = m1.compid and m.itemid = m1.itemid
or CTE
;with cte as
(
select distinct compid,row_number() over (partition by compid order by itemid) [ord_id], itemid
from #mytable
)
select
iif(left(m1.ord_id,1)>1,NULL,m.compid) [CompID],
iif(left(m1.ord_id,1)>1,NULL,m.compname) [CompName],
m.itemid,
m.itemdesc
from #mytable m
inner join cte m1
on m.compid = m1.compid and m.itemid = m1.itemid
if you are not happy with nulls replace the fields
iif(left(m1.ord_id,1)>1,'',cast(m.compid as varchar)) [CompID],
iif(left(m1.ord_id,1)>1,'',m.compname) [CompName],
Result
CompID CompName itemid itemdesc
1 Company A 100 Nestle
200 UniLever
600 Microsoft
2 Company B 300 Citrix
400 SQL
500 Oracle

Trouble deleting duplicate records using partition and rank() in sql server

I am trying to identify duplicate records and then delete one of the duplicate record using PARTITION and RANK() n SQL Server 2008. I condition to delete duplicate record is that it should not be referenced in another table.
I have a Language table that has some duplicate languages. Employee table has employees and mapping to language. I have to delete one of the duplicate records if that Language id is not being mapped in Employee table.
CREATE TABLE MY_LANGUAGE (LANGUAGEID INT, LANGUAGENAME VARCHAR(20))
CREATE TABLE MY_EMPLOYEE (EMPID INT, NAME VARCHAR(20), LANGUAGEID INT)
INSERT INTO MY_LANGUAGE VALUES(1, 'ENGLISH')
INSERT INTO MY_LANGUAGE VALUES(2, 'FRENCH')
INSERT INTO MY_LANGUAGE VALUES(3, 'ITALIAN')
INSERT INTO MY_LANGUAGE VALUES(4, 'GERMAN')
INSERT INTO MY_LANGUAGE VALUES(5, 'ITALIAN')
INSERT INTO MY_LANGUAGE VALUES(6, 'GERMAN')
INSERT INTO MY_LANGUAGE VALUES(7, 'SPANISH')
INSERT INTO MY_EMPLOYEE VALUES (10, 'GLEN', 1)
INSERT INTO MY_EMPLOYEE VALUES (20, 'PETER', 2)
INSERT INTO MY_EMPLOYEE VALUES (30, 'MARIA', 3)
If you see, I have two languages that are duplicate and one of them is being used by an employee. I want to delete language ids 4 and 5.
LANGUAGENAME LANGUAGEID EMPNAME
GERMAN 4
GERMAN 6
ITALIAN 3 MARIA
ITALIAN 5
I have tried to create a select statement to return what I want to delete:
WITH CTE AS (
SELECT L.LANGUAGENAME, L.LANGUAGEID, RANK() OVER(PARTITION BY L.LANGUAGENAME ORDER BY L.LANGUAGEID) AS RANKING
FROM MY_LANGUAGE L
INNER JOIN (
SELECT LANGUAGENAME, COUNT(*) AS DUPECOUNT
FROM MY_LANGUAGE
GROUP BY LANGUAGENAME
HAVING COUNT(*) > 1
) LC ON L.LANGUAGENAME = LC.LANGUAGENAME
WHERE NOT EXISTS (SELECT 1 FROM MY_EMPLOYEE WHERE MY_EMPLOYEE.LANGUAGEID = L.LANGUAGEID))
SELECT * FROM CTE WHERE RANKING = 1
This return the following
LANGUAGENAME LANGUAGEID RANKING
GERMAN 4 1
ITALIAN 5 1
When I try to delete I get an error:
WITH CTE AS (
SELECT L.LANGUAGENAME, L.LANGUAGEID, RANK() OVER(PARTITION BY L.LANGUAGENAME ORDER BY L.LANGUAGEID) AS RANKING
FROM MY_LANGUAGE L
INNER JOIN (
SELECT LANGUAGENAME, COUNT(*) AS DUPECOUNT
FROM MY_LANGUAGE
GROUP BY LANGUAGENAME
HAVING COUNT(*) > 1
) LC ON L.LANGUAGENAME = LC.LANGUAGENAME
WHERE NOT EXISTS (SELECT 1 FROM MY_EMPLOYEE WHERE MY_EMPLOYEE.LANGUAGEID = L.LANGUAGEID))
DELETE FROM CTE WHERE RANKING = 1
Error that I get is:
Msg 4405, Level 16, State 1, Line 1
View or function 'CTE' is not updatable because the modification affects multiple base tables.
Any ideas how to fix this or may be it can be simplified. Thanks to #Szymon for showing a temp table solution but I am hoping to get a solution without temp tables (if possible).
Query:
DELETE ll
FROM MY_LANGUAGE ll
JOIN (SELECT L.LANGUAGENAME,
L.LANGUAGEID,
ROW_NUMBER()OVER(PARTITION BY L.LANGUAGENAME, e.EMPID
ORDER BY L.LANGUAGEID ASC) rnk,
COUNT(*)OVER(PARTITION BY L.LANGUAGENAME) cnt,
e.EMPID
FROM MY_LANGUAGE l
LEFT JOIN MY_EMPLOYEE e ON e.LANGUAGEID = l.LANGUAGEID) a
ON ll.LANGUAGEID = a.LANGUAGEID
AND a.rnk = 1
AND a.cnt > 1
and a.EMPID IS NULL
Result:
| LANGUAGEID | LANGUAGENAME |
|------------|--------------|
| 1 | ENGLISH |
| 2 | FRENCH |
| 3 | ITALIAN |
| 6 | GERMAN |
| 7 | SPANISH |
You can get the records from your CTE query into a temporary table and then delete based on that table.
WITH CTE AS (
SELECT L.LANGUAGENAME, L.LANGUAGEID, RANK() OVER(PARTITION BY L.LANGUAGENAME ORDER BY L.LANGUAGEID) AS RANKING
FROM MY_LANGUAGE L
INNER JOIN (
SELECT LANGUAGENAME, COUNT(*) AS DUPECOUNT
FROM MY_LANGUAGE
GROUP BY LANGUAGENAME
HAVING COUNT(*) > 1
) LC ON L.LANGUAGENAME = LC.LANGUAGENAME
WHERE NOT EXISTS (SELECT 1 FROM MY_EMPLOYEE WHERE MY_EMPLOYEE.LANGUAGEID = L.LANGUAGEID))
SELECT * INTO #temp FROM CTE WHERE RANKING = 1
And then use records from #temp to delete from the original table.
Try this..
;WITH CTE AS (
SELECT t.LANGUAGEID, LANGUAGENAME, RANK() OVER(PARTITION BY T.LANGUAGENAME ORDER BY T.LANGUAGEID) AS RANKING
FROM MY_LANGUAGE T
WHERE T.LANGUAGEID IN (
SELECT L.LANGUAGEID
FROM MY_LANGUAGE L LEFT JOIN MY_EMPLOYEE E
ON L.LANGUAGEID = E.LANGUAGEID
WHERE E.LANGUAGEID IS NULL)
)
DELETE FROM CTE
WHERE RANKING = 1 AND (CTE.LANGUAGENAME IN (SELECT LANGUAGENAME
FROM MY_LANGUAGE
GROUP BY LANGUAGENAME
HAVING COUNT(LANGUAGENAME) > 1))

Recursively find all ancestors given the child

Given a child id, I need to return a query containing all parents of that child as well as their parents till I get to the root parent.
For example, given this data:
ID / Parent ID
1 / 0
2 / 1
3 / 2
4 / 0
5 / 3
So if I passed in ID 5 I would like to get a query with the results:
ID / Parent ID
1 / 0
2 / 1
3 / 2
This table does not work with a hierarchyid type so I suspect that this will need to be done with a CTE, but have no clue how. If it can be done in an SQL query / proc, any help would be appreciated.
Thanks
This is more or less what you want:
-- CTE to prepare hierarchical result set
;WITH #results AS
(
SELECT id,
parentid
FROM [table]
WHERE id = #childId
UNION ALL
SELECT t.id,
t.parentid
FROM [table] t
INNER JOIN #results r ON r.parentid = t.id
)
SELECT *
FROM #results;
Reference:
CTE: Common Table Expression
Working example:
-- create table with self lookup (parent id)
CREATE TABLE #tmp (id INT, parentid INT);
-- insert some test data
INSERT INTO #tmp (id, parentid)
SELECT 1,0 UNION ALL SELECT 2,1 UNION ALL SELECT 3,2
UNION ALL SELECT 4,0 UNION ALL SELECT 5,3;
-- prepare the child item to look up
DECLARE #childId INT;
SET #childId = 5;
-- build the CTE
WITH #results AS
(
SELECT id,
parentid
FROM #tmp
WHERE id = #childId
UNION ALL
SELECT t.id,
t.parentid
FROM #tmp t
INNER JOIN #results r ON r.parentid = t.id
)
-- output the results
SELECT *
FROM #results
WHERE id != #childId
ORDER BY id;
-- cleanup
DROP TABLE #tmp;
Output:
1 | 0
2 | 1
3 | 2

Resources