SQL Server- Return Items Only When All Sub-Items Are Available - sql

I have an Item table (denormalized for this example) containing a list of items, parts and whether the part is available. I want to return all the items for which all the parts are available. Each item can have a varying number of parts. For example:
Item Part Available
A 1 Y
A 2 N
A 3 N
B 1 Y
B 4 Y
C 2 N
C 5 Y
D 4 Y
D 6 Y
D 7 Y
The query should return the following:
Item Part
B 1
B 4
D 4
D 6
D 7
Thanks in advance for any assistance.

Here is one trick using Max() Over() Window aggregate Function
SELECT Item,
Part
FROM (SELECT Max([Available])OVER(partition BY [Item]) m_av,*
FROM yourtable) a
WHERE m_av = 'Y'
or using Group By and Having clause
Using IN clause
SELECT Item,
Part
FROM yourtable
WHERE Item IN (SELECT Item
FROM yourtable
GROUP BY Item
HAVING Count(*) = Sum(Iif(Available = 'Y', 1, 0)))
using Exists
SELECT Item,
Part
FROM yourtable A
WHERE EXISTS (SELECT 1
FROM yourtable B
WHERE A.Item = B.Item
HAVING Count(*) = Sum(Iif(Available = 'Y', 1, 0)))
using NOT EXISTS
SELECT Item,
Part
FROM yourtable A
WHERE NOT EXISTS (SELECT *
FROM yourtable B
WHERE A.Item = B.Item
AND B.Available = 'N')

I'd start with rephrasing the requirement - you want to return the items that don't have any parts that are not available. Once you put it like that, it's easy to translate the requirement to SQL using the not exists operator:
SELECT item, part
FROM parts a
WHERE NOT EXISTS (SELECT *
FROM parts b
WHERE a.item = b.item AND b.available = 'N')

Using window function does a single table read.
MIN and MAX window function
select *
from (
select
t.*,
max(available) over (partition by item) a,
min(available) over (partition by item) b
from your_table t
) t where a = b and a = 'Y';
COUNT window function:
select *
from (
select
t.*,
count(*) over (partition by item) n1
count(case when available = 'Y' then 1 end) over (partition by item) n2
from your_table t
) t where n1 = n2;

U can use NOT IN OR NOT EXISTS to achieve this
NOT EXISTS
Select item, part
from table as T1
where not exists( select 1 from tbl where item = t1.item and available = 'N')
NOT IN
Select item, part
from table
where item not in( select item from tbl where available = 'N')

I want to point out that the question in the text is: "I want to return all the items for which all the parts are available". However, your example results include the parts.
If the question is indeed that you want the items only, then you can use simple aggregation:
select item
from parts
group by item
having min(available) = max(available) and min(available) = 'Y';
If you indeed want the detail on the parts as well, then the other answers provide that information.

I do like it problems lend themselves well to being solved by infrequently used language features:
with cte as (
select * from (values
('A', 1, 'Y'),
('A', 2, 'N'),
('A', 3, 'N'),
('B', 1, 'Y'),
('B', 4, 'Y'),
('C', 2, 'N'),
('C', 5, 'Y'),
('D', 4, 'Y'),
('D', 6, 'Y'),
('D', 7, 'Y')
) as x(Item, Part, Available)
)
select *
into #t
from cte as c;
select *
from #t as c
where 'Y' = all (
select Available
from #t as a
where c.Item = a.Item
)
Here, we use a correlated subquery and the all keyword to see if all of the parts are available. My understanding is that, like exists, this will stop if it finds a counter-example.

Related

Grouping item recursively in sql

