Oracle join using two possible column data - sql

I work with an Oracle 12 database that represents mainframe data. Here is my question.
We have two levels of heirarchy, "System" and "Prin". Imagine them as state and county in the USA. Sometimes, a client will build everything at System level and all of it's children will always referece the System configuration. Other clients built at Prin level, and and child of the prin will first have to look at the PRIN level data for configuration, if prin is not built in the table, then it defaults to the system level config. Pretty easy.
Here's where i can't get the table join to work. A single client can have some systems built at the systems level, and others at the prin level. How can i dynamically join when i am not sure what configuration the client is using in that specific prin?
Example:
WITH tbl as (
select 80 SYSTEM, 0 PRIN, 2 DATA from dual
union
select 80 , 1 , 3 from dual
union
select 80 , 2 , 4 from dual
)
now if i have an item located in system 80 prin 3... it will need the 0 prin data because 0 denotes the "system" config.
so if i have prin 1, i want data "3". if i have prin 2, data "4" , if i have prin 8, i want data "2" because there is no prin 8 config built.
See where I am trying to get?
So when i do
select *
from tbl t
inner join tbl2 tt on t.sys = tt.sys and prin = ?????
how do I say "if prin is built in tbl, use prin, otherwise default to prin = 0"
I know this is a badly stated question. So please ask more specifics and i will try to answer quickly. This is affecting multiple tables.

Pretty ugly, but then so is the data model...
with
tbl ( s, prim, val ) as (
select 80, 0, 2 from dual union all
select 80, 1, 3 from dual union all
select 80, 2, 4 from dual
),
inputs ( s, prim ) as (
select 80, 1 from dual union all
select 80, 5 from dual
)
select t.s, i.prim i_prim, t.prim tbl_prim, t.val
from tbl t join inputs i
on t.s = i.s
and
( t.prim = i.prim
or t.prim = 0
and not exists ( select * from tbl
where s = i.s and prim = i.prim ))
;
S I_PRIM TBL_PRIM VAL
---- ---------- ---------- ----------
80 5 0 2
80 1 1 3
2 rows selected.

I would disadvise you from using a (complex) JOIN with OR-condition on slightly bigger tables (50k+), as the execution plan may totally go nuts from my own experience.
Under such circumstances rather use a Union (select cond1_match) union all (select cond2_default) ordered/ranked and select the first row or use a JOIN like
select coalesce(a1.prin, a2.prin)
from (select cond1_match) a1
full join (select cond2_default) a2
And if understand you right, that you have just one number as input and want to join another datatable, then my suggestion would look like this
with
tbl ( SYSTEM, PRIN, DATA ) as (
select 80, 0, 2 from dual union all
select 80, 1, 3 from dual union all
select 80, 2, 4 from dual
),
tbl2 (SYSTEM, PRIN, OTHERDATA) as (
select 80, 0, 99 from dual union all
select 80, 1, 333 from dual union all
select 80, 2, 444 from dual
)
select t.system, t.prin, t.data, tt.otherdata
from tbl t
inner join tbl2 tt on t.system = tt.system and t.prin = tt.prin
where t.prin = (select nvl(max(prin), 0) from tbl where system = t.system and prin = :pri)
;
system + prin have to be unique or max() would be random
:pri = 5
SYSTEM PRIN DATA OTHERDATA
------ ---- ---- ---------
80 0 2 99
:pri = 2
SYSTEM PRIN DATA OTHERDATA
------ ---- ---- ---------
80 2 4 444
Only guessing about tbl2 and the join condition, but that's basically how I was told to look up data or use a default if no_data_found in SQL

Related

Oracle SQL - Combining columns with 'OR' bit function

