1

Find the first common parent, if any, from many different children.

Example:

    1       
   / \     
  2   3    
 /   / \
7   8   9  
   / \
 10   11

Input: [10, 9]
Output: 3 (first common parent for this elements)

Table example:

+------------------+-----------+------+
|EmployeePositionId|Subdivision|Parent|
+------------------+-----------+------+
|4718              |485        |42    |
|4719              |5064       |485   |
|4720              |5065       |5064  |
|4721              |5065       |5064  |
|4722              |3000       |null  |
+------------------+-----------+------+

If I try to search for EmployeePositionId [4719, 4720, 4721], I would like to get the Subdivision 5064, because it is the closest common subdivision for both employees (5065 nested in 5064).

If I were looking for 4719, 4720, 4721, 4722, then I would like to get null, because these elements do not have a common parent.

Or the answer will help me how get the data so that later solve this in Python

Masta
  • 81
  • 6
  • Based on what logic? Please, be more specific and provide more details. – Maciej Los Nov 16 '21 at 19:31
  • Is there an indicator for the level of hierarchy in the table? And is the integrity of the tree guaranteed (no loops)? Min / max number of input IDs? All input IDs exist in the table? Your Postgres version? – Erwin Brandstetter Nov 16 '21 at 21:15
  • I found this, but it is only looking at two ids, not a variable number. https://stackoverflow.com/questions/608076/how-to-get-lowest-common-parent-for-2-rows-in-recursive-table-sql – dogyog Nov 16 '21 at 22:46
  • – Maciej Los, it is necessary to find the closest general department by employees, if any – Masta Nov 17 '21 at 07:21
  • 1
    – Erwin Brandstetter, there is no hierarchy indicator in the table, but you can add it in the query in the non-recursive part: 0 as "level" if it helps. There are definitely no cycles. This tree is "up". Min input - 0, Max - not more than 1500. All input IDs exist in the table. Postgres 10.11 – Masta Nov 17 '21 at 07:24

1 Answers1

1

This class of problems is hard for SQL.
It's even harder with your particular table. It's not properly normalized. There is no level indicator. And input IDs can be from mixed hierarchy levels.

Setup

You clarified in a later comment that every path is terminated with a row that has "Parent" IS NULL (root), even if sample data in the question suggest otherwise. That helps a bit.

I assume valid "EmployeePositionId" as input. And no loops in your tree or the CTE enters an endless loop.

If you don't have a level of hierarchy in the table, add it. It's a simple task. If you can't add it, create a VIEW or, preferably, a MATERIALIZED VIEW instead:

CREATE MATERIALIZED VIEW mv_tbl AS
WITH RECURSIVE cte AS (
   SELECT *, 0 AS level
   FROM   tbl
   WHERE  "Parent" IS NULL
   
   UNION ALL
   SELECT t.*, c.level + 1
   FROM   cte c
   JOIN   tbl t ON t."Parent" = c."Subdivision"
   )
TABLE cte;

These would be the perfect indices for the task:

CREATE UNIQUE INDEX mv_tbl_id_uni ON mv_tbl ("EmployeePositionId") INCLUDE ("Subdivision", "Parent", level);  
CREATE INDEX mv_tbl_subdivision_idx ON mv_tbl ("Subdivision") INCLUDE ("Parent", level);

See:

Query

Pure SQL solution with recursive CTE, based on a table with level indicator (or the MV from above):

WITH RECURSIVE init AS (
   SELECT "Subdivision", "Parent", level
   FROM   mv_tbl
   WHERE  "EmployeePositionId" IN (4719, 4720, 4721)  -- input
   )
, cte AS (
   TABLE init
   UNION
   SELECT c."Parent", t."Parent", c.level - 1
   FROM   cte c
   JOIN   mv_tbl t ON t."Subdivision" = c."Parent"  -- recursion terminated at "Parent" IS NULL
   )
, agg AS (
   SELECT level, min("Subdivision") AS "Subdivision", count(*) AS ct
   FROM   cte
   GROUP  BY  level
   )
SELECT "Subdivision"
FROM   agg a
WHERE  ct = 1                                  -- no other live branch
AND    level <  (SELECT max(level) FROM cte WHERE "Parent" IS NULL) IS NOT TRUE  -- no earlier dead end
AND    level <= (SELECT min(level) FROM init)  -- include highest (least) level
ORDER  BY level DESC                           -- pick earliest (greatest) qualifying level
LIMIT  1;

db<>fiddle here

Covers all possible input, works for any modern version of Postgres.

I added basic explanation in the code.

Related:

Legal, lower-case, unquoted identifiers make your life with Postgres easier. See:

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • For `{4719, 4720, 4721, 4722}` your query returns `42` instead of null. – Maciej Los Nov 16 '21 at 19:44
  • @MaciejLos: Consider the improved query. – Erwin Brandstetter Nov 16 '21 at 23:02
  • Unfortunately, there is no nesting level in the table. And I cannot add the constraint "Parent not null". "Parent = null" indicates that the unit is at the root. This is how a huge table is arranged and works, I cannot make such changes to it. But can solve the problem like this - tell me please how to get the data in such a way that i can solve the problem in post-processing in Python – Masta Nov 17 '21 at 07:40
  • @Masta I didn't suggest a constraint "Parent not null". Consider updates for your added information (which should be in the question). Good luck. – Erwin Brandstetter Nov 17 '21 at 21:14