Excluding records from table based on rules from another table - sql

I'm using Oracle SQL and I have a product table with diffrent attributes and sales volume for each product and another table with certain exclusion rules for different level of aggregation. Let's look at the example:
Here is our main table with sales data on which we want to perform some calculations:
And the other table contains diffrent rules which are supposed to exclude certain rows from table above:
When there is an "x", this column shouldn't be considered so our rules are:
1. exclude all rows with ATTR_3 = 'no'
2. exlcude all rows with ATTR_1 = 'Europe' and ATTR_2 = 'snacks' and ATTR_3 = 'no'
3. exlcude all rows with ATTR_1 = 'Africa'
And based on that our final output should be like that:
How this could be achived in SQL? I was thinking about join but I have no idea how to handle different levels of aggregation for exclusions.

I think your expected output is wrong. None of the rules excludes the 2nd row (Europe - snacks - yes).
SQL> with
2 -- sample data
3 test (product_id, attr_1, attr_2, attr_3) as
4 (select 81928 , 'Europe', 'beverages', 'yes' from dual union all
5 select 16534 , 'Europe', 'snacks' , 'yes' from dual union all
6 select 56468 , 'USA' , 'snacks' , 'no' from dual union all
7 select 129921, 'Africa', 'drinks' , 'yes' from dual union all
8 select 123021, 'Africa', 'snacks' , 'yes' from dual union all
9 select 165132, 'USA' , 'drinks' , 'yes' from dual
10 ),
11 rules (attr_1, attr_2, attr_3) as
12 (select 'x' , 'x' , 'no' from dual union all
13 select 'Europe', 'snacks', 'no' from dual union all
14 select 'Africa', 'x' , 'x' from dual
15 )
16 -- query you need
17 select t.*
18 from test t
19 where (t.attr_1, t.attr_2, t.attr_3) not in
20 (select
21 decode(r.attr_1, 'x', t.attr_1, r.attr_1),
22 decode(r.attr_2, 'x', t.attr_2, r.attr_2),
23 decode(r.attr_3, 'x', t.attr_3, r.attr_3)
24 from rules r
25 );
PRODUCT_ID ATTR_1 ATTR_2 ATT
---------- ------ --------- ---
81928 Europe beverages yes
16534 Europe snacks yes
165132 USA drinks yes
SQL>

You can use the join using CASE .. WHEN statement as follows:
SELECT P.*
FROM PRODUCT P
JOIN RULESS R ON
(R.ATTR_1 ='X' OR P.ATTR_1 <> R.ATTR_1)
AND (R.ATTR_2 ='X' OR P.ATTR_2 <> R.ATTR_2)
AND (R.ATTR_3 ='X' OR P.ATTR_3 <> R.ATTR_3)

You can use NOT EXISTS
SELECT *
FROM sales s
WHERE NOT EXISTS (
SELECT 0
FROM attributes a
WHERE ( ( a.attr_1 = s.attr_1 AND a.attr_1 IS NOT NULL )
OR a.attr_1 IS NULL )
AND ( ( a.attr_2 = s.attr_2 AND a.attr_2 IS NOT NULL )
OR a.attr_2 IS NULL )
AND ( ( a.attr_3 = s.attr_3 AND a.attr_3 IS NOT NULL )
OR a.attr_3 IS NULL )
)
where I considered the x values within the attributes table as NULL. If you really have x characters, then you can use :
SELECT *
FROM sales s
WHERE NOT EXISTS (
SELECT 0
FROM attributes a
WHERE ( ( NVL(a.attr_1,'x') = s.attr_1 AND NVL(a.attr_1,'x')!='x' )
OR NVL(a.attr_1,'x')='x' )
AND ( ( NVL(a.attr_2,'x') = s.attr_2 AND NVL(a.attr_2,'x')!='x' )
OR NVL(a.attr_2,'x')='x' )
AND ( ( NVL(a.attr_3,'x') = s.attr_3 AND NVL(a.attr_3,'x')!='x' )
OR NVL(a.attr_3,'x')='x' )
)
instead.
Demo

