Order by objects relation (PostgreSQL) - postgresql

Have 2 tables for example:
In 1st: object & parent columns
object | parent
-------+---------
object1| null
object2| object1
object3| null
2nd has: object & reference columns
object | reference
-------+---------
object1| null
object2| null
object3| object1
Need to query tables to order like following: parent is first, then - child(s), objects which have reference(s) to parent.
object1
object2
object3
Is it possible to do in one SQL query or need to sort manually in an array? Seems it is a classical task, probably solution already exists somewhere?

Is this what you're looking for?
CREATE TABLE oparen (object varchar(10), parent varchar(10));
CREATE TABLE oref (object varchar(10), ref varchar(10));
INSERT INTO oparen VALUES
('object1',null),('object2','object1'),
('object3',null),('object4','object2');
INSERT INTO oref VALUES
('object1',null),('object2',null),('object3','object1'),
('object5','object6'),('object6','object1'),('object7','object4');
WITH hier AS (
SELECT parent AS obj, 1 AS rank FROM oparen
WHERE parent IS NOT NULL
UNION
SELECT object, 2 FROM oparen
WHERE parent IS NOT NULL
UNION
SELECT object, 3 FROM oref
WHERE ref IS NOT NULL),
allobj AS (
SELECT object AS obj FROM oparen
UNION
SELECT object FROM oref)
SELECT a.obj, coalesce(h.rank, 4) AS rank
FROM allobj a LEFT JOIN hier h ON a.obj = h.obj
ORDER BY coalesce(h.rank, 4), a.obj;
EDIT: After the improved example in the answer below, the following query should do the trick:
WITH parents AS (
SELECT parent AS obj, 1 AS rank FROM oparen
WHERE parent IS NOT NULL
),
family AS (
SELECT * FROM parents
UNION ALL
SELECT object, 2 FROM oparen op
WHERE parent IS NOT NULL
AND NOT EXISTS (SELECT obj FROM parents WHERE obj = op.object)
),
hier AS (
SELECT * FROM family
UNION ALL
SELECT object AS obj, coalesce(f.rank + 2, 5) AS rank
FROM oref LEFT JOIN family f ON oref.ref = f.obj
WHERE ref IS NOT NULL
),
allobj AS (
SELECT object AS obj FROM oparen
UNION
SELECT object FROM oref)
SELECT a.obj, h.rank AS rank
FROM allobj a LEFT JOIN hier h ON a.obj = h.obj
ORDER BY h.rank, a.obj;
Testbed creation in the top is updated according to the new requirements.

I inserted following data:
INSERT INTO oparen VALUES
('object1',null),('object2','object1'),('object3',null),('object4','object2');
INSERT INTO oref VALUES
('object1',null),('object2',null),('object3','object1'),('object5','object6'),('object6','object1');
Order is incorrect and object2 listed twice. DISTINCT on obj breaks the order also. Should go 6 then 5.

No, does not work: checked for another data and simplified to use and only by oref table content:
INSERT INTO oref VALUES
('object1',null),('object2',null),('object3','object1'),
('object5','object6'),('object6','object1'),('object7','object4'), ('object4','object5');
WITH family AS (
SELECT object AS obj, 1 AS rank FROM oref
WHERE ref IS NULL
),
hier AS (
SELECT * FROM family
UNION ALL
SELECT object AS obj, coalesce(f.rank + 2, 5) AS rank
FROM oref LEFT JOIN family f ON oref.ref = f.obj
WHERE ref IS NOT NULL
),
allobj AS (
SELECT object AS obj FROM oref)
SELECT a.obj, h.rank AS rank
FROM allobj a
LEFT JOIN hier h ON a.obj = h.obj
ORDER BY h.rank, a.obj;
Think need to use recursive queries here. Will write and post here.

