Using recursive CTE with Ecto - postgresql

How would I go about using the result of a recursive CTE in a query I plan to run with Ecto? For example let's say I have a table, nodes, structured as so:
-- nodes table example --
id parent_id
1 NULL
2 1
3 1
4 1
5 2
6 2
7 3
8 5
and I also have another table nodes_users structured as so:
-- nodes_users table example --
node_id user_id
1 1
2 2
3 3
5 4
Now, I want to grab all the users with a node at or above a specific node, for the sake of an example let's choose the node w/ the id 8.
I could use the following recursive postgresql query to do so:
WITH RECURSIVE nodes_tree AS (
SELECT *
FROM nodes
WHERE nodes.id = 8
UNION ALL
SELECT n.*
FROM nodes n
INNER JOIN nodes_tree nt ON nt.parent_id = n.id
)
SELECT u.* FROM users u
INNER JOIN users_nodes un ON un.user_id = u.id
INNER JOIN nodes_tree nt ON nt.id = un.node_id
This should return users.* for the users w/ id of 1, 2, and 4.
I'm not sure how I could run this same query using ecto, ideally in a manner that would return a chainable output. I understand that I can insert raw SQL into my query using the fragment macro, but I'm not exactly sure where that would go for this use or if that would even be the most appropriate route to take.
Help and/or suggestions would be appreciated!

I was able to accomplish this using a fragment. Here's an example of the code I used. I'll probably move this method to a stored procedure.
Repo.all(MyProj.User,
from u in MyProj.User,
join: un in MyProj.UserNode, on: u.id == un.user_id,
join: nt in fragment("""
(
WITH RECURSIVE node_tree AS (
SELECT *
FROM nodes
WHERE nodes.id = ?
UNION ALL
SELECT n.*
FROM nodes n
INNER JOIN node_tree nt ON nt.parent_id == n.id
)
) SELECT * FROM node_tree
""", ^node_id), on: un.node_id == nt.id
)

Related

Postgresql recursive query

I have table with self-related foreign keys and can not get how I can receive firs child or descendant which meet condition. My_table structure is:
id
parent_id
type
1
null
union
2
1
group
3
2
group
4
3
depart
5
1
depart
6
5
unit
7
1
unit
I should for id 1 (union) receive all direct child or first descendant, excluding all groups between first descendant and union. So in this example as result I should receive:
id
type
4
depart
5
depart
7
unit
id 4 because it's connected to union through group with id 3 and group with id 2 and id 5 because it's connected directly to union.
I've tried to write recursive query with condition for recursive part: when parent_id = 1 or parent_type = 'depart' but it doesn't lead to expected result
with recursive cte AS (
select b.id, p.type_id
from my_table b
join my_table p on p.id = b.parent_id
where b.id = 1
union
select c.id, cte.type_id
from my_table c
join cte on cte.id = c.parent_id
where c.parent_id = 1 or cte.type_id = 'group'
)
Here's my interpretation:
if type='group', then id and parent_id are considered in the same group
id#1 and id#2 are in the same group, they're equals
id#2 and id#3 are in the same group, they're equals
id#1, id#2 and id#3 are in the same group
If the above is correct, you want to get all the first descendent of id#1's group. The way to do that:
Get all the ids in the same group with id#1
Get all the first descendants of the above group (type not in ('union', 'group'))
with recursive cte_group as (
select 1 as id
union all
select m.id
from my_table m
join cte_group g
on m.parent_id = g.id
and m.type = 'group')
select mt.id,
mt.type
from my_table mt
join cte_group cg
on mt.parent_id = cg.id
and mt.type not in ('union','group');
Result:
id|type |
--+------+
4|depart|
5|depart|
7|unit |
Sounds like you want to start with the row of id 1, then get its children, and continue recursively on rows of type group. To do that, use
WITH RECURSIVE tree AS (
SELECT b.id, b.type, TRUE AS skip
FROM my_table b
WHERE id = 1
UNION ALL
SELECT c.id, c.type, (c.type = 'group') AS skip
FROM my_table c
JOIN tree p ON c.parent_id = p.id AND p.skip
)
SELECT id, type
FROM tree
WHERE NOT skip

How to include and exclude ids in once query postgresql