I would do this with three different not exists:
select p.*
from product p
where not exists (select 1
from rules r
where r.attr_1 = p.attr_1 and r.attr_1 <> 'x'
) and
not exists (select 1
from rules r
where r.attr_2 = p.attr_2 and r.attr_2 <> 'x'
) and
not exists (select 1
from rules r
where r.attr_3 = p.attr_3 and r.attr_3 <> 'x'
) ;
In particular, this can take advantage of indexes on (attr_1), (attri_2) and (attr_3) -- something that is quite handy if you have a moderate number of rules.

Related

How do I select rows from table that have one or more than one specific value in a column?

I have a table containing data such as:
BP_NUMBER,CONTRACT_TYPE
0000123, 1
0000123, 2
0000123, 3
0000123, 4
0000124, 4
0000124, 4
0000124, 4
0000125, 4
0000126, 1
0000126, 5
I want to select rows containing one or more occurrences of CONTRACT_TYPE = 4. In other words, I want to know who are the clients with one or more contracts of the same type and type 4.
I tried this query:
SELECT * FROM (
SELECT BP_NUMBER, CONTRACT_TYPE, COUNT(*) OVER (PARTITION BY BP_NUMBER) CT FROM CONTRACTS
WHERE (1=1)
AND DATE = '18/10/2022'
AND CONTRACT_TYPE = 4)
WHERE CT= 1;
But it returns rows with only one occurrence of CONTRACT_TYPE = 4.
Also tried something like:
SELECT BP_NUMBER FROM CONTRACTS
WHERE (1=1)
AND CONTRACT_TYPE = 4
AND CONTRACT_TYPE NOT IN (SELECT CONTRACT_TYPE FROM CONTRACTS WHERE CONTRACT_TYPE != 4 GROUP BY CONTRACT_TYPE);
Trying to avoid any other contract types than 4. I really don't understand why it doesn't work.
The expected result would be:
0000124 --(4 occurrences of type 4)
0000125 --(1 occurrence of type 4)
Any help? Thanks
You can try something like this:
SELECT
BP_NUMBER
FROM CONTRACTS c1
WHERE CONTRACT_TYPE = 4
AND NOT EXISTS
(SELECT 1 FROM CONTRACTS c2 WHERE c2.BP_NUMBER = c1.BP_NUMBER
AND c2.CONTRACT_TYPE <> c1.CONTRACT_TYPE)
Depending on how you actually want to see it (and what other values you might want to include), you could either do a DISTINCT on the BP_NUMBER, or group on that column (and potentially others)
A similar result could also be achieved using an outer join between two instances of the CONTRACTS table. Essentially, you need the second instance of the same table so that you can exclude output rows when there are records with the "unwanted" contract types
You can just do the aggregation like here:
WITH
tbl AS
(
Select '0000123' "BP_NUMBER", '1' "CONTRACT_TYPE" From Dual Union All
Select '0000123', '2' From Dual Union All
Select '0000123', '3' From Dual Union All
Select '0000123', '4' From Dual Union All
Select '0000124', '4' From Dual Union All
Select '0000124', '4' From Dual Union All
Select '0000124', '4' From Dual Union All
Select '0000125', '4' From Dual Union All
Select '0000126', '1' From Dual Union All
Select '0000126', '5' From Dual
)
Select
BP_NUMBER "BP_NUMBER",
Count(*) "OCCURENCIES"
From
tbl
WHERE CONTRACT_TYPE = '4'
GROUP BY BP_NUMBER
ORDER BY BP_NUMBER
--
-- R e s u l t :
--
-- BP_NUMBER OCCURENCIES
-- --------- -----------
-- 0000123 1
-- 0000124 3
-- 0000125 1

Oracle Query : One to Many Relationship