I have this table (test.mytable in the sql script below)
CREATE OR REPLACE test.mytable (item STRING(1), I_groupe STRING(1));
INSERT INTO test.mytable (item, I_groupe)
values
('A', '1'),
('B', '1'),
('B', '2'),
('C', '2'),
('D', '3'),
item
Intermediate_group
A
1
B
1
B
2
C
2
D
3
My purpose is to group the item together. My expected result is :
item
Final_group
A,B,C
1
D
2
I would like to group the item A and B because they have at least one Intermediate_group in common (Intermediate_group 1). Then I would like to group A,B with C because there is an Intermediate_group in common (Intermediate_group 2). Item D has no intermediate group in common with other items. It is therefore alone in its final group.
I have this code:
WITH TEMP1 AS (
SELECT *
FROM (
select item as item_1,
array_agg(distinct I_groupe) as I_groupe1
from test.mytable
group by item_1) AS AA
cross join
(select item as item_2,
array_agg(distinct I_groupe) as I_groupe2
from test.mytable
group by item_2
) AS BB
)
,
TEMP2 AS (
SELECT item_1, item_2,
ARRAY(SELECT * FROM TEMP.I_groupe1
INTERSECT DISTINCT
(SELECT * FROM TEMP.I_groupe2)
) AS result
FROM TEMP1
)
,
TEMP3 AS (
SELECT item_1, item_2, test
FROM TEMP2, unnest(result) as test
)
,
TEMP4 AS (
SELECT STRING_AGG(DISTINCT item_2) as item, STRING_AGG(CAST(test AS STRING)) as I_groupe
FROM TEMP3
GROUP BY item_1
)
,
TEMP5 AS (
SELECT item, I_groupe
FROM TEMP4, UNNEST(SPLIT(item)) as item, UNNEST(SPLIT(I_groupe)) as I_groupe
)
I repeat this code/process manually three times for this "toy" example and finish by a select distinct to get only one row by Final_group
SELECT DISTINCT *
FROM TEMP14
But in a real example it's not scalable. I would like to use a recursive function or a loop to automate this code.
Thanks in advance for your help

Get only best ranked rows from a subquery

