Stuck on this union / except - sql

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;

Related

Showing NULL on purpose when a NULL joined value is present in SQL

I have a table with some input values and a table with lookup values like below:
select input.value, coalesce(mapping.value, input.value) result from (
select 'a' union all select 'c'
) input (value) left join (
select 'a', 'z' union all select 'b', 'y'
) mapping (lookupkey, value) on input.value = mapping.lookupkey
which gives:
value | result
--------------
a | z
c | c
i.e. I want to show the original values as well as the mapped value but if there is none then show the original value as the result.
The above works well so far with coalesce to determine if there is a mapped value or not. But now if I allow NULL as a valid mapped value, I want to see NULL as the result and not the original value, since it does find the mapped value, only that the mapped value is NULL. The same code above failed to achieve this:
select input.value, coalesce(mapping.value, input.value) result from (
select 'a' union all select 'c'
) input (value) left join (
select 'a', 'z' union all select 'b', 'y' union all select 'c', null
) mapping (lookupkey, value) on input.value = mapping.lookupkey
which gives the same output as above, but what I want is:
value | result
--------------
a | z
c | NULL
Is there an alternative to coalesce that can achieve what I want?
I think you just want a case expression e.g.
select input.[value]
, coalesce(mapping.[value], input.[value]) result
, case when mapping.lookupkey is not null then mapping.[value] else input.[value] end new_result
from (
select 'a'
union all
select 'c'
) input ([value])
left join (
select 'a', 'z'
union all
select 'b', 'y'
union all
select 'c', null
) mapping (lookupkey, [value]) on input.[value] = mapping.lookupkey
Returns:
value result new_result
a z z
c c NULL

Excluding records from table based on rules from another table

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.

SQL Beginner question: CASE AS END for multiple columns?