Following recursive query works:
WITH RECURSIVE tables(object, rank) AS (
SELECT DISTINCT o.object, 1 AS rank FROM oref o
WHERE o.ref IS NULL
UNION
SELECT o.object, t.rank + 1 AS rank
FROM (SELECT DISTINCT o.object, o.ref FROM oref o
WHERE ref IS NOT NULL) o, tables t
WHERE o.ref = t.object AND rank <= t.rank
),
ordered AS (
SELECT * FROM tables
)
SELECT * FROM tables
WHERE tables.rank = (SELECT MAX(rank) FROM ordered WHERE ordered.object = tables.object)
ORDER BY rank;
Any comments, questions, objections, propositions? ;)

Related

Updating a parent and its children without link between them

I'm facing an issue and hope you will be able to guide me in the right direction.
A facility can have 1..n sub-facilities. Sub-facilities can have 1..n sub-facilities as well. And this can be a never ending relationship.
In the database, we only have keys to connect the parent and first children.
Here's a short schema:
--Facility has : facilityid|parentid
---child1 has : facilityid|parentid to facility
----child2 has : facilityid|parentid to child 1 / no key to main parent
-----child2 has : facilityid|parentid to child 2 / no key to main parent
What I am trying to do is update a column named value and set it to true for a parent and all of its children and sub-children included. Right now, since I am only able to see the first parent and its children but not the sub-children, I can only update the value of those. But that leaves all my sub-children not updated.
Knowing that there is no link between sub-children and main parent, how would I accomplish that?
Here's a query that gives me a parent and all of its children.
WITH attributes AS (
select fa.facilityid, a.id
from caip_attribute a
join caip_facility_attribute fa on a.id = fa.attributeid)
select a.facilityid as parentfacilityid, a.id as parentattributeid, f.id as facilitychildid, fa.attributeid as childattributeid
from attributes a
join caip_facility f on f.parentid = a.facilityid
join caip_facility_attribute fa on fa.facilityid = f.id
join caip_attribute at on at.id = fa.attributeid
where at.definitionTypeKey = 'AUTO_CREATE_PROCESS'
order by f.id asc
The update statement is missing here but this is how I get the values that will later need to be updated.
Thank you!
To update all descendants of a parent you can use a recursive CTE.
Suppose we have a data table with relations set using self-referencing foreign key column parent_id:
CREATE TABLE t (
id SERIAL PRIMARY KEY,
value BOOL DEFAULT FALSE,
parent_id INT,
FOREIGN KEY (parent_id) REFERENCES t(id)
);
First, a query that returns a parent with id = 1 and all it's descendants would be like this:
WITH RECURSIVE children(id, value, parent_id) AS (
SELECT id, value, parent_id FROM t WHERE id = 1
UNION
SELECT t.id, t.value, t.parent_id FROM t
INNER JOIN children ON children.id = t.parent_id
)
SELECT * FROM children
Second, to update a value of parent and all it's descendants we can set value for all rows with ids in our select query:
UPDATE t SET value = true
WHERE id IN (
WITH RECURSIVE children(id, value, parent_id) AS (
SELECT id, value, parent_id FROM t WHERE id = 1
UNION
SELECT t.id, t.value, t.parent_id FROM t
INNER JOIN children ON children.id = t.parent_id
)
SELECT id FROM children
)
Check a demo

Handle Null in jsonb_array_elements