In my application there are one-to-many relationship as explained below.
Table one : Application
app_id
app_name
1
ABC
2
XYZ
Table two : Application_attribute [One application can have multiple attribute and variable list of attribute]
App_attr_id
app_id
attr_name
attr_value
1
1
attr1
white
2
1
attr2
12
3
1
attr3
45
4
2
attr1
red
5
2
attr2
12
6
2
attr4
45
7
2
attr7
62
Each application can have variable list of attributes.
Query Requirement
I want to fetch list of application based on multiple attribute.
Example get list of application whose attributes are attr1=white,attr2=12,attr3=45
In above case, problem can be solved by joining application table with application_attribute table 3 times but attribute will vary per application so it will not be generic solution.
Query Solution for my requirement
SELECT a.*
FROM application a,
application_attribute at1,
application_attribute at2,
application_attribute at3
WHERE a.app_id = at1.app_id
AND a.app_id = at2.app_id
AND a.app_id = at3.app_id
AND at1.attr_name = 'attr1'
AND at1.attr_value = 'white'
AND at2.attr_name = 'attr2'
AND at2.attr_value = '12'
AND at3.attr_name = 'attr3'
AND at3.attr_value = '45'
Expected Result
app_id
app_name
1
ABC
One option is two create dynamic query. Is it possible to write generic query which can be used to search n number of attribute like 3,4,5..n ?
You want to select applications where exist certain attributes. So, select from the application table and have a where clause checkink the existence of the attributes with EXISTS or IN:
select *
from aplication
where app_id in (select app_id from application_attribute where attr_name = 'attr1' and attr_value = 'white')
and app_id in (select app_id from application_attribute where attr_name = 'attr2' and attr_value = '12')
and app_id in (select app_id from application_attribute where attr_name = 'attr3' and attr_value = '45')
order by app_id;
As to generic: Simply build the query with a programming language and a loop over the desired attributes. In Oracle you can use PL/SQL for this.
You can use EXISTS and a HAVING clause in the correlated sub-query:
SELECT *
FROM application a
WHERE EXISTS (
SELECT 1
FROM application_attribute aa
WHERE a.app_id = aa.app_id
AND (aa.attr_name, aa.attr_value)
IN (('attr1', 'white'), ('attr2', '12'), ('attr3', '45'))
HAVING COUNT(/*DISTINCT*/ aa.attr_name) = 3
)
Note: If there can be duplicate attribute values then you can COUNT(DISTINCT ...) rather than just COUNT(...).
Which, for the sample data:
CREATE TABLE application (app_id, app_name) AS
SELECT 1, 'ABC' FROM DUAL UNION ALL
SELECT 2, 'XYZ' FROM DUAL;
CREATE TABLE Application_attribute (App_attr_id, app_id, attr_name, attr_value) AS
SELECT 1, 1, 'attr1', 'white' FROM DUAL UNION ALL
SELECT 2, 1, 'attr2', '12' FROM DUAL UNION ALL
SELECT 3, 1, 'attr3', '45' FROM DUAL UNION ALL
SELECT 4, 2, 'attr1', 'red' FROM DUAL UNION ALL
SELECT 5, 2, 'attr2', '12' FROM DUAL UNION ALL
SELECT 6, 2, 'attr4', '45' FROM DUAL UNION ALL
SELECT 7, 2, 'attr7', '62' FROM DUAL;
Outputs:
APP_ID
APP_NAME
1
ABC
db<>fiddle here
You can join tables only once by conditionally matching the values of attr_name and attr_value columns while filtering out the result only for the returning records with exactly triple attribute names such as
SELECT a.app_id, a.app_name
FROM application a
JOIN application_attribute aa
ON aa.app_id = a.app_id
WHERE DECODE( aa.attr_name, 'attr1', aa.attr_value ) = 'white' -- you can make these literals parametric by prefixing with : or & such as &p_attr1 and enter 'white' whenever prompted
OR DECODE( aa.attr_name, 'attr2', aa.attr_value ) = '12'
OR DECODE( aa.attr_name, 'attr3', aa.attr_value ) = '45'
GROUP BY a.app_id, a.app_name
HAVING COUNT(DISTINCT aa.attr_name)=3
Demo
If we re-write your query using joins there will be the 3 variables that you need to change in the WHERE. This will make it much easier to modify them.
Select a.*
from application a
left join application-attribute at1 on a.app_id = at1.app_id and at1.attr_name = 'attr1'
left join application_attribute at2 on a.app_id = at2.app_id and at2.attr_name = 'attr2'
left join application_attribute at3 on a.app_id = at3.app_id and at3.attr_name = 'attr3'
left join application_attribute at4 on a.app_id = at4.app_id and at4.attr_name = 'attr4'
where at1.attr_value = 'white'
and at2.attr_value = '12'
and at3.attr_value = '45'
/* and at4.attr_value = not needed */
;

