Getting parent data if child data is null in Oracle hierarchical table - sql

In Oracle 10g I have the following hierarchical table:
corp_id, parent_corp_id, col1, col2, col3
I want to flatten out the structure such that we get the first row's data where col1 OR col2 OR col3 is not null.
So for example, suppose I have:
corp_id = 1
parent_corp_id = null
col1 = 5
col2 = NULL
col3 = NULL
corp_id = 3
parent_corp_id = 1
col1 = NULL
col2 = NULL
col3 = NULL
the results of this query would give me:
corp_id, parent_corp_id, col1, col2, col3
3 , 1 , 5 , NULL, NULL
Another scenario:
Suppose I put col2 = 6 where corp_id = 3
Well, then the result set should be:
corp_id, parent_corp_id, col1, col2, col3
3 , 1 , NULL, 6, NULL
In other words, if the child has data in one of these three columns we grab it. Otherwise, we try the parent and so on and so forth. Shouldn't be more than 3 levels deep but it could have 3 levels to look into.
Pretty new to hierarchical queries, so pardon me if this is a rudimentary question.

Use the coalesce() function, which returns the first non-null value in its list:
select
c.corp_id,
c.parent_corp_id,
coalesce(c.col1, p.col1) col1,
coalesce(c.col2, p.col2) col2,
coalesce(c.col3, p.col3) col3
from mytable c
left join mytable p on p.corp_id = c.parent_corp_id
to get the "first row that has a not-null value", add:
where coalesce(c.col1, p.col1, c.col2, p.col2, c.col3, p.col3) is not null
and rownum = 1