I have 2 tables a and b
Table a
id | name | code
VARCHAR VARCHAR jsonb
1 xyz [14, 15, 16 ]
2 abc [null]
3 def [null]
Table b
id | name | code
1 xyz [16, 15, 14 ]
2 abc [null]
I want to figure out where the code does not match for same id and name. I sort code column in b b/c i know it same but sorted differently
SELECT a.id,
a.name,
a.code,
c.id,
c.name,
c.code
FROM a
FULL OUTER JOIN ( SELECT id,
name,
jsonb_agg(code ORDER BY code) AS code
FROM (
SELECT id,
name,
jsonb_array_elements(code) AS code
FROM b
GROUP BY id,
name,
jsonb_array_elements(code)
) t
GROUP BY id,
name
) c
ON a.id = c.id
AND a.name = c.name
AND COALESCE (a.code, '[]'::jsonb) = COALESCE (c.code, '[]'::jsonb)
WHERE (a.id IS NULL OR c.id IS NULL)
My answer in this case should only return id = 3 b/c its not in b table but my query is returning id = 2 as well b/c i am not handling the null case well enough in the inner subquery
How can i handle the null use case in the inner subquery?
demo:db<>fiddle
The <# operator checks if all elements of the left array occur in the right one. The #> does other way round. So using both you can ensure that both arrays contain the same elements:
a.code #> b.code AND a.code <# b.code
Nevertheless it will be accept as well if one array contains duplicates. So [42,42] will be the same as [42]. If you want to avoid this as well you should check the array length as well
AND jsonb_array_length(a.code) = jsonb_array_length(b.code)
Furthermore you might check if both values are NULL. This case has to be checked separately:
a.code IS NULL and b.code IS NULL
A little bit shorter form is using the COALESCE function:
COALESCE(a.code, b.code) IS NULL
So the whole query could look like this:
SELECT
*
FROM a
FULL OUTER JOIN b
ON a.id = b.id AND a.name = b.name
AND (
COALESCE(a.code, b.code) IS NULL -- both null
OR (a.code #> b.code AND a.code <# b.code
AND jsonb_array_length(a.code) = jsonb_array_length(b.code) -- avoid accepting duplicates
)
)
After that you are able to filter the NULL values in the WHERE clause

How to merge JSONB field in a tree structure?

I have a table in Postgres which stores a tree structure. Each node has a jsonb field: params_diff:
CREATE TABLE tree (id INT, parent_id INT, params_diff JSONB);
INSERT INTO tree VALUES
(1, NULL, '{ "some_key": "some value" }'::jsonb)
, (2, 1, '{ "some_key": "other value", "other_key": "smth" }'::jsonb)
, (3, 2, '{ "other_key": "smth else" }'::jsonb);
The thing I need is to select a node by id with additional generated params field which contains the result of merging all params_diff from the whole parents chain:
SELECT tree.*, /* some magic here */ AS params FROM tree WHERE id = 3;
id | parent_id | params_diff | params
----+-----------+----------------------------+-------------------------------------------------------
3 | 2 | {"other_key": "smth else"} | {"some_key": "other value", "other_key": "smth else"}
Generally, a recursive CTE can do the job. Example:
Use table alias in another query to traverse a tree
We just need a more magic to decompose, process and re-assemble the JSON result. I am assuming from your example, that you want each key once only, with the first value in the search path (bottom-up):
WITH RECURSIVE cte AS (
SELECT id, parent_id, params_diff, 1 AS lvl
FROM tree
WHERE id = 3
UNION ALL
SELECT t.id, t.parent_id, t.params_diff, c.lvl + 1
FROM cte c
JOIN tree t ON t.id = c.parent_id
)
SELECT id, parent_id, params_diff
, (SELECT json_object(array_agg(key ORDER BY lvl)
, array_agg(value ORDER BY lvl))::jsonb
FROM (
SELECT key, value
FROM (
SELECT DISTINCT ON (key)
p.key, p.value, c.lvl
FROM cte c, jsonb_each_text(c.params_diff) p
ORDER BY p.key, c.lvl
) sub1
ORDER BY lvl
) sub2
) AS params
FROM cte
WHERE id = 3;
How?
Walk the tree with a classic recursive CTE.
Create a derived table with all keys and values with jsonb_each_text() in a LATERAL JOIN, remember the level in the search path (lvl).
Use DISTINCT ON to get the "first" (lowest lvl) value for each key. Details:
Select first row in each GROUP BY group?
Sort and aggregate resulting keys and values and feed the arrays to json_object() to build the final params value.
SQL Fiddle (only as far as pg 9.3 can go with json instead of jsonb).

Sorting rows by children?

