Oracle/procedure to find path between two nodes - oracle

I'm still new to oracle and I need help with the below:
let's say I have a table containing nodes :
DECLARE #nodes TABLE (node VARCHAR(5))
INSERT INTO #nodes
VALUES
('A'),
('B'),
('C'),
('D')
and a table containing how these nodes are connected together:
DECLARE #connected_nodes TABLE (node_1 VARCHAR(5),node_2 VARCHAR(5))
INSERT INTO #connected_nodes
VALUES
('A','B'),
('B','C'),
('A','C'),
('C','D')
I need to write a procedure in oracle that find the path between two nodes , meaning that for example if I want to go from A to C , I have in above case two paths:
A-->C
A--B-->C
so the procedure must return these two paths along with the number of hops (1 for the first path and 2 for the second in this case)
noting that hundreds/thousands of nodes exist.
your help is very appreciated

You need a hierarchical query to get the result you need, applying some logic to only get the paths from a given value to another one:
with nodes (node) as (
select 'A' from dual union all
select 'B' from dual union all
select 'C' from dual union all
select 'D' from dual
),
connected_nodes (node_1,node_2 ) as
(
select 'A','B' from dual union all
select 'B','C' from dual union all
select 'B','D' from dual union all
select 'A','C' from dual union all
select 'A','D' from dual union all
select 'D','C' from dual union all
select 'C','D' from dual
)
select ltrim(sys_connect_by_path(node_1, '->'), '->') as path,
level -1 as hops
from
(
select node as node_1, node_2
from nodes
left join connected_nodes
on(node = node_1)
)
where node_1 = 'C' /* where to stop */
connect by nocycle prior node_2 = node_1
and prior node_1 is not null
start with node_1 = 'A' /* where to start from */;
gives:
PATH HOPS
---------- ----------
A->B->C 2
A->B->D->C 3
A->C 1
A->D->C 2

Note that you cannot use DECLARE #connected_nodes TABLE in Oracle as in SQL SERVER. You need to create a table and insert records into table.
The output you require to be displayed can be achieved with this hierarchical query.
select LTRIM ( SYS_CONNECT_BY_PATH ( node_1,'->' ) ,'->')as paths
, LEVEL-1 as number_of_hops
FROM connected_nodes WHERE LEVEL > 1
CONNECT BY NOCYCLE PRIOR node_2 = node_1
;
Here is a complete demo of all the steps.
DEMO
EDIT: : If you only need to find path between specific nodes(A->C), use
additional conditions.
select LTRIM ( SYS_CONNECT_BY_PATH ( node_1,'->' ) ,'->')as paths
, LEVEL-1 as number_of_hops
FROM connected_nodes WHERE
node_1 = 'C'
START WITH node_1 = 'A'
CONNECT BY NOCYCLE PRIOR node_2 = node_1
;

This might be a way to do it without the hierarchical queries.
They give me a headache.
drop table connected_nodes;
create table connected_nodes (node_1 VARCHAR2(5),node_2 VARCHAR2(5));
INSERT INTO connected_nodes VALUES('A','B');
INSERT INTO connected_nodes VALUES('B','A');
INSERT INTO connected_nodes VALUES('B','C');
INSERT INTO connected_nodes VALUES('A','C');
INSERT INTO connected_nodes VALUES('C','D');
commit;
drop table links;
create table links
(from_node_1 VARCHAR2(5),
from_node_2 VARCHAR2(5),
to_node_1 VARCHAR2(5),
to_node_2 VARCHAR2(5),
first_node_1 VARCHAR2(5),
first_node_2 VARCHAR2(5),
link_num NUMBER);
drop table paths;
create table paths as select * from links;
create or replace procedure get_paths(p_node_1 VARCHAR2,p_node_2 VARCHAR2)
as
prev_num_links number;
num_links number;
begin
-- get first links in paths
insert into links
select
NULL,
NULL,
cn.node_1,
cn.node_2,
cn.node_1,
cn.node_2,
1
from
connected_nodes cn
where
cn.node_1 = p_node_1;
-- loop until number of path links does not increase
prev_num_links := 0;
loop
select count(*) into num_links from links;
if num_links = prev_num_links then
exit;
end if;
-- add new links
insert into links
select
l.to_node_1,
l.to_node_2,
c.node_1,
c.node_2,
l.first_node_1,
l.first_node_2,
l.link_num+1
from connected_nodes c,links l
where
l.to_node_2 = c.node_1 and
l.to_node_2 <> p_node_2 and
(l.to_node_1,
l.to_node_2,
c.node_1,
c.node_2) not in
(select
from_node_1,
from_node_2,
to_node_1,
to_node_2
from links);
commit;
prev_num_links := num_links;
end loop;
-- populate paths table with links that go backward
-- from end node to beginning.
-- add end nodes
insert into paths
select * from links
where to_node_2 = p_node_2;
-- loop until number of paths rows does not increase
prev_num_links := 0;
loop
select count(*) into num_links from paths;
if num_links = prev_num_links then
exit;
end if;
-- add new links
insert into paths
select
*
from links l
where
(l.to_node_1,
l.to_node_2,
l.first_node_1,
l.first_node_2,
l.link_num+1)
in
(select
from_node_1,
from_node_2,
first_node_1,
first_node_2,
link_num
from paths) and
(l.from_node_1,
l.from_node_2,
l.to_node_1,
l.to_node_2,
l.first_node_1,
l.first_node_2,
l.link_num)
not in
(select * from paths);
commit;
prev_num_links := num_links;
end loop;
end;
/
show errors
execute get_paths('A','C');
select
to_node_1,to_node_2,link_num,first_node_1,first_node_2
from paths
order by first_node_1,first_node_2,link_num;
The output looks like this:
TO_NO TO_NO LINK_NUM FIRST FIRST
----- ----- ---------- ----- -----
A B 1 A B
B C 2 A B
A C 1 A C
The columns first_node_1 and first_node_2
define the path and the link_num column
is which link it is in the path.
It is super long and messy. Probably are cases I didn't handle.
I guess use hierarchical queries unless you can't. This is one
attempt to use SQL and PL/SQL without them.