Pivoting Multiple Attributres and grouping them as a 'single' attribute (many to one)

So I Have a table called Value that's associated with different 'Fields'. Note that some of these fields have similar 'names' but they are named differently. Ultimately I want these 'similar names' to be pivoted/grouped as the same field name in the result set
VALUE_ID VALUE_TX FIELD_NAME Version_ID
1 Yes Adult 1
2 18 Age 1
3 Black Eye Color 1
4 Yes Is_Adult 2
5 25 Years_old 2
6 Brown Color_of_Eyes 2
I have a table called Submitted that looks like the following:
Version_ID Version_Name
1 TEST_RUN
2 REAL_RUN
I need a result set that Looks like this:
Submitted_Name Adult? Age Eye_Color
TEST_RUN Yes 18 Black
REAL_RUN Yes 25 Brown
I've tried the following:
SELECT * FROM (
select value_Tx, field_name, version_id
from VALUE
)
PIVOT (max (value_tx) for field_name in (('Adult', 'Is_Adult') as 'Adult?', ('Age', 'Years_old') as 'Age', ('Eye Color', 'Color_of_Eyes') as 'Eye_Color')
);
What am I doing wrong? Please let me know if I need to add any additional details / data.
Thanks in advance!
The error message that I am getting is the following:
ORA-00907: missing right parenthesis
I would change the field names in the subquery:
SELECT *
FROM (select value_Tx,
(case when field_name in ('Adult', 'Is_Adult') then 'Adult?'
field_name in ('Age', 'Years_old') then 'Age'
field_name in ('Eye Color', 'Color_of_Eyes') then 'Eye_Color'
else field_name
end) as field_name, version_id
from VALUE
)
PIVOT (max(value_tx) for field_name in ('Adult?', 'Age', 'Eye_Color'));
You can use double quotes for column aliasing within the pivot clause's part, and I think decode function suits well for this question. You can consider using the following query :
with value( value_id, value_tx, field_name, version_id ) as
(
select 1 ,'Yes' ,'Adult' ,1 from dual union all
select 2 ,'18' ,'Age' ,1 from dual union all
select 3 ,'Black','Eye_Color' ,1 from dual union all
select 4 ,'Yes' ,'Is_Adult' ,2 from dual union all
select 5 ,'25' ,'Years_old' ,2 from dual union all
select 6 ,'Brown','Color_of_Eyes',2 from dual
), Submitted( version_id, version_name ) as
(
select 1 ,'TEST_RUN' from dual union all
select 2 ,'REAL_RUN' from dual
)
select * from
(
select s.version_name as "Submitted_Name", v.value_Tx,
decode(v.field_name,'Adult','Is_Adult','Age','Years_old','Eye_Color',
'Color_of_Eyes',v.field_name) field_name
from value v
join Submitted s
on s.version_id = v.version_id
group by decode(v.field_name,'Adult','Is_Adult','Age','Years_old','Eye_Color',
'Color_of_Eyes',v.field_name),
v.value_Tx, s.Version_Name
)
pivot(
max(value_tx) for field_name in ( 'Is_Adult' as "Adult?", 'Years_old' as "Age",
'Color_of_Eyes' as "Eye_Color" )
);
Submitted_Name Adult? Age Eye_Color
REAL_RUN Yes 25 Brown
TEST_RUN Yes 18 Black
I think, better to solve as much as shorter way, as an example, using modular arithmetic would even be better as below :
select *
from
(
select s.version_name as "Submitted_Name", v.value_Tx, mod(v.value_id,3) as value_id
from value v
join Submitted s
on s.version_id = v.version_id
group by v.value_Tx, s.version_name, mod(v.value_id,3)
)
pivot(
max(value_tx) for value_id in ( 1 as "Adult?", 2 as "Age", 0 as "Eye_Color" )
)
Demo