You do need to use a hiearchial query (w/ the connect by clause) because of the fact that you have a parent with a child and that child is the parent of another child (although your example data doesn't bring that into play) however the requirement that you show the 'first not null col1' and the 'first not null col2' etc. is a separate issue from the hierarchical relationship altogether.
Try the following, I added some additional sample data to the fiddle (check the DDL on the left side) for illustrative purposes.
It looks like in your expected output you don't want to show the highest level parents, which is why I put "where s.parent_corp_id is not null" at the end. If you actually do want to show those, take that line out.
Otherwise, this will show the col1/col2/col3 values based on their group. Notice how in the example 2 is a high level parent and has 4 as a child, and 4 is also a parent of 8. So corp_id 8 and 4 are part of the same branch and they therefore show the same col1/col2/col3 values, and those are, based on your requirements, the first not null value of each throughout the branch.
Fiddle: http://sqlfiddle.com/#!4/ef218/14/0
with sub as
(select corp_id,
parent_corp_id,
col1,
col2,
col3,
level as lvl,
rownum - level as grp
from tbl
connect by prior corp_id = parent_corp_id
start with parent_corp_id is null),
col1_lvl as
(select grp, col1
from sub s
where s.lvl = (select min(x.lvl)
from sub x
where x.col1 is not null
and x.grp = s.grp)),
col2_lvl as
(select grp, col2
from sub s
where s.lvl = (select min(x.lvl)
from sub x
where x.col2 is not null
and x.grp = s.grp)),
col3_lvl as
(select grp, col3
from sub s
where s.lvl = (select min(x.lvl)
from sub x
where x.col3 is not null
and x.grp = s.grp))
select s.corp_id, s.parent_corp_id, c1.col1, c2.col2, c3.col3
from sub s
left join col1_lvl c1
on s.grp = c1.grp
left join col2_lvl c2
on s.grp = c2.grp
left join col3_lvl c3
on s.grp = c3.grp
where s.parent_corp_id is not null
If this doesn't provide the output you're expecting based on the sample data I used please provide the expected output for the data I used in the DDL on the fiddle.

Related

SQL Server Weird Grouping Scenario by multiple columns and OR

I have a weird grouping scenario and have some troubles finding out what would be the best way for grouping in SQL.
Imagine we have the following one table
CREATE TABLE Item
(
KeyId VARCHAR(1) NOT NULL,
Col1 INT NULL,
Col2 INT NULL,
Col3 INT NULL
)
GO
INSERT INTO Item (KeyId, Col1, Col2, Col3)
VALUES
('a',1,2,3),
('b',5,4,3),
('c',5,7,6),
('d',8,7,9),
('e',11,10,9),
('f',11,12,13),
('g',20,22,21),
('h',23,22,24)
I need to group records in this table so that if Col1 OR Col2 OR Col3 is the same for two records, then these two records should be in the same group, and there should be chaining.
In other words, with the data as above record 'a' (first record) has Col3 = 3 and record 'b' (second record) has also Col3 = 3, so these two should be in one group. But then record 'b' has the same Col1 as record 'c', so record 'c' should be in the same group as 'a' and 'b'. And then record 'd' has the same Col2 as in 'c', so this should also be in the same group. Similarly 'e' and 'f' has the same values in Col3 and Col1 respectively.
On the other hand records 'g' and 'h' will be in one group (because they have the same Col2 = 22), but this group will be different from the group for records 'a','b','c','d','e','f'.
The result of the query should be something like
KeyId GroupId
'a' 1
'b' 1
'c' 1
'd' 1
'e' 1
'f' 1
'g' 2
'h' 2
There is probably a way of doing this with some loops/cursors, but I started thinking about cleaner way and this seems quite difficult.
Here you go:
with g (rootid, previd, level, keyid, col1, col2, col3) as (
select keyid, '-', 1, keyid, col1, col2, col3 from item
union all
select g.rootid, g.keyid, g.level + 1, i.keyid, i.col1, i.col2, i.col3
from g
join item i on i.col1 = g.col1 or i.col2 = g.col2 or i.col3 = g.col3
where i.keyid > g.keyid
),
m (keyid, rootid) as (
select keyid, min(rootid) from g group by keyid
)
select * from m;
Result:
keyid rootid
----- ------
a a
b a
c a
d a
e a
f a
g g
h g
Note: Keep in mind that SQL Server has by default a limit of 100 iterations (number of rows per group) when processing recursive CTEs. In English: even though it's possible to do this as shown above, there are clear limitations to what SQL Server can process. If you reach this limit you'll get the message:
The maximum recursion 100 has been exhausted before statement completion.
If this happens consider adding the clause option (maxrecursion 32767).

Checking to see if multiple conditions are true within a group in SQL

Lets say I have the following table depicting a one-many relationship
col1 | col2
-------------
1 | foo
1 | bar
2 | foo
3 | buzz
I need to group by col1 and I need a boolean indicating whether or not there is both a mapping to 'foo' and a mapping to 'bar'.
So, the final result set would be
col1 | foobar
-------------
1 | 1
2 | 0
3 | 0
What is the best way to achieve this in T-SQL?
I've been trying something roughly equivalent to the following query with no luck.
SELECT
col1
, (
MAX (
CASE WHEN
COL2 = 'foo'
THEN 1 ELSE 0
END) = 1
AND
MAX (
CASE WHEN
COL2 = 'bar'
THEN 1 ELSE 0
END) = 1
)
FROM
table
GROUP BY
col1
EDIT:
To clarify, this table is a simplification.
I am looking for a solution to the general problem of having a one-many mapping and needing to produce a new 1-1 mapping with a Boolean indicating if a variable number of predicates are true of the different elements in the groups in the co-domain. (grouped by the fact that the same element maps to them)
Also, I should clarify that these various predicates could be anything.
For example, maybe I want to see if at least one of the columns in one of the rows = 'foo' and also that a different column in a (possibly different) row within the same group is between a certain set of numeric values.
What about this?
EDIT: Better use COUNT(DISTINCT col2)
DECLARE #tbl TABLE(col1 INT,col2 VARCHAR(100));
INSERT INTO #tbl VALUES
(1,'foo')
,(1,'bar')
,(2,'foo')
,(3,'buzz');
SELECT col1
,COUNT(DISTINCT col2)-1
FROM #tbl
GROUP BY col1
UPDATE:
If you try it like this, you would even see, which values are there. If you are only interested in "one or many" you might check for a comma in the returned string:
DECLARE #tbl TABLE(col1 INT,col2 VARCHAR(100));
INSERT INTO #tbl VALUES
(1,'foo')
,(1,'bar')
,(2,'foo')
,(3,'buzz');
SELECT outerTbl.col1
,STUFF
(
(
SELECT DISTINCT ', ' + col2
FROM #tbl AS innerTbl
WHERE innerTbl.col1=outerTbl.col1
FOR XML PATH('')
),1,2,''
)
FROM #tbl AS outerTbl
GROUP BY outerTbl.col1
This is the result:
1 bar, foo
2 foo
3 buzz
Here is a simple option
select col1
,MAX(case when col2='foo' then 1 else 0 end)*MAX(case when col2='bar' then 1 else 0 end) foobar
from #tbl
group by col1
Try this:
select
col1,
case when foo > 0 and bar > 0 then true else false end foobar
from (
select
col1,
sum(case when col2 = 'foo' then 1 end) foo,
sum(case when col2 = 'bar' then 1 end) bar
from table
group by col1) x

Is there a SQL Statement that allows me to copy and insert existing rows but with one column change?

You might not understood what I want to ask from the title but ,here is the explanation.
I have a data in Oracle database table. What I wanted to do is insert a new data to the table. This new data is based on the existing data but I have to change the value of one columns. So if I have 10 rows in the database after the insertion i will have 20 rows but the new 10 rows contain the same data except on of the columns is changed.
E.g table before insertion a new data
Col1 Col2 Col3
a b AA
1 2 33
table after insertion a new data
Col1 Col2 Col3
a b **BB**
1 2 **44**
Provided that you can encode what the new value should be; yes.
INSERT INTO
myTable (
Col1,
Col2,
Col3
)
SELECT
Col1,
Col2, -- This is a specific example based on your comment.
Col3 + 6 -- This just adds 6 to the existing value, but any SQL
FROM -- could actually go here, such as a CASE statement...
myTable
So, the question becomes; Do you have rules that you can implement in SQL for calculating the new value for Col3?
The rules could be something basic like...
CASE WHEN Col3 = 'AA' THEN '**BB**'
WHEN Col3 = '33' THEN '**44**'
ELSE 'Unknown'
END,
Or you could have all the new values in another table and look them up using a join...
INSERT INTO
myTable (
Col1,
Col2,
Col3
)
SELECT
OldTable.Col1,
OldTable.Col2,
COALESCE(NewTable.Col3, 'Unknown')
FROM
myTable AS OldTable
LEFT JOIN
lookup AS NewTable
ON OldTable.Col1 = NewTable.Col1
AND OldTable.Col2 = NewTable.Col2
Or a whole bunch of other options.
It will depend on how you determine how to change the data. How do you know, for example, that AA should become BB or that 33 should become 44?
Something like this will work for the two cases you posted. You can adapt it to whatever rule you want by changing the CASE statement to compute the new value differently.
INSERT INTO table_name( col1, col2, col3 )
SELECT col1,
col2,
(CASE WHEN col3 = 'AA'
THEN 'BB'
WHEN col3 = '33'
THEN '44'
ELSE null
END)
FROM table_name;

how to select the records whose several fields' combination will equal to a specific value

Assume I have the following style table, col1 col2 and col3 have same value scopes, I want to select the records when two of the 3 columns have a value combination such as ('ab' and 'bc'), in the following example, the first 3 records should be selected. Any good way to do this? I am using Sybase.
| id | col1 | col2 | col3 |
1 ab bc null
2 null ab bc
3 ab ab bc
4 de ab xy
Thanks.
I don't have Sybase to check, but you can Try this:
select * from Table where (col1 = "ab" or col2 = "ab" or Col3 = "ab")
and (col1 = "bc" or col2 = "bc" or Col3 = "bc")
I agree that the answer given here is totally acceptable, however I feel that if there were more than a few columns that needed to be evaluated, nesting the comparisons in the WHERE clause could become a bit cumbersome and illegible. I had to overcome a similar problem and I found that the following technique was quite helpful (I have adapted it to solve the issue listed above). Note that the evaluation of Total in the outer query can be easily adapted to increase or decrease the number of columns that contain the criteria being evaluated:
SELECT *
FROM
(SELECT id
, col1
, col2
, col3
, SUM(CASE WHEN UPPER(col1) IN ('AB', 'BC') THEN 1 ELSE 0 END
+ CASE WHEN UPPER(col2) IN ('AB', 'BC') THEN 1 ELSE 0 END
+ CASE WHEN UPPER(col3) IN ('AB', 'BC') THEN 1 ELSE 0 END) as Total
FROM <table>
GROUP BY id
, col1
, col2
, col3) as results
WHERE Total >= 2
ORDER BY id

How to choose a columns of a select statement based on conditions?

I have to create an SQL Server 2005 query which checks for the value of one attribute in the table and based on its value, select different sets of columns.
How can I do that?
for e.g.
In table 'car', if the values of 'type' attribute are 1 and 2
when type = 1, i want to execute a select 'query1' with 3 columns.
when type = 2, i want to execute another select 'query2' with 4 other columns.
How do I do that?
Please help.
I think you're looking at a Stored Procedure with an If statement. CASE will work, but it can't change the number of columns returned.
SELECT
Col1 = CASE WHEN Type = 1 THEN (SELECT Null FROM T1)
ELSE (SELECT Col1 FROM T2) END
, Col2 = CASE WHEN Type = 1 THEN (SELECT Col1 FROM T1)
ELSE (SELECT Col2 FROM T2) END
, Col3 = CASE WHEN Type = 1 THEN (SELECT Col2 FROM T1)
ELSE (SELECT Col4 FROM T2) END
, Col4 = CASE WHEN Type = 1 THEN (SELECT Col3 FROM T1)
ELSE (SELECT Col4 FROM T2) END
FROM Cars
If you would show us the DDL of all tables involved, you'd probably get a better answer or a different (read better) approach.