How do I find the final "link in the chain" using a recursive CTE - sql

I'm close on this but missing something. How do I only get the first and last links in chains such as A->B, B->C? How do I just get A->C?
CREATE TEMP TABLE IF NOT EXISTS chains (
cname TEXT PRIMARY KEY,
becomes TEXT
);
INSERT INTO chains
VALUES
('A', NULL),
('B', 'C'),
('C', 'D'),
('D', 'E'),
('E', NULL)
;
WITH RECURSIVE
final_link AS (
SELECT
chains.cname,
chains.becomes
FROM
chains
UNION
SELECT
chains.cname,
final_link.becomes
FROM
chains
INNER JOIN final_link
ON chains.becomes = final_link.cname
)
SELECT * FROM final_link;
The results I would like are:
cname | becomes
------|--------
'B' | 'E'
'C' | 'E'
'D' | 'E'

Here is one approach:
with recursive final_link as (
select cname, becomes, cname original_cname, 0 lvl
from chains
where becomes is not null
union all
select c.cname, c.becomes , f.original_cname, f.lvl + 1
from chains c
inner join final_link f on f.becomes = c.cname
where c.becomes is not null
)
select distinct on (original_cname) original_cname, becomes
from final_link
order by original_cname, lvl desc
The idea is to have the subquery keep track of the starting node, and of the level of each node in the tree. You can then filter with distinct on in the outer query.
Demo on DB Fiddle:
original_cname | becomes
:------------- | :------
B | E
C | E
D | E

You can achieve this by starting the recursion only with the chain ends, not with all links, then iteratively prepending links as you are already doing:
WITH RECURSIVE final_link AS (
SELECT cname, becomes
FROM chains c
WHERE (SELECT becomes IS NULL FROM chains WHERE cname = c.becomes)
UNION
SELECT c.cname, fl.becomes
FROM chains c
INNER JOIN final_link fl ON c.becomes = fl.cname
)
SELECT * FROM final_link;
(Demo)

Related

How to get a recursive tree for a single table element

I have a table of this type
| id | parent_id | | title |
parent_id refers to the id of the same table
I need to get a recursive tree for an element knowing only its parent.
it will be clearer what I mean in the picture
On the picture i need to get recursive parent tree for element E, (С id is known) i need get A - C - E tree without B and D and other elements, only for my element E
The nesting can be even greater, I just need to get all the parents in order without unnecessary elements.
This is needed for bread crumbs on my website
How i can do this in PostgreSQL?
Use RECURSIVE query
with recursive rec(id,parent_id, title) as (
select id,parent_id, title from t
where title = 'E'
union all
select t.*
from rec
join t on t.id = rec.parent_id
)
select * from rec
id|parent_id|title|
--+---------+-----+
5| 3|E |
3| 1|C |
1| |A |
Join your table on herself
SELECT t1.title, t2.title as parent, t3.title as great_parent, ...
FROM my_table t1
JOIN my_table t2 on t1.parent_id = t2.id
JOIN my_table t3 on t2.parent_id = t3.id
...
WHERE t1.title = 'curent'
if you don't know how many parent you have, use LEFT JOIN and do as mutch column as needed
thanks to Marmite Bomber
and with a small improvement to know the kinship level :
--drop table if exists recusive_test ;
create table recusive_test (id_parent integer, id integer, title varchar);
insert into recusive_test (id_parent , id , title) values
(1, 2, 'A')
,(2, 3, 'B')
,( 2, 4, 'C')
,( 4, 5, 'D')
,( 3, 6, 'E')
,( 3, 7, 'F')
,( 6, 8, 'G')
,( 6, 9, 'H')
,( 4, 10, 'I')
,( 4, 11, 'J');
WITH RECURSIVE search_tree(id, id_parent, title, step) AS (
SELECT t.id, t.id_parent, t.title ,1
FROM recusive_test t
where title = 'I'
UNION ALL
SELECT t.id, t.id_parent, t.title, st.step+1
FROM recusive_test t, search_tree st
WHERE t.id = st.id_parent
)
SELECT * FROM search_tree ORDER BY step DESC;

Cross join to varray without using Table() expression