I am making a stored procedure which creates a target data table (#tmp_target_table), does some checking on it, and outputs the results in a resultset table (#tmp_resultset_table). The resultset table needs to have multiple new columns: user_warning_id, user_warning_note, and user_warning_detail in addition to existing columns from #tmp_target_table.
I have a working stored procedure as in the following but this has some issue. I need to write conditionA, conditionB, and conditionB repeatedly but these conditions will need to be changed in the future. How would you write a code that is more extensible?
<Working code>
SELECT existing_col1, existing_col2,
CASE
WHEN conditionA
THEN user_warning_id_A
WHEN conditionB
THEN user_warning_id_B
WHEN conditionC
THEN user_warning_id_C
END AS user_warning_id,
CASE
WHEN conditionA
THEN user_warning_note_A
WHEN conditionB
THEN user_warning_note_B
WHEN conditionC
THEN user_warning_note_C
END AS user_warning_note,
CASE
WHEN conditionA
THEN user_warning_detail_A
WHEN conditionB
THEN user_warning_detail_B
WHEN conditionC
THEN user_warning_detail_C
END AS user_warning_detail,
existing_col3, existing_col4
INTO #tmp_resultset_table
FROM #tmp_target_table
SELECT * FROM #tmp_resultant_table
In SQL Server, you can use a lateral join (i.e., apply) to arrange the data so you can use a reference table:
select tt.*,
v2.user_warning_id, v2.user_warning_note, v2.user_warning_detail
from #tmp_target_table tt cross apply
(values (case when conditionA then 'a'
when conditionA then 'b'
when conditionA then 'c'
end)
) v(cond) left join
(values ('a', user_warning_id_A, user_warning_note_A, user_warning_detail_A),
('b', user_warning_id_B, user_warning_note_B, user_warning_detail_B),
('c', user_warning_id_C, user_warning_note_C, user_warning_detail_C)
) v2(cond, user_warning, user_warning_note, user_warning_detail)
on v2.cond = v.cond;
This also makes it pretty easy to add more levels, if you like.
Note: You could combine v and v2 into a single values list. I separated them, because you might want to consider making v2 an actual reference table.
EDIT:
DB2 supports lateral joins with the lateral keyword. I don't remember if DB2 supports values(). So try this:
select tt.*,
v2.user_warning_id, v2.user_warning_note, v2.user_warning_detail
from #tmp_target_table tt cross join lateral
(select (case when conditionA then 'a'
when conditionA then 'b'
when conditionA then 'c'
end)
from sysibm.sysdummy1
) v(cond) left join
(select 'a' as cond, user_warning_id_A as user_warning_id, user_warning_note_A as user_warning_note, user_warning_detail_A user_warning_detail
from sysibm.sysdummy1
union all
select 'b', user_warning_id_B, user_warning_note_B, user_warning_detail_B
from sysibm.sysdummy1
union all
select 'c', user_warning_id_C, user_warning_note_C, user_warning_detail_C
from sysibm.sysdummy1
) v2(cond, user_warning, user_warning_note, user_warning_detail)
on v2.cond = v.cond;
You could put the messages into a table and the condition logic into a function.
Just using temp tables so you can test it out.
Warnings
select warningID = 1, note = 'note 1', detail = 'notes on warning 1'
into #warning
union
select warningID = 2, note = 'note 2', detail = 'notes on warning 2'
union
select warningID = 3, note = 'note 3', detail = 'notes on warning 3'
union
select warningID = 4, note = 'note 4', detail = 'notes on warning 4'
Data values that have to meet conditions coming from some table ... #conditions
select condID = 1, val1 = 10, val2 = 1
into #conditions
union
select condID = 2, val1 = 20, val2 = 1
union
select condID = 3, val1 = 5, val2 = 2
union
select condID = 4, val1 = 30, val2 = 1
union
select condID = 4, val1 = 12, val2 = 1
Then a function that determines warnings based on conditions in the data. Takes values as input and returns a warningID
create function testWarningF
(
#val1In int
)
returns int
as
begin
declare #retVal int
select #retVal = case when #val1In <= 10 then 1
when #val1In > 10 and #val1In <=20 then 2
else 3
end
return #retVal
end
go
Then, the SQL ...
select *
from #conditions c
inner join #warning w on w.warningID = dbo.warningF(val1)
... returns this result
condID val1 val2 warningID note detail
1 10 1 1 note 1 notes on warning 1
2 20 1 2 note 2 notes on warning 2
3 5 2 1 note 1 notes on warning 1
4 12 1 2 note 2 notes on warning 2
4 30 1 3 note 3 notes on warning 3
Possibly the simplest method it to move the Conditions into a sub-select, then reference a token in the other select. E.g.
SELECT existing_col1
, existing_col2
, CASE CON
WHEN 'A' THEN user_warning_id_A
WHEN 'B' THEN user_warning_id_B
WHEN 'C' THEN user_warning_id_C END AS user_warning_id
, CASE CON
WHEN 'A' THEN user_warning_note_A
WHEN 'B' THEN user_warning_note_B
WHEN 'C' THEN user_warning_note_C END AS user_warning_note
, CASE CON
WHEN 'A' THEN user_warning_detail_A
WHEN 'B' THEN user_warning_detail_B
WHEN 'C' THEN user_warning_detail_C END AS user_warning_detail
, existing_col3
, existing_col4
FROM (
SELECT T.*
, CASE WHEN conditionA THEN 'A'
WHEN conditionB THEN 'B'
WHEN conditionC THEN 'C' END AS CON
FROM
#tmp_target_table T
)
although Gordon's answer is also neat, even though it adds two joins in the access plan. In Db2 Syntax, this works (on Db2 11.1.3.3 anyway)
select tt.*,
v2.user_warning_id, v2.user_warning_note, v2.user_warning_detail
from #tmp_target_table tt
, (values (case when conditionA then 'a'
when conditionB then 'b'
when conditionC then 'c'
end)
) v(cond) left join
(values ('a', 'user_warning_id_A', 'user_warning_note_A', 'user_warning_detail_A'),
('b', 'user_warning_id_B', 'user_warning_note_B', 'user_warning_detail_B'),
('c', 'user_warning_id_C', 'user_warning_note_C', 'user_warning_detail_C')
) v2(cond, user_warning_id, user_warning_note, user_warning_detail)
on v2.cond = v.cond;
testing with
create table #tmp_target_table(i int);
insert into #tmp_target_table(values 1);
create variable conditionA boolean;
create variable conditionB boolean default true;
create variable conditionC boolean;
returns
I USER_WARNING_ID USER_WARNING_NOTE USER_WARNING_DETAIL
- ----------------- ------------------- ---------------------
1 user_warning_id_B user_warning_note_B user_warning_detail_B

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

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.

Pivoting , Dynamic columns in SQL Server

I have tables like these
LoanPrograms
Id Name
------------
1 LP1
2 LP2
3 LP3
Channels
Id Name
----------
4 Channel1
5 Channel2
6 Channel3
LoanProgramsChannels
LoanProgramId Channelid
----------------------
1 4
1 5
2 4
I wanted to get data like these
LoanProgarmNames channel1 channel2 channel3
---------------- -------- -------- --------
LP1 y y N
LP2 y N N
LP3 N N N
I am quite new to SQL, I know I have to use PIVOT to achieve these, but not sure how can I achieve in these scenario. Can anybody help on this ?
Because you need to "fill in the gaps" the best way is to create a cartesian product using a CROSS JOIN:
;WITH CTE AS (
SELECT A.NAME AS LOANPROGRAMNAME
, B.NAME AS CHANNELNAME
, CASE WHEN C.CHANNELID IS NULL THEN 'N' ELSE 'Y' END AS LOANPROGRAMCHANNELS
FROM LOANPROGRAMS AS A
CROSS JOIN CHANNELS AS B
LEFT JOIN LOANPROGRAMSCHANNELS AS C
ON CAST(A.ID AS VARCHAR)+CAST(B.ID AS VARCHAR) =
CAST(C.LOANPROGRAMID AS VARCHAR)+CAST(C.CHANNELID AS VARCHAR))
SELECT LOANPROGRAMNAME, [CHANNEL1], [CHANNEL2], [CHANNEL3]
FROM CTE
PIVOT(MAX(LOANPROGRAMCHANNELS) FOR CHANNELNAME IN ([CHANNEL1], [CHANNEL2], [CHANNEL3])) PIV
Once you've got the condition (Y/N) you can pivot the required columns, as done here.
There might be better ways to code this, but I believe the following will provide what you want:
declare #tbLoanPrograms table (
ID int, Name varchar(12)
)
insert into #tbLoanPrograms (
ID , Name
)
values (1, 'LP1')
, (2, 'LP2')
, (3, 'LP3')
declare #tbChannels table (
ID int, Name varchar(12)
)
insert into #tbChannels (
ID , Name
)
values (4, 'Channel1')
, (5, 'Channel2')
, (6, 'Channel3')
declare #tbLoanProgramsChannels table (
LoanProgramId int, Channelid int
)
insert into #tbLoanProgramsChannels (
LoanProgramId , Channelid
)
values (1, 4)
, (1, 5)
, (2, 4)
select
t.Name
, Channel1 = max(Channel1)
, Channel2 = max(Channel2)
, Channel3 = max(Channel3)
from (
select
lp.Name
, Channel1 =
case
when lpc.LoanProgramId is not null and lpc.Channelid = 4
then 'y'
else 'N'
end
, Channel2 =
case
when lpc.LoanProgramId is not null and lpc.Channelid = 5
then 'y'
else 'N'
end
, Channel3 =
case
when lpc.LoanProgramId is not null and lpc.Channelid = 6
then 'y'
else 'N'
end
from
#tbLoanPrograms lp
left join #tbLoanProgramsChannels lpc on lpc.LoanProgramId = lp.ID
left join #tbChannels c on c.ID = lpc.Channelid
) t
group by t.Name