Related

Oracle delete from tableA where a duplicate row is in tableB

As the title says, I am looking for a way to remove all rows from TableA where there is a matching row in TableB.
the Tables A & B have about 30 columns in them so a WHERE A.col1 = B.col1 etc would be a little problematical. Ideally I was hoping for something like
DELETE FROM tableA WHERE IN TableB
(overly simplified by this type of thing)
IN clause can compare all columns returned from select
DELETE FROM tableA WHERE ( col1,col2,col3,.. ) IN ( select col1,col2,col3... FROM TableB );
The brute force way to establish if two records from each table are the same is to just compare every column:
DELETE
FROM tableA a
WHERE EXISTS (SELECT 1 FROM tableB b WHERE a.col1 = b.col1 AND a.col2 = b.col2 AND ...
a.col30 = b.col30);
You could create function which checks structures of tables and, if they are the same, creates string containing correct conditions to compare.
For example here are two tables:
create table t1 (id, name, age) as (
select 1, 'Tom', 67 from dual union all
select 2, 'Tia', 42 from dual union all
select 3, 'Bob', 16 from dual );
create table t2 (id, name, age) as (
select 1, 'Tom', 51 from dual union all
select 3, 'Bob', 16 from dual );
Now use function:
select generate_condition('T1', 'T2') from dual;
result:
T1.ID = T2.ID and T1.NAME = T2.NAME and T1.AGE = T2.AGE
Copy this, paste and run delete query:
delete from t1 where exists (select 1 from t2 where <<PASTE_HERE>>)
Here is the function, adjust it if needed. I used user_tab_columns so if tables are on different schemas you need all_tab_columns and compare owners too. If you have Oracle 11g you can replace loop with listagg(). Second table has to contain all columns of first table and they have to be same type and length.
create or replace function generate_condition(i_t1 in varchar2, i_t2 in varchar2)
return varchar2 is
v varchar2(1000) := '';
begin
for rec in (select column_name, u2.column_id
from user_tab_cols u1
left join (select * from user_tab_cols where table_name = i_t2) u2
using (column_name, data_type, data_length)
where u1.table_name = i_t1 order by u1.column_id)
loop
if rec.column_id is null then
v := 'ERR: incompatible structures';
goto end_loop;
end if;
v := v||' and '||i_t1||'.'||rec.column_name
||' = '||i_t2||'.'||rec.column_name;
end loop;
<< end_loop >>
return(ltrim(v, ' and '));
end;
If you want to avoid running process manually you need dynamic PL/SQL.
create table tableA (a NUMBER, b VARCHAR2(5), c INTEGER);
create table tableB (a NUMBER, b VARCHAR2(5), c INTEGER);
As you said
WHERE A.col1 = B.col1 etc would be a little problematical
you could intersect the tables and mention all columns from tableA one time, like this:
delete tableA
where (a,b,c) in (select * from tableA
intersect
select * from tableB);