ORACLE MAX GROUP BY

I am using Oracle (SQL Developer). Please find below the example and an outcome which would like to get (purpose of select is to find out people who submitted project A and have not done any activities in project B yet):
Data table:
CREATE TABLE "XXX"."TABLE1"
( "STATUS" VARCHAR2(20 BYTE),
"PROJECT_NAME" VARCHAR2(20 BYTE),
"VERSION_NUMBER" NUMBER,
"PERSON" VARCHAR2(20 BYTE)
);
Insert into XXX.TABLE1 (STATUS,PROJECT_NAME,VERSION_NUMBER,PERSON) values ('SUBMITTED','A','0','PETER');
Insert into XXX.TABLE1 (STATUS,PROJECT_NAME,VERSION_NUMBER,PERSON) values ('SUBMITTED','A','0','JOHN');
Insert into XXX.TABLE1 (STATUS,PROJECT_NAME,VERSION_NUMBER,PERSON) values ('SUBMITTED','A','1','JOHN');
Insert into XXX.TABLE1 (STATUS,PROJECT_NAME,VERSION_NUMBER,PERSON) values ('NEW','A','2','JOHN');
Insert into XXX.TABLE1 (STATUS,PROJECT_NAME,VERSION_NUMBER,PERSON) values ('SUBMITTED','A','0','MARY');
Insert into XXX.TABLE1 (STATUS,PROJECT_NAME,VERSION_NUMBER,PERSON) values ('SUBMITTED','B','0','PETER');
Insert into XXX.TABLE1 (STATUS,PROJECT_NAME,VERSION_NUMBER,PERSON) values ('NEW','B','1','PETER');
Insert into XXX.TABLE1 (STATUS,PROJECT_NAME,VERSION_NUMBER,PERSON) values ('SUBMITTED','B','0','JOHN');
Created table should look like this:
TABLE1:
TABLE1.STATUS TABLE1.PROJECT_NAME TABLE1.VERSION_NUMBER TABLE1.PERSON
SUBMITTED A 0 PETER
SUBMITTED A 0 JOHN
SUBMITTED A 1 JOHN
NEW A 2 JOHN
SUBMITTED A 0 MARY
SUBMITTED B 0 PETER
NEW B 1 PETER
SUBMITTED B 0 JOHN
Result what I want get is this:
STATUS PROJECT_NAME VERSION_NUMBER PERSON STATUS_1 PROJECT_NAME_1 VERSION_NUMBER_1 PERSON_1
SUBMITTED A 0 PETER NEW B 1 PETER
SUBMITTED A 1 JOHN SUBMITTED B 0 JOHN
SUBMITTED A 0 MARY
Select which I am using now is:
select t.*,v.*
from TABLE1 t
left outer join ( select u.*
from TABLE1 u
where exists (select max(z.VERSION_NUMBER)
,z.PERSON
,z.PROJECT_NAME
from TABLE1 z
where z.PROJECT_NAME = 'B'
and u.PROJECT_NAME = z.PROJECT_NAME
and u.PERSON = z.PERSON
group by z.PERSON, z.PROJECT_NAME
having u.VERSION_NUMBER = max(z.VERSION_NUMBER))) v
on t.PERSON = v.PERSON
where exists (select max (w.VERSION_NUMBER)
,w.PERSON
,w.PROJECT_NAME
from TABLE1 w
where w.PROJECT_NAME = 'A'
and w.STATUS = 'SUBMITTED'
and t.PROJECT_NAME = w.PROJECT_NAME
and t.PERSON = w.PERSON
group by w.PERSON, w.PROJECT_NAME
having t.VERSION_NUMBER = max (w.VERSION_NUMBER))
QUESTION: What would be best(right) way to write such select (best practice), should I better use Analytic functions or use something else instead of EXISTS?
I think you've over-complicated this...
WITH
project_status (status, project_name, version_number, person)
AS
(SELECT 'SUBMITTED','A','0','PETER' FROM dual UNION ALL
SELECT 'SUBMITTED','A','0','JOHN' FROM dual UNION ALL
SELECT 'SUBMITTED','A','1','JOHN' FROM dual UNION ALL
SELECT 'NEW','A','2','JOHN' FROM dual UNION ALL
SELECT 'SUBMITTED','A','0','MARY' FROM dual UNION ALL
SELECT 'SUBMITTED','B','0','PETER' FROM dual UNION ALL
SELECT 'NEW','B','1','PETER' FROM dual UNION ALL
SELECT 'SUBMITTED','B','0','JOHN' FROM dual
)
SELECT DISTINCT
ps.person
,ps.project_name
,ps.status
FROM
project_status ps
WHERE 1=1
AND ps.project_name = 'A'
AND ps.status = 'SUBMITTED'
AND NOT EXISTS
(SELECT 1
FROM project_status ps2
WHERE ps2.person = ps.person
AND ps2.project_name = 'B'
)
;
purpose of select is to find out people who submitted project A and
have not done any activities in project B yet
If your purpose is just to get the people, then you don't need the complete rows. One method to answer this is to use group by and having:
select t1.person
from "XXX"."TABLE1" t1
group by t1.person
having sum(case when project_name = 'A' and status = 'New' then 1 else 0 end) > 0 and
sum(case when project_name = 'B' then 1 else 0 end) = 0;
If you need the complete rows, then Christian has a reasonable solution.