I have this table:
CREATE TABLE items (
id SERIAL PRIMARY KEY,
data TEXT,
parent INT,
posted INT
);
Each item has a piece of data, a timestamp, and a parent. I'd like to select the top 10 root items (parent = 0), sorted by the timestamp of the most recent child.
If item #1 has a child #2 that has a child #3, #3 is considered a child of #1.
How can I do this?
EDIT:
The query has been rewritten to
first sort the child items
get the root parent id and the rank for each item
select the top 10 parents
select the details for the top 10 parents
Common Table expressions have been used to incrementally select the data following the above steps.
WITH recursive c AS
(
SELECT *
FROM seeds
UNION ALL
SELECT
T.id,
T.parent,
c.topParentID,
(c.child_level + 1),
c.child_rank
FROM items AS T
INNER JOIN c ON T.parent = c.id
WHERE T.id <> T.parent
)
, seeds AS
(
SELECT
id,
parent,
parent AS topParentID,
0 AS child_level,
rank() OVER (ORDER BY posted DESC) child_rank
FROM items
WHERE parent <> 0
ORDER BY posted DESC
)
, rank_level AS
(
SELECT DISTINCT
c2.id id,
c_ranks.min_child_rank child_rank,
c_roots.max_child_level root_level
FROM
(
SELECT
id,
MAX(child_level) max_child_level
FROM c
GROUP BY id
)
c_roots
INNER JOIN c c2 ON c_roots.id = c2.id
INNER JOIN
(
SELECT
id,
MIN(child_rank) min_child_rank
FROM c
GROUP BY id
)
c_ranks
ON c2.id = c_ranks.id
)
, top_10_parents AS
(
SELECT
c.topParentID id,
MIN(rl.child_rank) id_rank
FROM rank_level rl
INNER JOIN c ON rl.id = c.id AND c.child_level = rl.root_level
GROUP BY c.topParentID
ORDER BY MIN(rl.child_rank)
limit 10
)
SELECT
i.*
FROM
items i
INNER JOIN top_10_parents tp ON tp.id = i.id
ORDER BY tp.id_rank;
SQL Fiddle
Reference:
WITH Queries (Common Table Expressions) on PostgreSQL Manual

How to get the top most parent in PostgreSQL

I have a tree structure table with columns:
id,parent,name.
Given a tree A->B->C,
how could i get the most top parent A's ID according to C's ID?
Especially how to write SQL with "with recursive"?
Thanks!
WITH RECURSIVE q AS
(
SELECT m
FROM mytable m
WHERE id = 'C'
UNION ALL
SELECT m
FROM q
JOIN mytable m
ON m.id = q.parent
)
SELECT (m).*
FROM q
WHERE (m).parent IS NULL
To implement recursive queries, you need a Common Table Expression (CTE).
This query computes ancestors of all parent nodes. Since we want just the top level, we select where level=0.
WITH RECURSIVE Ancestors AS
(
SELECT id, parent, 0 AS level FROM YourTable WHERE parent IS NULL
UNION ALL
SELECT child.id, child.parent, level+1 FROM YourTable child INNER JOIN
Ancestors p ON p.id=child.parent
)
SELECT * FROM Ancestors WHERE a.level=0 AND a.id=C
If you want to fetch all your data, then use an inner join on the id, e.g.
SELECT YourTable.* FROM Ancestors a WHERE a.level=0 AND a.id=C
INNER JOIN YourTable ON YourTable.id = a.id
Assuming a table named "organization" with properties id, name, and parent_organization_id, here is what worked for me to get a list that included top level and parent level org ID's for each level.
WITH RECURSIVE orgs AS (
SELECT
o.id as top_org_id
,null::bigint as parent_org_id
,o.id as org_id
,o.name
,0 AS relative_depth
FROM organization o
UNION
SELECT
allorgs.top_org_id
,childorg.parent_organization_id
,childorg.id
,childorg.name
,allorgs.relative_depth + 1
FROM organization childorg
INNER JOIN orgs allorgs ON allorgs.org_id = childorg.parent_organization_id
) SELECT
*
FROM
orgs order by 1,5;