For update of this query expression is not allowed (Cursor)

I am running below query
Select location, sum(units) as Units
from
( Select a.from_loc as location, sum(units) as units
from emp a, dept b
where a.id=b.id
Union all
Select a.to_loc as location, sum(units) as units
feom emp a, dept b
where a.id=b.id)
group by location;
Above query is giving me data in below format.
Location | sum(Units)
--------------------
100 | 350
200 | 450
Now i need to update another table Class with units given by above query.
Class is having Location as primary key column and units column also.
I tried to create a cursor but its throwing error, for update cannot be used
Here is snippet of cursor code
Declare
V_location number(20);
V_units (20),
Cursor c1 is
Select location, sum(units) as Units
from
( Select a.from_loc as location, sum(units) as units
from emp a, dept b
where a.id=b.id
Union all
Select a.to_loc as location, sum(units) as units
from emp a, dept b
where a.id=b.id)
group by location -----above select query
for update;
Begin
Open c1;
Loop
Fetch c1 into v_location, v_units;
Exit when c1%notfound;
Update class set units=v_units
where location=v_location;
End loop;
Close c1;
End;
Its throwing For update of this query expression is not allowed
Could someone please let me know what i am doing wrong in here. Or some other approach to update the class table
Would this do any good?
I presume that the FOR UPDATE clause causes problems
I removed the whole DECLARE section and used your SELECT statement in a cursor FOR loop as it is easier to maintain (no opening, closing, exiting, ...)
BEGIN
FOR cur_r IN ( SELECT location, SUM (units) AS units
FROM (SELECT a.from_loc AS location, SUM (units) AS units
FROM emp a, dept b
WHERE a.id = b.id
UNION ALL
SELECT a.to_loc AS location, SUM (units) AS units
FROM emp a, dept b
WHERE a.id = b.id)
GROUP BY location)
LOOP
UPDATE class
SET units = cur_r.units
WHERE location = cur_r.location;
END LOOP;
END;
[EDIT, after reading a comment]
IF-THEN-ELSE is to be done using CASE (or DECODE); for example:
update class set
units = case when location between 1 and 100 then cur_r.units / 10
else cur_r.units / 20
end
where location = cur_r.location

How to get the failing column name for ora-01722: invalid number when inserting