SQL Running Subtraction

I have a result set as below:
Item ExpectedQty ReceivedQty Short
Item01 30 45 5
Item01 20 45 5
Item02 40 38 2
item03 50 90 10
item03 30 90 10
item03 20 90 10
query is:
select a.Item, a.ExpectedQty,b.ReceivedQty, b.Short
from a join b on a.Item = b.Item
I need to get result as in second chart. Basically I have a total of received quantity in each line and I need to show received quantity against Expected Quantity, if there is any shortage I need to show in last line.
Expected:
Item ExpectedQty ReceivedQty Short
item01 30 30 0
item01 20 15 5
item02 40 38 2
item03 50 50 0
item03 30 30 0
item03 20 10 10
Thanks in advance.
Edited,
Vession 02 ;
-- Just a brief of business scenario is table has been created for a good receipt.
-- So here we have good expected line with PurchaseOrder(PO) in first few line.
-- And then we receive each expected line physically and that time these
-- quantity may be different
-- due to business case like quantity may damage and short quantity like that.
-- So we maintain a status for that eg: OK, Damage, also we have to calculate
-- short quantity
-- based on total of expected quantity of each item and total of received line.
if object_id('DEV..Temp','U') is not null
drop table Temp
CREATE TABLE Temp
(
ID INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
Item VARCHAR(32),
PO VARCHAR(32) NULL,
ExpectedQty INT NULL,
ReceivedQty INT NULL,
[STATUS] VARCHAR(32) NULL,
BoxName VARCHAR(32) NULL
)
-- Please see first few line with PO data will be the expected lines,
-- and then rest line will be received line
INSERT INTO TEMP (Item,PO,ExpectedQty,ReceivedQty,[STATUS],BoxName)
SELECT 'ITEM01','PO-01','30',NULL,NULL,NULL UNION ALL
SELECT 'ITEM01','PO-02','20',NULL,NULL,NULL UNION ALL
SELECT 'ITEM02','PO-01','40',NULL,NULL,NULL UNION ALL
SELECT 'ITEM03','PO-01','50',NULL,NULL,NULL UNION ALL
SELECT 'ITEM03','PO-02','30',NULL,NULL,NULL UNION ALL
SELECT 'ITEM03','PO-03','20',NULL,NULL,NULL UNION ALL
SELECT 'ITEM04','PO-01','30',NULL,NULL,NULL UNION ALL
SELECT 'ITEM01',NULL,NULL,'20','OK','box01' UNION ALL
SELECT 'ITEM01',NULL,NULL,'25','OK','box02' UNION ALL
SELECT 'ITEM01',NULL,NULL,'5','DAMAGE','box03' UNION ALL
SELECT 'ITEM02',NULL,NULL,'38','OK','box04' UNION ALL
SELECT 'ITEM02',NULL,NULL,'2','DAMAGE','box05' UNION ALL
SELECT 'ITEM03',NULL,NULL,'30','OK','box06' UNION ALL
SELECT 'ITEM03',NULL,NULL,'30','OK','box07' UNION ALL
SELECT 'ITEM03',NULL,NULL,'30','OK','box08' UNION ALL
SELECT 'ITEM03',NULL,NULL,'10','DAMAGE','box09' UNION ALL
SELECT 'ITEM04',NULL,NULL,'25','OK','box10'
-- Below Table is my expected result based on above data.
-- I need to show those data following way.
-- So I appreciate if you can give me an appropriate query for it.
-- Note: first row is blank and it is actually my table header. :)
SELECT ''as'ITEM', ''as'PO#', ''as'ExpectedQty',''as'ReceivedQty',
''as'DamageQty' ,''as'ShortQty' UNION ALL
SELECT 'ITEM01','PO-01','30','30','0' ,'0' UNION ALL
SELECT 'ITEM01','PO-02','20','15','5' ,'0' UNION ALL
SELECT 'ITEM02','PO-01','40','38','2' ,'0' UNION ALL
SELECT 'ITEM03','PO-01','50','50','0' ,'0' UNION ALL
SELECT 'ITEM03','PO-02','30','30','0' ,'0' UNION ALL
SELECT 'ITEM03','PO-03','20','10','10','0' UNION ALL
SELECT 'ITEM04','PO-01','30','25','0' ,'5'
One part of the problem is to get the running totals of expected item qunatities. For that you'd need a way to distinguish rows with same items from each other and a rule for the order of discharging same item quantities.
For the purpose of my attempt at solving your problem I'm going to assume there's a timestamp column whose values provide the order of discharge and are unique within same item groups.
Here's the sample data definition I was testing my solution on:
CREATE TABLE TableA (Item varchar(50), ExpectedQty int, Timestamp int);
INSERT INTO TableA
SELECT 'Item01', 30, 1 UNION ALL
SELECT 'Item01', 20, 2 UNION ALL
SELECT 'Item02', 40, 1 UNION ALL
SELECT 'item03', 50, 1 UNION ALL
SELECT 'item03', 30, 2 UNION ALL
SELECT 'item03', 20, 3;
CREATE TABLE TableB (Item varchar(50), ReceivedQty int);
INSERT INTO TableB
SELECT 'Item01', 45 UNION ALL
SELECT 'Item02', 38 UNION ALL
SELECT 'item03', 90;
And here's my solution:
SELECT
Item,
ExpectedQty,
ReceivedQty = CASE
WHEN RemainderQty >= 0 THEN ExpectedQty
WHEN RemainderQty < -ExpectedQty THEN 0
ELSE RemainderQty + ExpectedQty
END,
Short = CASE
WHEN RemainderQty >= 0 THEN 0
WHEN RemainderQty < -ExpectedQty THEN ExpectedQty
ELSE ABS(RemainderQty)
END
FROM (
SELECT
a.Item,
a.ExpectedQty,
RemainderQty = b.ReceivedQty - a.RunningTotalQty
FROM (
SELECT
a.Item,
a.Timestamp,
a.ExpectedQty,
RunningTotalQty = SUM(a2.ExpectedQty)
FROM TableA a
INNER JOIN TableA a AS a2 ON a.Item = a2.Item AND a.Timestamp >= a2.Timestamp
GROUP BY
a.Item,
a.Timestamp,
a.ExpectedQty
) a
INNER JOIN TableB b ON a.Item = b.Item
) s
select a.Item, a.ExpectedQty,b.ReceivedQty, (a.ExpectedQty - b.ReceivedQty) as 'Short' from a join b on a.Item = b.Item
SELECT a.ExpectedQty,
b.ReceivedQty,
CASE WHEN b.ReceivedQty < a.ExpectedQty
THEN b.ReceivedQty - a.ExpectedQty
ELSE 0
END Short
FROM dbo.a a
INNER JOIN dbo.b b
ON a.ItemId = b.ItemId