I'm trying to learn about cross joining to varray collections.
Example 1: The following query works. It propagates rows via cross joining to the elements in the varray column letter_array, using the Table() expression:
with cte as (
select 1 as id, sys.odcivarchar2list('a', 'b') as letter_array from dual union all
select 2 as id, sys.odcivarchar2list('c', 'd', 'e', 'f') as letter_array from dual)
select
cte.id,
t.column_value
from
cte
cross join
table(letter_array) t
ID COLUMN_VALUE
----- -----------
1 a
1 b
2 c
2 d
2 e
2 f
It produces n rows for each ID (n is a varying number of elements in the letter_array).
Example 2: In a related post, we determined that the TABLE() keyword for collections is now optional.
So, in the next example (with slightly different data), I'll use sys.odcivarchar2list('c', 'd', 'e', 'f') hardcoded in the join, without using the TABLE() expression:
with cte as (
select 1 as id from dual union all
select 2 as id from dual)
select
*
from
cte
cross join
sys.odcivarchar2list('c', 'd', 'e', 'f')
ID COLUMN_VALUE
----- ------------
1 c
1 d
1 e
1 f
2 c
2 d
2 e
2 f
That works as expected too. It produces 4 rows for each ID (4 is the number of elements in the hardcoded varray expression).
Example 3: However, what I actually want, is to use the data from the first example, except this time, use a varray column in the join, without using the TABLE() expression:
with cte as (
select 1 as id, sys.odcivarchar2list('a', 'b') as letter_array from dual union all
select 2 as id, sys.odcivarchar2list('c', 'd', 'e', 'f') as letter_array from dual)
select
cte.id,
t.column_value
from
cte
cross join
letter_array t
Error:
ORA-00942: table or view does not exist
00942. 00000 - "table or view does not exist"
*Cause:
*Action:
Error at Line: 11 Column: 10
Question:
How can I cross join to the varray without using the Table() expression?
Use CROSS APPLY:
with cte (id, letter_array) as (
select 1, sys.odcivarchar2list('a', 'b') from dual union all
select 2, sys.odcivarchar2list('c', 'd', 'e', 'f') from dual
)
select id,
t.column_value
from cte
CROSS APPLY letter_array t
Which outputs:
ID
COLUMN_VALUE
1
a
1
b
2
c
2
d
2
e
2
f
Note: Using a legacy comma join, ANSI CROSS JOIN and CROSS JOIN LATERAL all fail, in Oracle 18c and 21c, without the TABLE keyword and succeed with it. See the fiddle below.
Note 2: TABLE(...) is not a function, it is the syntax for a table collection expression (which wraps a collection expression).
db<>fiddle here

Return a value after postgreSQL identify a sequency in a array

i need to find the correspond position in array B that starts a sequence in a array A of a query in postgreSQL.
My dataset look like
name A B
apple | {0,0,1} | {x y z}
bean | {0,0,0} | {i h j}
rice | {0,1,0} | {o l ç}
and the sequence is 0,1
so i need that query return
Apple | y
bean |
rice | o
How can I do?
I tried to use 'any()', but it just returns apple and rice rows.
I found this quite tricky, because I don't know a way to search for one array within another in Postgres -- and return the index. So, this requires some unnesting.
Try this:
with t as (
select v.*
from (values ('apple', array[0,0,1], array['x', 'y', 'z']),
('bean', array[0,0,0], array['i', 'h', 'j']),
('rice', array[0,1,0] , array['o', 'l', 'ç'])
) v(name, a, b)),
comparison as (
select v.*
from (values (array[0,1])) v(comp)
)
select name,
min(case when v_array = comp then e end)
from (select v.*, x.*,
(array_agg(d) over (partition by v.name order by x.rn rows between current row and unbounded following))[1:array_length(c.comp, 1)] as v_array,
c.comp
from (values ('apple', array[0,0,1], array['x', 'y', 'z']),
('bean', array[0,0,0], array['i', 'h', 'j']),
('rice', array[0,1,0] , array['o', 'l', 'ç'])
) v(name, a, b) cross join
unnest(a, b) with ordinality x(d, e, rn) cross join
comparison c
) vx
group by name;
Here is a db<>fiddle.

How to optimize mapping over several tables