I use PostgreSQL 13.3
I'm trying to think how I can make include/exclude in query at the same time
I have include_system_ids [1,5] and exclude_system_ids [3]
There's one big table - records
system_records table
record
system_id
1
1
1
5
1
3
2
1
2
5
If a record contains an exclusive identifier, then it should not be included in the final selection. I had some several tries, but I didn't get a necessary result
Awaiting result: record with id 2
Fact result: 1, 2
My variants
select r.id from records r
left join (select record_id from system_records
where system_id in (1,5)
) include_ids on r.id = include_ids
left join (select record_id from system_records
where system_id not in (3)
) exclude_ids on r.id = exclude_ids.id
Honestly, I don't understand how I can do it((
Is there anyone who can help me
Maybe this query could be a solution (result here)
with x as (select record,string_agg(system_id::varchar,',') as sys_id from records group by record)
select records.*
from records,x
where records.record = x.record
and x.sys_id = '1,5'

I want to get unique rows on joining tables

I am running the following query while trying to join 3 tables :
select
a.project_id, a.acc_name, a.project_name, a.iot,a.acc_id, a.active,
b.app_fte, b.contact_person, c.cost_call_date
from
Account a, Application b, account_version c
where
a.acc_id in (Select acc_id from account where acc_name='GGG') and
EXTRACT(MONTH FROM c.cost_call_date) = 3;
Sample data from the tables are as follows :
Account :
acc_id acc_name iot acc_contact project_id project_name ilc_code license_no active
2 GGG NA YYY 7777 HHH TTR 766 false
Application :
app_id app_name app_fte contact_person acc_id
1 sfsf 4 sdsdff 2
Account_version :
line_id acc_id version_no chargable_fte cost_call_date is_approved
9 2 7 4 2018-03-20
Here acc_id is the primary key for the Account table and the foreign key for the Application and Account_version tables. When I am running the above query I am getting 30 rows I have also tried using the distinct keyword but still I get 10 rows. Please help me in getting unique rows.
Try something like this
SELECT DISTINCT a.project_id, a.acc_name, a.project_name, a.iot,a.acc_id, a.active, b.app_fte, b.contact_person, c.cost_call_date
FROM Account a
INNER JOIN Application b
USING (acc_id)
INNER JOIN account_version c
USING (acc_id)
WHERE a.acc_name = 'GGG'
AND EXTRACT(MONTH FROM c.cost_call_date) = 3
For reference as to why your query was giving you more rows than expected, try running this:
SELECT a.*, b.*
FROM generate_series(1, 10) a, generate_series(1, 10) b
What you are doing by selecting from multiple tables as you did is a cross join. What you should actually be doing is an inner join to get the rows you want, and a DISTINCT if required to get only distinct rows from the results.

Subsetting records that contain multiple values in one column

In my postgres table, I have two columns of interest: id and name - my goal is to only keep records where id has more than one value in name. In other words, would like to keep all records of ids that have multiple values and where at least one of those values is B
UPDATE: I have tried adding WHERE EXISTS to the queries below but this does not work
The sample data would look like this:
> test
id name
1 1 A
2 2 A
3 3 A
4 4 A
5 5 A
6 6 A
7 7 A
8 2 B
9 1 B
10 2 B
and the output would look like this:
> output
id name
1 1 A
2 2 A
8 2 B
9 1 B
10 2 B
How would one write a query to select only these kinds records?
Based on your description you would seem to want:
select id, name
from (select t.*, min(name) over (partition by id) as min_name,
max(name) over (partition by id) as max_name
from t
) t
where min_name < max_name;
This can be done using EXISTS:
select id, name
from test t1
where exists (select *
from test t2
where t1.id = t2.id
and t1.name <> t2.name) -- this will select those with multiple names for the id
and exists (select *
from test t3
where t1.id = t3.id
and t3.name = 'B') -- this will select those with at least one b for that id
Those records where for their id more than one name shines up, right?
This could be formulated in "SQL" as follows:
select * from table t1
where id in (
select id
from table t2
group by id
having count(name) > 1)

Simplified cross joins?

Let's sat I have a Table 'A' with rows:
A
B
C
D
Is there a simple way to do a cross join that creates
A 1
A 2
A 3
A 4
...
D 1
D 2
D 3
D 4
without creating a second table?
Something like:
SELECT *
FROM A
CROSS JOIN (1,2,3,4)
something like that should work, i guess
select * from A cross join (select 1 union all select 2 union all select 3 union all select 4) as tmp
you will create a second table, but you won't persist it.
The following would work for a table of any size (though I only tested it against 6 rows). It uses the ranking functions available in SQL Server 2005 and up, but the idea should be adaptible to any RDBMS.
SELECT ta.SomeColumn, cj.Ranking
from TableA ta
cross join (select row_number() over (order by SomeColumn) Ranking from TableA) cj
order by ta.SomeColumn, cj.Ranking
You should be able to achieve this via
select * from A cross join
(select 1
union all
select 2
union all
select 3
union all
select 4)