I want to get the price of an article for a specific customer.
There are several levels of prices which i ranked in my query.
So Article A has a price on rank 1, 4, 6. The result should always be the lowest ranked price.
Article B rank 3 ,5
So article A price is ranked 1 and Article b is price ranked 3.
My query is below .
SELECT p2.* FROM(
SElect ART_ID, MIN(RANG) RANG FROM (
Select p.ART_ID, p.betrag ,
CASE p.PREIS_EBENE WHEN 'KA' THEN 1 WHEN 'KW' THEN 2 WHEN 'W' THEN 7 WHEN 'A' THEN 6 ELSE 99 END RANG
FROM MDART a
INNER JOIN MDPRSVK p ON (a.KLIENT_ID = p.KLIENT_ID AND a.ART_ID = p.ART_ID)
WHERE ICP_KZ.IS_SET(KENNUNG_USER, 'P') = 1
ORDER BY RANG)
GROUP BY ART_ID) T
INNER JOIN MDPRSVK p2 ON (p2.ART_ID = T.ART_ID AND p2.PREIS_EBENE = p.PREIS_EBENE)
i want to have every article appearing only once in the result
You have tagged your request PL/SQL, so I guess your DBMS may be Oracle.
If I understand correctly, the table MDPRSVK contains several prices per ART_ID. And you want to select each ART_ID's best price (best to worst: 'KA' -> 'KW' -> 'A' -> 'W' -> any other PREIS_EBENE).
You can use a window function (ROW_NUMBER, RANK or DENSE_RANK) for this:
select *
from mdprsvk
order by row_number()
over (partition by art_id
order by decode(preis_ebene, 'KA', 1, 'KW', 2, 'A', 3, 'W', 4, 5))
fetch first row with ties;
This is standard SQL. In Oracle, FETCH FIRST is available as of version 12c. In earlier versions you'd use a subquery instead:
select *
from
(
select
mdprsvk.*,
row_number() over (partition by art_id
order by decode(preis_ebene, 'KA', 1, 'KW', 2, 'A', 3, 'W', 4, 5))
as rn
from mdprsvk
)
where rn = 1;
Or use OraclesKEEP FIRST`:
select art_id, max(betrag)
keep (dense_rank first
order by decode(preis_ebene, 'KA', 1, 'KW', 2, 'A', 3, 'W', 4, 5))
from mdprsvk
group by art_id;
It is not clear, how MDART comes into play. It looks like you want to restrict your results to articles for certain clients and KENNUNG_USER is the column in MDART to check. If so, add a WHERE clause:
where exists
(
select *
from mdart
where mdart.klient_id = mdprsvk.klient_id
and mdart.art_id = mdprsvk.art_id
and icp_kz.is_set(mdart.kennung_user, 'p') = 1
)
Or with IN instead of EXISTS:
where (klient_id, art_id) in
(
select klient_id, art_id
from mdart
where icp_kz.is_set(kennung_user, 'p') = 1
)

ORACLE sum inside a case statement

Hi I need the result of this. so if a entityID matches to a value I need the sum of certain column.I am getting an expression missing error. Can someone point me to where the error is?
Thanks.
SELECT
p.jobTitle,
p.department,
p.person,
ufr.meets,
ufr.exceeds,
CASE
WHEN ufr.entityid = 'AHT' THEN (AD.acdcalls + AD.daacdcalls)
WHEN ufr.entityid = 'ACW' THEN (AD.acdcalls + AD.daacdcalls)
WHEN ufr.entityid = 'Adherence' THEN SUM(AA.totalSched)
WHEN ufr.entityid = 'Conformance' THEN SUM(AS.minutes)
ELSE null
END as weight,
(weight * meets) AS weightedMeets,
(weight * exceeds) AS weightedExceeds
FROM M_PERSON p
JOIN A_TMP5408_UNFLTRDRESULTSAG ufr
ON ufr.department = p.department AND ufr.jobTitle = p.jobTitle
LEFT JOIN M_AvayaDAgentChunk AD
ON AD.person = p.person and ufr.split = AD.split
LEFT JOIN M_AgentAdherenceChunk AA
ON AA.person = p.person
LEFT JOIN M_AgentScheduleChunk AS
ON AS.person = p.person
GROUP BY
p.person,
p.department,
p.jobTitle,
ufr.meets,
ufr.exceeds,
weight,
weightedMeets,
weightedExceeds
As well as the issues mentioned by #GordonLinoff (that AS is a keyword) and #DCookie (you need entityid in the group-by):
you also need acdcalls and daacdcalls in the group-by (unless you can aggregate those);
you can't refer to a column alias in the same level of query, so (weight * meets) AS weightedMeets isn't allowed - you've just define what weight is, in the same select list. You need to use an inline view, or a CTE, if you don't want to repeat the case logic.
I think this does what you want:
SELECT
jobTitle,
department,
person,
meets,
exceeds,
weight,
(weight * meets) AS weightedMeets,
(weight * exceeds) AS weightedExceeds
FROM
(
SELECT
MP.jobTitle,
MP.department,
MP.person,
ufr.meets,
ufr.exceeds,
CASE
WHEN ufr.entityid = 'AHT' THEN (MADAC.acdcalls + MADAC.daacdcalls)
WHEN ufr.entityid = 'ACW' THEN (MADAC.acdcalls + MADAC.daacdcalls)
WHEN ufr.entityid = 'Adherence' THEN SUM(MAAC.totalSched)
WHEN ufr.entityid = 'Conformance' THEN SUM(MASC.minutes)
ELSE null
END as weight
FROM M_PERSON MP
JOIN A_TMP5408_UNFLTRDRESULTSAG ufr
ON ufr.department = MP.department AND ufr.jobTitle = MP.jobTitle
LEFT JOIN M_AvayaDAgentChunk MADAC
ON MADAC.person = MP.person and ufr.split = MADAC.split
LEFT JOIN M_AgentAdherenceChunk MAAC
ON MAAC.person = MP.person
LEFT JOIN M_AgentScheduleChunk MASC
ON MASC.person = MP.person
GROUP BY
MP.person,
MP.department,
MP.jobTitle,
ufr.meets,
ufr.exceeds,
ufr.entityid,
MADAC.acdcalls,
MADAC.daacdcalls
);
Your fist two case branches could be combined since the calculation is the same, but will work either way.
In addition to the alias issue identified by Gordon, I think you'll find you need to use an aggregate function in all the THEN clauses of your CASE statement, and that you need to GROUP BY ufr.entityid as well. Otherwise you'll start getting ora-00979 errors (not a GROUP BY expression). If you don't want the aggregate function in all clauses, then you'll have to group by the expressions you're summing as well.
Small illustration:
CREATE TABLE tt (ID varchar2(32), sub_id varchar2(32), x NUMBER, y NUMBER);
INSERT INTO tt VALUES ('ID1', 'A', 1, 6);
INSERT INTO tt VALUES ('ID1', 'B', 1, 7);
INSERT INTO tt VALUES ('ID2', 'A', 2, 6);
INSERT INTO tt VALUES ('ID2', 'B', 2, 7);
INSERT INTO tt VALUES ('ID3', 'A', 3, 6);
INSERT INTO tt VALUES ('ID3', 'B', 3, 7);
INSERT INTO tt VALUES ('ID3', 'C', 3, 8);
SELECT ID, CASE WHEN sub_id = 'A' THEN SUM(y)
WHEN sub_id = 'B' THEN SUM(x)
ELSE (x + y) END tst
FROM tt
GROUP BY ID
ORA-00979: not a GROUP BY expression (points at sub_id in WHEN)
SELECT ID, CASE WHEN sub_id = 'A' THEN SUM(y)
WHEN sub_id = 'B' THEN SUM(x)
ELSE (x + y) END tst
FROM tt
GROUP BY ID, sub_id
ORA-00979: not a GROUP BY expression (points at x in ELSE)
SQL> SELECT ID, CASE WHEN sub_id = 'A' THEN SUM(y)
2 WHEN sub_id = 'B' THEN SUM(x)
3 ELSE SUM(x + y) END tst
4 FROM tt
5 GROUP BY ID, sub_id;
ID TST
-------------------------------- ----------
ID1 6
ID3 6
ID3 3
ID1 1
ID2 6
ID2 2
ID3 11

Delete rows in table that are sum of other rows per group

Group rows by T, and in each group find the row that is the largest or smallest (if values are negative) sum of other rows from that group, and delete that row (one for each group), if group does not have enough elements to find sum or enough but none of the rows indicates sum of others nothing happens
CREATE TABLE Test (
T varchar(10),
V int
);
INSERT INTO Test
VALUES ('A', 4),
('B', -5),
('C', 5),
('A', 2),
('B', -1),
('C', 10),
('A', 2),
('B', -4),
('C', 5),
('D', 0);
expected result:
A 2
A 2
B -1
B -4
C 5
C 5
D 0
Like the comments, the requirements seem strange. The below code assumes that the summing is already pre-populated and merely removes the largest/smallest as long as the highest value is not 0.
if object_id('tempdb..#test') is not null
drop table #test
CREATE TABLE #Test (
T varchar(10),
V int
);
INSERT INTO #Test
VALUES ('A', 4), ('B', -5), ('C', 5), ('A', 2), ('B', -1), ('C', 10), ('A', 2), ('B', -4), ('C', 5), ('D', 0);
if object_id('tempdb..#test2') is not null
drop table #test2
SELECT
T,
V,
ABS(V) as absV
INTO #TEST2
FROM #TEST
SELECT * FROM #TEST2
if object_id('tempdb..#max') is not null
drop table #max
SELECT
T,
MAX(absV) AS MaxAbsV
INTO #Max
FROM #TEST2
GROUP BY T
HAVING MAX(AbsV) != 0
DELETE #TEST2
FROM #TEST2
INNER JOIN #MAX ON #TEST2.T = #MAX.T AND #TEST2.absV = #Max.MaxAbsV
SELECT * FROM #TEST2
ORDER BY T ASC
; with cte as
(
select T, V,
R = row_number() over (partition by T order by ABS(V) desc),
C = count(*) over (partition by T)
from Test
)
delete c
from cte c
inner join
(
select T, S = sum(V)
from cte
where R <> 1
group by T
) s on c.T = s.T
where c.C >= 3
and c.R = 1
and c.V = s.S
Using ABS and NOT Exists
DECLARE #Test TABLE (
T varchar(10),
V int
);
INSERT INTO #Test
VALUES ('A', 4), ('B', -5), ('C', 5), ('A', 2), ('B', -1), ('C', 10), ('A', 2), ('B', -4), ('C', 5), ('D', 0);
;WITH CTE as (
select T,max(ABS(v ))v from #Test
WHERE V <> 0
GROUP BY T )
SELECT T,V FROM #Test T where NOT exists (Select 1 FROM cte WHERE T = T.T AND v = ABS(T.V) )
ORDER BY T.T
Determine first if the rows are positive or negative by checking if SUM(V) is positive. And then determine if the smallest or largest value is equal to the SUM of the other rows, by subtracting from SUM(V) the MIN(V) if negative or MAX(V) if positive:
DELETE t
FROM Test t
INNER JOIN (
SELECT
T,
SUM(V) - CASE WHEN SUM(V) >= 0 THEN MAX(V) ELSE MIN(V) END AS ToDelete
FROM Test
GROUP BY T
HAVING COUNT(*) >= 3
) a
ON a.T = t.T
AND a.ToDelete = t.V
ONLINE DEMO
You can use the below query to get the required output :-
select * into #t1 from test
select * from
(
select TT.T as T,TT.V as V
from test TT
JOIN
(select T,max(abs(V)) as V from #t1
group by T) P
on TT.T=P.T
where abs(TT.V) <> P.V
UNION ALL
select A.T as T,A.V as V from test A
JOIN(
select T,count(T) as Tcount from test
group by T
having count(T)=1) B on A.T=B.T
) X order by T
drop table #t1
You are looking for a value per group that is the sum of all the group's other values. E.g. 4 of (2,2,4) or -5 of (-5,-4,-1).
This is usually only one record per group. But it can be multiple times the same number. Here are examples for ties: (0,0) or (-2,2,4,4), or (-2,-2,4,4,4) or (-10,3,3,3,3,4).
As you see, you are looking in any way for values that equal half of the group's total sum. (Of course. We are looking for n+n, where one n is in one record and the other n is the sum of all the other records.)
The only special case is when there is only one value in the group which is zero. That we don't want to delete of course.
Here is an update statement that cannot deal with ties, but would delete all maximum values instead of just one:
delete from test
where 2 * v =
(
select case when count(*) = 1 then null else sum(v) end
from test fullgroup
where fullgroup.t = test.t
);
In order to deal with ties you would need artificial row numbers, so as to delete only one record of all candidates.
with candidates as
(
select t, v, row_number() over (partition by t order by t) as rn
from
(
select
t, v,
sum(v) over (partition by t) as sumv,
count(*) over (partition by t) as cnt
from test
) comparables
where sumv = 2 * v and cnt > 1
)
delete
from candidates
where rn = 1;
SQL fiddle: http://sqlfiddle.com/#!6/6d97e/1
See if below query helps:
DELETE [Audit].[dbo].[Test] FROM [Audit].[dbo].[Test] as AA
INNER JOIN (select T,
CASE
WHEN MAX(V) < 0 THEN MIN(V)
WHEN MIN(V) > 0 THEN MAX(V) ELSE MAX(V)
END as MAX_V,
CASE
WHEN SUM(V) > 0 THEN SUM(V) - MAX(V)
WHEN SUM(V) < 0 THEN SUM(V) - MIN(V) ELSE SUM(V)
END as SUM_V_REST
from [Audit].[dbo].[Test]
Group by T
Having Count(V) > 1) as BB ON AA.T = BB.T and AA.V = BB.MAX_V

Stuck on this union / except

Trying to find the best way to proceed with this, for some reason it is really tripping me up.
I have data like this:
transaction_id(pk) decision_id(pk) accepted_ind
A 1 NULL
A 2 <blank>
A 4 Y
B 1 <blank>
B 2 Y
C 1 Y
D 1 N
D 2 O
D 3 Y
Each transaction is guaranteed to have decision 1
There can be multiple decision possibilities (what-if's) type of scenarios
Accepted can have multiple values or be blank or NULL but only one can be accepted_ind = Y
I am trying to write a query to:
Return one row for each transaction_id
Return the decision_id where the accepted_ind = Y or if the transaction has no rows accepted_ind = Y, then return the row with decision_id = 1 (regardless of value in the accepted_ind)
I have tried:
1. Using logical "or" to pull the records, kept getting duplicates.
2. Using a union and except but can not quite get the logic down correctly.
Any assistance is appreciated. I am not sure why this is tripping me up so much!
Adam
Try this. Basically the WHERE clause says:
Where Accepted = 'Y'
OR
There is no accepted row for this transaction and the decision_id = 1
SELECT Transaction_id, Decision_ID, Accepted_id
FROM MyTable t
WHERE Accepted_ind = 'Y'
OR (NOT EXISTS (SELECT 1 FROM MyTable t2
WHERE Accepted_ind = 'Y'
and t2.Transaction_id = t.transaction_id)
AND Decision_id = 1)
This approach uses ROW_NUMBER() and therefore will only work on SQL Server 2005 or later
I have modified your sample data as as it stands, all transaction_id have a Y indicator!
DECLARE #t TABLE (
transaction_id NCHAR(1),
decision_id INT,
accepted_ind NCHAR(1) NULL
)
INSERT #t VALUES
( 'A' , 1 , NULL ),
( 'A' , 2 , '' ),
( 'A' , 4 , 'Y' ),
( 'B' , 1 , '' ),
( 'B' , 2 , 'N' ), -- change from your sample data
( 'C' , 1 , 'Y' ),
( 'D' , 1 , 'N' ),
( 'D' , 2 , 'O' ),
( 'D' , 3 , 'Y' )
And here is the query itself:
SELECT transaction_id, decision_id, accepted_ind FROM (
SELECT transaction_id, decision_id, accepted_ind,
ROW_NUMBER() OVER (
PARTITION BY transaction_id
ORDER BY
CASE
WHEN accepted_ind = 'Y' THEN 1
WHEN decision_id = 1 THEN 2
ELSE 3
END
) rn
FROM #t
) Raw
WHERE rn = 1
Results:
transaction_id decision_id accepted_ind
-------------- ----------- ------------
A 4 Y
B 1
C 1 Y
D 3 Y
The ROW_NUMBER() clause gives a 'priority' to each criterion you mention; we then ORDER BY to pick the best, and take the first row.
There's probably a neater/more efficient query, but I think this will get the job done. It assumes the table name is Decision:
SELECT CASE
WHEN accepteddecision.transaction_id IS NOT NULL THEN
accepteddecision.transaction_id
ELSE firstdecision.transaction_id
END AS transaction_id,
CASE
WHEN accepteddecision.decision_id IS NOT NULL THEN
accepteddecision.decision_id
ELSE firstdecision.decision_id
END AS decision_id,
CASE
WHEN accepteddecision.accepted_ind IS NOT NULL THEN
accepteddecision.accepted_ind
ELSE firstdecision.accepted_ind
END AS accepted_ind
FROM decision
LEFT OUTER JOIN (SELECT *
FROM decision AS accepteddecision
WHERE accepteddecision.accepted_ind = 'Y') AS
accepteddecision
ON accepteddecision.transaction_id = decision.transaction_id
LEFT OUTER JOIN (SELECT *
FROM decision AS firstdecision
WHERE firstdecision.decision_id = 1) AS firstdecision
ON firstdecision.transaction_id = decision.transaction_id
GROUP BY accepteddecision.transaction_id,
firstdecision.transaction_id,
accepteddecision.decision_id,
firstdecision.decision_id,
accepteddecision.accepted_ind,
firstdecision.accepted_ind
Out of interest, the following uses UNION and EXCEPT (plus a JOIN) as specified in the question title:
WITH T AS (SELECT * FROM (
VALUES ('A', 1, NULL),
('A', 2, ''),
('A', 4, 'Y'),
('B', 1, ''),
('B', 2, 'Y'),
('C', 1, 'Y'),
('D', 1, 'N'),
('D', 2, 'O'),
('D', 3, 'Y'),
('E', 2, 'O'), -- smaple data extended
('E', 1, 'N') -- smaple data extended
) AS T (transaction_id, decision_id, accepted_ind)
)
SELECT *
FROM T
WHERE accepted_ind = 'Y'
UNION
SELECT T.*
FROM (
SELECT transaction_id
FROM T
WHERE decision_id = 1
EXCEPT
SELECT transaction_id
FROM T
WHERE accepted_ind = 'Y'
) D
JOIN T
ON T.transaction_id = D.transaction_id
AND T.decision_id = 1;