I'm trying to optimize my code. The solution described below works fine, but I'm pretty sure there are better ways to do it. Do you have any recommendations?
I have one table with business contracts and some characteristic attributes:
table_contracts
contract_number attribute_1 attribute_2 attribute_3
123 a e t
456 a f s
789 b g s
And a second table that maps each contract into a specific group. These groups have different priorities (higher number => higher priority). If the attribute column is empty it means that it is not required (=> m3 is the catch all mapping)
table_mappings
map_number priority attribute_1 attribute_2 attribute_3
m1 5 a e t
m2 4 a
m3 3
As a result I need the the contract_number and the corresponding map_number with the highest priority.
This is how I did it, it works but does anyone knows how to optimize that?
with
first_selection as
(
select
table_contracts.contract_number
,table_mappings.priority
,row_number() over(partition by table_contracts.contract_number order by table_mappings.priority desc)
from table_contracts
left join table_mappings
on (table_contracts.attribute_1 = table_mappings.attribute_1 or table_mappings.attribute_1 is null)
and (table_contracts.attribute_2 = table_mappings.attribute_2 or table_mappings.attribute_2 is null)
and (table_contracts.attribute_3 = table_mappings.attribute_3 or table_mappings.attribute_3 is null)
),
second_selection as
(
select
table_contracts.contract_number
,table_mappings.priority
,table_mappings.map_number
from table_contracts
left join table_mappings
on (table_contracts.attribute_1 = table_mappings.attribute_1 or table_mappings.attribute_1 is null)
and (table_contracts.attribute_2 = table_mappings.attribute_2 or table_mappings.attribute_2 is null)
and (table_contracts.attribute_3 = table_mappings.attribute_3 or table_mappings.attribute_3 is null)
)
select
first_selection.contract_number
,second_selection.map_number
from first_selection
join second_selection
on first_selection.contract_number = second_selection.contract_number and first_selection.priority = second_selection.priority
where first_selection.rn = 1
The output of this code would be:
Results
contract_number map_number
123 m1
456 m2
789 m3
I think you only need one of the selections :
with prioritized as(
select c.contract_number, c.attribute_1, c.attribute_2, c.attribute_3, m.map_number
,row_number() over(
partition by c.contract_number
order by m.priority desc
) as rn
from table_contracts c
left join table_mappings m on(
(c.attribute_1 = m.attribute_1 or m.attribute_1 is null)
and (c.attribute_2 = m.attribute_2 or m.attribute_2 is null)
and (c.attribute_3 = m.attribute_3 or m.attribute_3 is null)
)
)
select *
from prioritized
where rn = 1
Try out the below logic using CTE version similar to yours. Hope it helps!
Demo
WITH contracts AS
(SELECT 123 AS contract_number, 'a' AS attribute_1, 'e' AS attribute_2, 't' AS attribute_3 FROM dual
UNION
SELECT 456, 'a', 'f', 's' FROM dual
UNION SELECT 789, 'b', 'g', 's' FROM dual
),
mappings AS
(SELECT 'm1' AS map_number, 5 AS priority, 'a' AS attribute_1, 'e' AS attribute_2, 't' AS attribute_3 FROM dual
UNION
SELECT 'm2', 4, 'a', NULL, NULL FROM dual
UNION
SELECT 'm3', 3, NULL, NULL, NULL FROM dual
),
prioritymap AS
(SELECT contract_number,
map_number,
Rank() over(PARTITION BY contracts.contract_number ORDER BY mappings.priority DESC) AS rank
FROM contracts
JOIN mappings
ON ( contracts.attribute_1 = mappings.attribute_1 OR mappings.attribute_1 IS NULL )
AND ( contracts.attribute_2 = mappings.attribute_2 OR mappings.attribute_2 IS NULL )
AND ( contracts.attribute_3 = mappings.attribute_3 OR mappings.attribute_3 IS NULL )
)
SELECT contract_number, map_number
FROM prioritymap
WHERE prioritymap.rank = 1
You can simply join the tables on the condition given (an attribute is null in the maping table or must match the attribute in the contracts table). Then aggregate per contract number to get the best map_number.
select
c.contract_number,
max(m.map_number) keep (dense_rank last order by m.priority) as map_number
from table_contracts c
join table_mappings m
on (m.attribute_1 is null or m.attribute_1 = c.attribute_1)
and (m.attribute_2 is null or m.attribute_2 = c.attribute_2)
and (m.attribute_3 is null or m.attribute_3 = c.attribute_3)
group by c.contract_number
order by c.contract_number;
Anyway, you are doing this for all contracts and a mapping may match on any combination of attributes, so this will lead to full table scans. The only way I can see to get this quicker is parallel excecution. Maybe the DBMS is set to do this automatically, otherwise you can use a hint:
select /*+parallel(4)*/
...

SQL querying the same table twice with criteria

I have 1 table
table contains something like:
ID, parent_item, Comp_item
1, 123, a
2, 123, b
3, 123, c
4, 456, a
5, 456, b
6, 456, d
7, 789, b
8, 789, c
9, 789, d
10, a, a
11, b, b
12, c, c
13, d, d
I need to return only the parent_items that have a Comp_item of a and b
so I should only get:
123
456
Here is a canonical way to do this:
SELECT parent_item
FROM yourTable
WHERE Comp_item IN ('a', 'b')
GROUP BY parent_item
HAVING COUNT(DISTINCT Comp_item) = 2
The idea here to aggregate by parent_item, restricting to only records having a Comp_item of a or b, then asserting that the distinct number of Comp_item values is 2.
Alternatively you could use INTERSECT:
select parent_item from my_table where comp_item = 'a'
intersect
select parent_item from my_table where comp_item = 'b';
If you have a parent item table, the most efficient method is possibly:
select p.*
from parent_items p
where exists (select 1 from t1 where t1.parent_id = p.parent_id and t1.comp_item = 'a') and
exists (select 1 from t1 where t1.parent_id = p.parent_id and t1.comp_item = 'b');
For optimal performance, you want an index on t1(parent_id, comp_item).
I should emphasize that I very much like the aggregation solution by Tim. I bring this up because performance was brought up in a comment. Both intersect and group by expend effort aggregating (in the first case to remove duplicates, in the second explicitly). An approach like this does not incur that cost -- assuming that a table with unique parent ids is available.