How to get the failing column name for ora-01722: invalid number when inserting.
I have two tables. Table A - all hundred columns are varchar2 and table B one half varchar2 other half number.
table A ( c1 varchar2(100), c2 varchar2(100) ... c100 varchar2(100) )
table B ( c1 number, c2 number .. c49 number, c50 varchar2(100), c51 varchar2(100) ... c100 varchar2(100) )
Need to load table a into table b. the mapping is not 1 to 1. column names are not matching.
insert into b ( c1, c2, c3, c4 .. c50,c51 .. c100 ) select to_number(c20) + to_number(c30), to_number(c10), to_number(c80) .. c4, c5 .. c40 from a.
It takes a lot of time to find the failing column manually in case of ora-01722.
So i tried - log errors into err$_b ('INSERT') reject limit unlimited.
It gives you failing rows, but not the column.
Still need to find the failing column manually . Not much of help.
Tried error handling, but always get 0 offset position with
exception when others then
v_ret := DBMS_SQL.LAST_ERROR_POSITION;
dbms_output.put_line(dbms_utility.format_error_stack);
dbms_output.put_line('Error at offset position '||v_ret);
I could use custom is_number function to check if number before inserting.
But in this case I will get null target value not knowing if source is empty or it has wrong number format.
It is possible automate the process by running data check to find failing rows and columns.
If mapping is one to one - then simple data dictionairy query could generate the report to show what columns will fail.
select 'select col_name, count(*) rc, max(rowid_col) rowid_example from (' from dual union all
select txt from
(
with s as ( select t.owner, t.table_name, t.column_name, t.column_id
from all_tab_columns s join all_tab_columns t on s.owner =t.owner and s.column_name = t.column_name
where lower(s.table_name) = 'a' and s.data_type = 'VARCHAR2' and
lower(t.table_name) = 'b' and t.data_type = 'NUMBER' )
select 'select ''' || column_name ||''' col_name, rowid rowid_col, is_number('|| column_name || ') is_number_check from ' || owner ||'.'|| table_name ||
case when column_id = (select max(column_id) from s )
then ' ) where is_number_check = 0 group by col_name'
else ' union all' end txt
from s order by column_id
)
If there is no one to one mapping between table columns, then you could define and store the mapping into a separate table and then run the report.
Is there something more oracle could help in finding the failing column?
Are there any better manual ways to quicly find it?

How to compare items in an array to those in a database column using regular expressions?

I'm trying to take a list of elements in an array like this:
['GRADE', 'GRATE', 'GRAPE', /*About 1000 other entries here ...*/ ]
and match them to their occurrences in a column in an Oracle database full of entries like this:
1|'ANTERIOR'
2|'ANTEROGRADE'
3|'INGRATE'
4|'RETROGRADE'
5|'REIGN'
...|...
/*About 1,000,000 other entries here*/
For each entry in that array of G words, I'd like to loop through the word column of the Oracle database and try to find the right-sided matches for each entry in the array. In this example, entries 2, 3, and 4 in the database would all match.
In any other programming language, it would look something like this:
for entry in array:
for each in column:
if entry.right_match(each):
print entry
How do I do this in PL/SQL?
In PL/SQL it can be done in this way:
declare
SUBTYPE my_varchar2_t IS varchar2( 100 );
TYPE Roster IS TABLE OF my_varchar2_t;
names Roster := Roster( 'GRADE', 'GRATE', 'GRAPE');
begin
FOR c IN ( SELECT id, name FROM my_table )
LOOP
FOR i IN names.FIRST .. names.LAST LOOP
IF regexp_like( c.name, names( i ) ) THEN
DBMS_OUTPUT.PUT_LINE( c.id || ' ' || c.name );
END IF;
END LOOP;
END LOOP;
end;
/
but this is row by row processing, for large table it would be very slow.
I think it might be better to do it in a way shown below:
create table test123 as
select 1 id ,'ANTERIOR' name from dual union all
select 2,'ANTEROGRADE' from dual union all
select 3,'INGRATE' from dual union all
select 4,'RETROGRADE' from dual union all
select 5,'REIGN' from dual ;
create type my_table_typ is table of varchar2( 100 );
/
select *
from table( my_table_typ( 'GRADE', 'GRATE', 'GRAPE' )) x
join test123 y on regexp_like( y.name, x.column_value )
;
COLUMN_VALUE ID NAME
------------- ---------- -----------
GRADE 2 ANTEROGRADE
GRATE 3 INGRATE
GRADE 4 RETROGRADE

Oracle: how to INSERT if a row doesn't exist

What is the easiest way to INSERT a row if it doesn't exist, in PL/SQL (oracle)?
I want something like:
IF NOT EXISTS (SELECT * FROM table WHERE name = 'jonny') THEN
INSERT INTO table VALUES ("jonny", null);
END IF;
But it's not working.
Note: this table has 2 fields, say, name and age. But only name is PK.
INSERT INTO table
SELECT 'jonny', NULL
FROM dual -- Not Oracle? No need for dual, drop that line
WHERE NOT EXISTS (SELECT NULL -- canonical way, but you can select
-- anything as EXISTS only checks existence
FROM table
WHERE name = 'jonny'
)
Assuming you are on 10g, you can also use the MERGE statement. This allows you to insert the row if it doesn't exist and ignore the row if it does exist. People tend to think of MERGE when they want to do an "upsert" (INSERT if the row doesn't exist and UPDATE if the row does exist) but the UPDATE part is optional now so it can also be used here.
SQL> create table foo (
2 name varchar2(10) primary key,
3 age number
4 );
Table created.
SQL> ed
Wrote file afiedt.buf
1 merge into foo a
2 using (select 'johnny' name, null age from dual) b
3 on (a.name = b.name)
4 when not matched then
5 insert( name, age)
6* values( b.name, b.age)
SQL> /
1 row merged.
SQL> /
0 rows merged.
SQL> select * from foo;
NAME AGE
---------- ----------
johnny
If name is a PK, then just insert and catch the error. The reason to do this rather than any check is that it will work even with multiple clients inserting at the same time. If you check and then insert, you have to hold a lock during that time, or expect the error anyway.
The code for this would be something like
BEGIN
INSERT INTO table( name, age )
VALUES( 'johnny', null );
EXCEPTION
WHEN dup_val_on_index
THEN
NULL; -- Intentionally ignore duplicates
END;
I found the examples a bit tricky to follow for the situation where you want to ensure a row exists in the destination table (especially when you have two columns as the primary key), but the primary key might not exist there at all so there's nothing to select.
This is what worked for me:
MERGE INTO table1 D
USING (
-- These are the row(s) you want to insert.
SELECT
'val1' AS FIELD_A,
'val2' AS FIELD_B
FROM DUAL
) S ON (
-- This is the criteria to find the above row(s) in the
-- destination table. S refers to the rows in the SELECT
-- statement above, D refers to the destination table.
D.FIELD_A = S.FIELD_A
AND D.FIELD_B = S.FIELD_B
)
-- This is the INSERT statement to run for each row that
-- doesn't exist in the destination table.
WHEN NOT MATCHED THEN INSERT (
FIELD_A,
FIELD_B,
FIELD_C
) VALUES (
S.FIELD_A,
S.FIELD_B,
'val3'
)
The key points are:
The SELECT statement inside the USING block must always return rows. If there are no rows returned from this query, no rows will be inserted or updated. Here I select from DUAL so there will always be exactly one row.
The ON condition is what sets the criteria for matching rows. If ON does not have a match then the INSERT statement is run.
You can also add a WHEN MATCHED THEN UPDATE clause if you want more control over the updates too.
Using parts of #benoit answer, I will use this:
DECLARE
varTmp NUMBER:=0;
BEGIN
-- checks
SELECT nvl((SELECT 1 FROM table WHERE name = 'john'), 0) INTO varTmp FROM dual;
-- insert
IF (varTmp = 1) THEN
INSERT INTO table (john, null)
END IF;
END;
Sorry for I don't use any full given answer, but I need IF check because my code is much more complex than this example table with name and age fields. I need a very clear code. Well thanks, I learned a lot! I'll accept #benoit answer.
In addition to the perfect and valid answers given so far, there is also the ignore_row_on_dupkey_index hint you might want to use:
create table tq84_a (
name varchar2 (20) primary key,
age number
);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Johnny', 77);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Pete' , 28);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Sue' , 35);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Johnny', null);
select * from tq84_a;
The hint is described on Tahiti.
you can use this syntax:
INSERT INTO table_name ( name, age )
select 'jonny', 18 from dual
where not exists(select 1 from table_name where name = 'jonny');
if its open an pop for asking as "enter substitution variable" then use this before the above queries:
set define off;
INSERT INTO table_name ( name, age )
select 'jonny', 18 from dual
where not exists(select 1 from table_name where name = 'jonny');
You should use Merge:
For example:
MERGE INTO employees e
USING (SELECT * FROM hr_records WHERE start_date > ADD_MONTHS(SYSDATE, -1)) h
ON (e.id = h.emp_id)
WHEN MATCHED THEN
UPDATE SET e.address = h.address
WHEN NOT MATCHED THEN
INSERT (id, address)
VALUES (h.emp_id, h.address);
or
MERGE INTO employees e
USING hr_records h
ON (e.id = h.emp_id)
WHEN MATCHED THEN
UPDATE SET e.address = h.address
WHEN NOT MATCHED THEN
INSERT (id, address)
VALUES (h.emp_id, h.address);
https://oracle-base.com/articles/9i/merge-statement
CTE and only CTE :-)
just throw out extra stuff. Here is almost complete and verbose form for all cases of life. And you can use any concise form.
INSERT INTO reports r
(r.id, r.name, r.key, r.param)
--
-- Invoke this script from "WITH" to the end (";")
-- to debug and see prepared values.
WITH
-- Some new data to add.
newData AS(
SELECT 'Name 1' name, 'key_new_1' key FROM DUAL
UNION SELECT 'Name 2' NAME, 'key_new_2' key FROM DUAL
UNION SELECT 'Name 3' NAME, 'key_new_3' key FROM DUAL
),
-- Any single row for copying with each new row from "newData",
-- if you will of course.
copyData AS(
SELECT r.*
FROM reports r
WHERE r.key = 'key_existing'
-- ! Prevent more than one row to return.
AND FALSE -- do something here for than!
),
-- Last used ID from the "reports" table (it depends on your case).
-- (not going to work with concurrent transactions)
maxId AS (SELECT MAX(id) AS id FROM reports),
--
-- Some construction of all data for insertion.
SELECT maxId.id + ROWNUM, newData.name, newData.key, copyData.param
FROM copyData
-- matrix multiplication :)
-- (or a recursion if you're imperative coder)
CROSS JOIN newData
CROSS JOIN maxId
--
-- Let's prevent re-insertion.
WHERE NOT EXISTS (
SELECT 1 FROM reports rs
WHERE rs.name IN(
SELECT name FROM newData
));
I call it "IF NOT EXISTS" on steroids. So, this helps me and I mostly do so.

Resources