Oracle 12.2 - I have a table with 3 columns... ID, ParentID and ProductList. ID is unique, with multiple IDs rolling up to a ParentID. (this is a account model... basically multiple accounts have the same parent...) ProductList is a string...also exactly 20 bytes... right now it is 20 letters of 'Y' and 'N', such as YYNYNYNYNNNY... but I can change the 'Y' and 'N' to 1 and 0 if it will help... what I need to do is within a group of ParentID, calculate a bitwise OR of the ProductList. The end result I need is a 20 byte string (or some type of 20 bits of data) that says - for each respective letter/bit - if any 'Y' then return 'Y'. Again, I can use 1/0 if easier than Y/N.
Here is pseudoCode of what I am trying to do... Any help appreciated.
with T1 as
(
select 10 as ID, 20 as ParentID, 'YYNNYNYNYNYYNNYNYNYN' as ProductList from dual
union
select 11 as ID, 20 as ParentID, 'NNNNNNNNNNYYYYYYYYYY' as ProductList from dual
union
select 22 as ID, 20 as ParentID, 'YYNNNNNNNNNNNNNNNNNN' as ProductList from dual
)
SELECT ParentID, BitWiseOr(ProductList) FROM t1
group by ParentID;
You can use the brute force method of taking the maximum of each character and then using ||:
SELECT ParentID,
(max(substr(productlist, 1, 1)) ||
max(substr(productlist, 2, 1)) ||
max(substr(productlist, 3, 1)) ||
. . .
max(substr(productlist, 20, 1)) ||
)
FROM t1
GROUP BY ParentID;
This works because 'Y' > 'N'.
Note: This is a lousy data model. You should have a separate table with one row per id and product.
You can destruct string to atomic values, compute result of or operation and assemble it back into string. (Credit to #GordonLinoff for Y>N trick.) dbfiddle here.
Unfortunately, Oracle does not allow something like unpivot (val FOR substring(ProductList,i,1 in ... and also Oracle does not have equivalent to Postgres bool_or, which would both made solution simpler. At least this solution scales with ProductList length.
Anyway you should avoid violating 1st normal form. If you cannot, it IMHO does not matter how boolean is modelled.
with T1 as
(
select 10 as ID, 20 as ParentID, 'YYNNYNYNYNYYNNYNYNYN' as ProductList from dual
union
select 11 as ID, 20 as ParentID, 'NNNNNNNNNNYYYYYYYYYY' as ProductList from dual
union
select 22 as ID, 20 as ParentID, 'YYNNNNNNNNNNNNNNNNNN' as ProductList from dual
), series (i) as (
select level as i
from dual
connect by level <= 20
), applied_or as (
select t1.parentid
, max(substr(t1.productlist, series.i, 1)) as or_result
, series.i
from t1
cross join series
group by t1.parentid, series.i
)
select parentid
, listagg(or_result) within group (order by i)
from applied_or
group by parentid

How do I partially share param values across union selects using different constraints?

I have a view
create or replace view v_collected as
select car.id_car
,car.state_car
,[..]
from cars car
,garages garage
,[..]
where [..]
Which is at least unique for car.id
Now I want to union 3 different queries into a second view, which takes user set parameters:
select 0 as score
,p0.*
from v_collected p0
where 1 = 1
-- User IO Binding
and p0.car = 'Audi'
and p0.garage = 'P01'
and p0.state_car = 'Ok'
union
select 1 as score
,p1.*
from v_collected p1
where 1 = 1
-- Should access the identical binding
and p1.car <> p0.car
and p1.state_car = p0.state_car
union
select 2 as score
,p2.*
from v_collected p2
where 1 = 1
-- Should access the identical binding
and p2.state_car = p0.state_car
The above does not work as the 2nd query has no access to the the 1st one. As the constraints change, I believe I cannot use a CTE. What are my options?
Since the requirements are vague, I can't say for sure that the following solution is complete, however, I would look into breaking the p0, p1, p2 into with clause sub queries that way you can use p0 in p1 and p2. For example:
with cars as
(
select 2 id_car, 'Ford' car, 'Ok' state_car from dual union
select 1 id_car, 'Audi' car, 'Ok' state_car from dual
)
, garages as
(
select 2 id_car, 'P02' garage from dual union
select 1 id_car, 'P01' garage from dual
)
, v_collected as
(
select car.id_car
,car.car
,car.state_car
,garage.garage
from cars car
,garages garage
where 1=1
and car.id_car = garage.id_car
)
-- select * from v_collected;
, p0_subquery as
(
select 0 as score
,p0.*
from v_collected p0
where 1 = 1
-- User IO Binding
and p0.car = 'Audi'
and p0.garage = 'P01'
and p0.state_car = 'Ok'
)
--select * from p0_subquery;
, p1_subquery as
(
select 1 as score
,p1.*
from v_collected p1
, p0_subquery p0
where 1 = 1
-- Should access the identical binding
and p1.car <> p0.car
and p1.state_car = p0.state_car
)
, p2_subquery as
(
select 2 as score
,p2.*
from v_collected p2
, p0_subquery p0
where 1 = 1
-- Should access the identical binding
and p2.state_car = p0.state_car
)
select * from p0_subquery
union
select * from p1_subquery
union
select * from p2_subquery
;

SQL hierarchy count totals report

I'm creating a report with SQL server 2012 and Report Builder which must show the total number of Risks at a high, medium and low level for each Parent Element.
Each Element contains a number of Risks which are rated at a certain level. I need the total for the Parent Elements. The total will include the number of all the Child Elements and also the number the Element itself may have.
I am using CTEs in my query- the code I have attached isn't working (there are no errors - it's just displaying the incorrect results) and I'm not sure that my logic is correct??
Hopefully someone can help. Thanks in advance.
My table structure is:
ElementTable
ElementTableId(PK) ElementName ElementParentId
RiskTable
RiskId(PK) RiskName RiskRating ElementId(FK)
My query:
WITH cte_Hierarchy(ElementId, ElementName, Generation, ParentElementId)
AS (SELECT ElementId,
NAME,
0,
ParentElementId
FROM Extract.Element AS FirtGeneration
WHERE ParentElementId IS NULL
UNION ALL
SELECT NextGeneration.ElementId,
NextGeneration.NAME,
Parent.Generation + 1,
Parent.ElementId
FROM Extract.Element AS NextGeneration
INNER JOIN cte_Hierarchy AS Parent
ON NextGeneration.ParentElementId = Parent.ElementId),
CTE_HighRisk
AS (SELECT r.ElementId,
Count(r.RiskId) AS HighRisk
FROM Extract.Risk r
WHERE r.RiskRating = 'High'
GROUP BY r.ElementId),
CTE_LowRisk
AS (SELECT r.ElementId,
Count(r.RiskId) AS LowRisk
FROM Extract.Risk r
WHERE r.RiskRating = 'Low'
GROUP BY r.ElementId),
CTE_MedRisk
AS (SELECT r.ElementId,
Count(r.RiskId) AS MedRisk
FROM Extract.Risk r
WHERE r.RiskRating = 'Medium'
GROUP BY r.ElementId)
SELECT rd.ElementId,
rd.ElementName,
rd.ParentElementId,
Generation,
HighRisk,
MedRisk,
LowRisk
FROM cte_Hierarchy rd
LEFT OUTER JOIN CTE_HighRisk h
ON rd.ElementId = h.ElementId
LEFT OUTER JOIN CTE_MedRisk m
ON rd.ElementId = m.ElementId
LEFT OUTER JOIN CTE_LowRisk l
ON rd.ElementId = l.ElementId
WHERE Generation = 1
Edit:
Sample Data
ElementTableId(PK) -- ElementName -- ElementParentId
1 ------------------- Main --------------0
2 --------------------Element1-----------1
3 --------------------Element2 ----------1
4 --------------------SubElement1 -------2
RiskId(PK) RiskName RiskRating ElementId(FK)
a -------- Financial -- High ----- 2
b -------- HR --------- High ----- 3
c -------- Marketing -- Low ------- 2
d -------- Safety -----Medium ----- 4
Sample Output:
Element Name High Medium Low
Main ---------- 2 ---- 1 -------1
Here is your sample tables
SELECT * INTO #TABLE1
FROM
(
SELECT 1 ElementTableId, 'Main' ElementName ,0 ElementParentId
UNION ALL
SELECT 2,'Element1',1
UNION ALL
SELECT 3, 'Element2',1
UNION ALL
SELECT 4, 'SubElement1',2
)TAB
SELECT * INTO #TABLE2
FROM
(
SELECT 'a' RiskId, 'Fincancial' RiskName,'High' RiskRating ,2 ElementId
UNION ALL
SELECT 'b','HR','High',3
UNION ALL
SELECT 'c', 'Marketing','Low',2
UNION ALL
SELECT 'd', 'Safety','Medium',4
)TAB
We are finding the children of a parent, its count of High,Medium and Low and use cross join to show parent with all the combinations of its children's High,Medium and Low
UPDATE
The below variable can be used to access the records dynamically.
DECLARE #ElementTableId INT;
--SET #ElementTableId = 1
And use the above variable inside the query
;WITH CTE1 AS
(
SELECT *,0 [LEVEL] FROM #TABLE1 WHERE ElementTableId = #ElementTableId
UNION ALL
SELECT E.*,e2.[LEVEL]+1 FROM #TABLE1 e
INNER JOIN CTE1 e2 on e.ElementParentId = e2.ElementTableId
AND E.ElementTableId<>#ElementTableId
)
,CTE2 AS
(
SELECT E1.*,E2.*,COUNT(RiskRating) OVER(PARTITION BY RiskRating) CNT
from CTE1 E1
LEFT JOIN #TABLE2 E2 ON E1.ElementTableId=E2.ElementId
)
,CTE3 AS
(
SELECT DISTINCT T1.ElementName,C2.RiskRating,C2.CNT
FROM #TABLE1 T1
CROSS JOIN CTE2 C2
WHERE T1.ElementTableId = #ElementTableId
)
SELECT *
FROM CTE3
PIVOT(MIN(CNT)
FOR RiskRating IN ([High], [Medium],[Low])) AS PVTTable
SQL FIDDLE
RESULT
UPDATE 2
I am updating as per your new requirement
Here is sample table in which I have added extra data to test
SELECT * INTO #ElementTable
FROM
(
SELECT 1 ElementTableId, 'Main' ElementName ,0 ElementParentId
UNION ALL
SELECT 2,'Element1',1
UNION ALL
SELECT 3, 'Element2',1
UNION ALL
SELECT 4, 'SubElement1',2
UNION ALL
SELECT 5, 'Main 2',0
UNION ALL
SELECT 6, 'Element21',5
UNION ALL
SELECT 7, 'SubElement21',6
UNION ALL
SELECT 8, 'SubElement22',7
UNION ALL
SELECT 9, 'SubElement23',7
)TAB
SELECT * INTO #RiskTable
FROM
(
SELECT 'a' RiskId, 'Fincancial' RiskName,'High' RiskRating ,2 ElementId
UNION ALL
SELECT 'b','HR','High',3
UNION ALL
SELECT 'c', 'Marketing','Low',2
UNION ALL
SELECT 'd', 'Safety','Medium',4
UNION ALL
SELECT 'e' , 'Fincancial' ,'High' ,5
UNION ALL
SELECT 'f','HR','High',6
UNION ALL
SELECT 'g','HR','High',6
UNION ALL
SELECT 'h', 'Marketing','Low',7
UNION ALL
SELECT 'i', 'Safety','Medium',8
UNION ALL
SELECT 'j', 'Safety','High',8
)TAB
I have written the logic in query
;WITH CTE1 AS
(
-- Here you will find the level of every elements in the table
SELECT *,0 [LEVEL]
FROM #ElementTable WHERE ElementParentId = 0
UNION ALL
SELECT ET.*,CTE1.[LEVEL]+1
FROM #ElementTable ET
INNER JOIN CTE1 on ET.ElementParentId = CTE1.ElementTableId
)
,CTE2 AS
(
-- Filters the level and find the major parant of each child
-- ie, 100->150->200, here the main parent of 200 is 100
SELECT *,CTE1.ElementTableId MajorParentID,CTE1.ElementName MajorParentName
FROM CTE1 WHERE [LEVEL]=1
UNION ALL
SELECT CTE1.*,CTE2.MajorParentID,CTE2.MajorParentName
FROM CTE1
INNER JOIN CTE2 on CTE1.ElementParentId = CTE2.ElementTableId
)
,CTE3 AS
(
-- Since each child have columns for main parent id and name,
-- you will get the count of each element corresponding to the level you have selected directly
SELECT DISTINCT CTE2.MajorParentName,RT.RiskRating ,
COUNT(RiskRating) OVER(PARTITION BY MajorParentID,RiskRating) CNT
FROM CTE2
JOIN #RiskTable RT ON CTE2.ElementTableId=RT.ElementId
)
SELECT MajorParentName, ISNULL([High],0)[High], ISNULL([Medium],0)[Medium],ISNULL([Low],0)[Low]
FROM CTE3
PIVOT(MIN(CNT)
FOR RiskRating IN ([High], [Medium],[Low])) AS PVTTable
SQL FIDDLE

Keep order from 'IN' clause

Is it possible to keep order from a 'IN' conditional clause?
I found this question on SO but in his example the OP have already a sorted 'IN' clause.
My case is different, 'IN' clause is in random order
Something like this :
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField IN (45,2,445,12,789)
I would like to retrieve results in (45,2,445,12,789) order. I'm using an Oracle database. Maybe there is an attribute in SQL I can use with the conditional clause to specify to keep order of the clause.
There will be no reliable ordering unless you use an ORDER BY clause ..
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField IN (45,2,445,12,789)
order by case TestResult.SomeField
when 45 then 1
when 2 then 2
when 445 then 3
...
end
You could split the query into 5 queries union all'd together though ...
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField = 4
union all
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField = 2
union all
...
I'd trust the former method more, and it would probably perform much better.
Decode function comes handy in this case instead of case expressions:
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField IN (45,2,445,12,789)
ORDER BY DECODE(SomeField, 45,1, 2,2, 445,3, 12,4, 789,5)
Note that value,position pairs (e.g. 445,3) are kept together for readability reasons.
Try this:
SELECT T.SomeField,T.OtherField
FROM TestResult T
JOIN
(
SELECT 1 as Id, 45 as Val FROM dual UNION ALL
SELECT 2, 2 FROM dual UNION ALL
SELECT 3, 445 FROM dual UNION ALL
SELECT 4, 12 FROM dual UNION ALL
SELECT 5, 789 FROM dual
) I
ON T.SomeField = I.Val
ORDER BY I.Id
There is an alternative that uses string functions:
with const as (select ',45,2,445,12,789,' as vals)
select tr.*
from TestResult tr cross join const
where instr(const.vals, ','||cast(tr.somefield as varchar(255))||',') > 0
order by instr(const.vals, ','||cast(tr.somefield as varchar(255))||',')
I offer this because you might find it easier to maintain a string of values rather than an intermediate table.
I was able to do this in my application using (using SQL Server 2016)
select ItemID, iName
from Items
where ItemID in (13,11,12,1)
order by CHARINDEX(' ' + Convert("varchar",ItemID) + ' ',' 13 , 11 , 12 , 1 ')
I used a code-side regex to replace \b (word boundary) with a space. Something like...
var mylist = "13,11,12,1";
var spacedlist = replace(mylist,/\b/," ");
Importantly, because I can in my scenario, I cache the result until the next time the related items are updated, so that the query is only run at item creation/modification, rather than with each item viewing, helping to minimize any performance hit.
Pass the values in via a collection (SYS.ODCINUMBERLIST is an example of a built-in collection) and then order the rows by the collection's order:
SELECT t.SomeField,
t.OtherField
FROM TestResult t
INNER JOIN (
SELECT ROWNUM AS rn,
COLUMN_VALUE AS value
FROM TABLE(SYS.ODCINUMBERLIST(45,2,445,12,789))
) i
ON t.somefield = i.value
ORDER BY rn
Then, for the sample data:
CREATE TABLE TestResult ( somefield, otherfield ) AS
SELECT 2, 'A' FROM DUAL UNION ALL
SELECT 5, 'B' FROM DUAL UNION ALL
SELECT 12, 'C' FROM DUAL UNION ALL
SELECT 37, 'D' FROM DUAL UNION ALL
SELECT 45, 'E' FROM DUAL UNION ALL
SELECT 100, 'F' FROM DUAL UNION ALL
SELECT 445, 'G' FROM DUAL UNION ALL
SELECT 789, 'H' FROM DUAL UNION ALL
SELECT 999, 'I' FROM DUAL;
The output is:
SOMEFIELD
OTHERFIELD
45
E
2
A
445
G
12
C
789
H
fiddle

SELECT DISTINCT for data groups

I have following table:
ID Data
1 A
2 A
2 B
3 A
3 B
4 C
5 D
6 A
6 B
etc. In other words, I have groups of data per ID. You will notice that the data group (A, B) occurs multiple times. I want a query that can identify the distinct data groups and number them, such as:
DataID Data
101 A
102 A
102 B
103 C
104 D
So DataID 102 would resemble data (A,B), DataID 103 would resemble data (C), etc. In order to be able to rewrite my original table in this form:
ID DataID
1 101
2 102
3 102
4 103
5 104
6 102
How can I do that?
PS. Code to generate the first table:
CREATE TABLE #t1 (id INT, data VARCHAR(10))
INSERT INTO #t1
SELECT 1, 'A'
UNION ALL SELECT 2, 'A'
UNION ALL SELECT 2, 'B'
UNION ALL SELECT 3, 'A'
UNION ALL SELECT 3, 'B'
UNION ALL SELECT 4, 'C'
UNION ALL SELECT 5, 'D'
UNION ALL SELECT 6, 'A'
UNION ALL SELECT 6, 'B'
In my opinion You have to create a custom aggregate that concatenates data (in case of strings CLR approach is recommended for perf reasons).
Then I would group by ID and select distinct from the grouping, adding a row_number()function or add a dense_rank() your choice. Anyway it should look like this
with groupings as (
select concat(data) groups
from Table1
group by ID
)
select groups, rownumber() over () from groupings
The following query using CASE will give you the result shown below.
From there on, getting the distinct datagroups and proceeding further should not really be a problem.
SELECT
id,
MAX(CASE data WHEN 'A' THEN data ELSE '' END) +
MAX(CASE data WHEN 'B' THEN data ELSE '' END) +
MAX(CASE data WHEN 'C' THEN data ELSE '' END) +
MAX(CASE data WHEN 'D' THEN data ELSE '' END) AS DataGroups
FROM t1
GROUP BY id
ID DataGroups
1 A
2 AB
3 AB
4 C
5 D
6 AB
However, this kind of logic will only work in case you the "Data" values are both fixed and known before hand.
In your case, you do say that is the case. However, considering that you also say that they are 1000 of them, this will be frankly, a ridiculous looking query for sure :-)
LuckyLuke's suggestion above would, frankly, be the more generic way and probably saner way to go about implementing the solution though in your case.
From your sample data (having added the missing 2,'A' tuple, the following gives the renumbered (and uniqueified) data:
with NonDups as (
select t1.id
from #t1 t1 left join #t1 t2
on t1.id > t2.id and t1.data = t2.data
group by t1.id
having COUNT(t1.data) > COUNT(t2.data)
), DataAddedBack as (
select ID,data
from #t1 where id in (select id from NonDups)
), Renumbered as (
select DENSE_RANK() OVER (ORDER BY id) as ID,Data from DataAddedBack
)
select * from Renumbered
Giving:
1 A
2 A
2 B
3 C
4 D
I think then, it's a matter of relational division to match up rows from this output with the rows in the original table.
Just to share my own dirty solution that I'm using for the moment:
SELECT DISTINCT t1.id, D.data
FROM #t1 t1
CROSS APPLY (
SELECT CAST(Data AS VARCHAR) + ','
FROM #t1 t2
WHERE t2.id = t1.id
ORDER BY Data ASC
FOR XML PATH('') )
D ( Data )
And then going analog to LuckyLuke's solution.