简体   繁体   中英

SQL query to return matched rows with their ancestors in tree-like structure

I have an Oracle 12c database with a table with tree-like structure for organisational units (departments and so on):

CREATE TABLE "OUS" (
  "ID" NUMBER(38,0) NOT NULL ENABLE, 
  "NAME" VARCHAR2(255 CHAR) NOT NULL ENABLE, 
  "PARENT_ID" NUMBER(38,0),
  PRIMARY KEY("ID"),
  CONSTRAINT "OUS_HIERARCHY_FK" FOREIGN KEY ("PARENT_ID") REFERENCES "OUS" ("ID") ON DELETE CASCADE
);

So, having structure like

| id | name          | parent_id |
| -: | ------------- | --------: |
|  1 | Root          |    (NULL) |
|  2 | Territorial 1 |         1 |
|  3 | Regional 1-1  |         2 |
|  4 | Alpha dept    |         3 |
|  5 | Beta dept     |         3 |
|  6 | Regional 1-2  |         2 |
|  7 | Gamma dept    |         6 |
|  8 | Delta dept    |         7 |
|  9 | Territorial 2 |         1 |
| 10 | Regional 2-1  |         9 |
| 11 | Epsilon dept  |        10 |
| 12 | Zeta dept     |        10 |

You can create it with SQL like:

INSERT INTO ous (id, name, parent_id) VALUES ( 13, 'Root',          NULL);
INSERT INTO ous (id, name, parent_id) VALUES (  2, 'Territorial 1',   13);
INSERT INTO ous (id, name, parent_id) VALUES (  1, 'Regional 1-1',     2);
INSERT INTO ous (id, name, parent_id) VALUES (  5, 'Alpha dept',       1);
INSERT INTO ous (id, name, parent_id) VALUES (  4, 'Beta dept',        1);
INSERT INTO ous (id, name, parent_id) VALUES (  6, 'Regional 1-2',     2);
INSERT INTO ous (id, name, parent_id) VALUES (  7, 'Gamma dept',       6);
INSERT INTO ous (id, name, parent_id) VALUES (  8, 'Delta dept',       6);
INSERT INTO ous (id, name, parent_id) VALUES (  9, 'Territorial 2',   13);
INSERT INTO ous (id, name, parent_id) VALUES (  3, 'Regional 2-1',     9);
INSERT INTO ous (id, name, parent_id) VALUES ( 15, 'Epsilon dept',     3);
INSERT INTO ous (id, name, parent_id) VALUES ( 12, 'Zeta dept',        3);

I need to found some of OU, matching given criteria (like name = 'Alpha' OR name = 'Epsilon ) and get a subtree of these OU and their ancestors.

For example:

| id | name          | parent_id |
| -: | ------------- | --------: |
|  1 | Root          |    (NULL) | ← Ancestor of Alpha and Epsilon
|  2 | Territorial 1 |         1 | ← Ancestor of Alpha
|  3 | Regional 1-1  |         2 | ← Ancestor of Alpha
|  4 | Alpha dept    |         3 | ← Matches the WHERE clause!
|  9 | Territorial 2 |         1 | ← Ancestor of Epsilon
| 10 | Regional 2-1  |         9 | ← Ancestor of Epsilon
| 11 | Epsilon dept  |        10 | ← Matches the WHERE clause!

I'm looked at various Hierarchical and recursive queries in SQL : Oracle Hierarchical queries and CTEs , but can't figure out a query that can return me such a result.

I'm using Oracle Database 12c.

I've tried queries like:

SELECT ous.* FROM ous
WHERE name = 'Alpha' OR name = 'Epsilon'
START WITH 
  parent_id IS NULL
CONNECT BY
  PRIOR id = parent_id 
ORDER SIBLINGS BY name;

But it returns 0 rows as WHERE is applied to all rows (so ancestors are being filtered)

Also I've tried:

WITH RECURSIVE all_nodes (id, parent_id, name) AS (
  SELECT ous.id, ous.parent_id, name FROM ous WHERE (name = 'Alpha' OR name = 'Epsilon')
  UNION
  SELECT ous.id, ous.parent_id, name FROM ous INNER JOIN all_nodes ON ous.parent_id = all_nodes.id
)
SELECT * FROM all_nodes INNER JOIN ous ON all_nodes.id = ous.id ORDER BY name;

But it returns error SQL Error [905] [42000]: ORA-00905: keyword is missing

You can do this with a recursive CTE:

with t(name, id, parent_id) as (
      select name, id, parent_id
      from ous
      where name in ('alpha', 'epsilon')
      union all
      select ous.name, ous.id, ous.parent_id
      from t join
           ous
           on ous.id = t.parent_id
    )
select distinct t.id, t.name, t.parent
from t
order by t.id;

The select distinct is probably not necessary.

Recursive CTEs have the advantage of being standard SQL, so the logic is supported by many different databases.

Of course you can use a hierarchical query for this. The problem is that if you start with multiple leaves, at some point you will start getting duplicate rows. You can remove the duplicates with "distinct" but this will impair performance, especially on a very large table or if you start with too many leaves. At the end of the day, a recursive query is often more difficult to write, but more efficient than a hierarchical query.

For completeness, here is the hierarchical solution. Using the EMP table in the SCOTT schema for illustration. First showing the straight hierarchical query (and the duplicates in the output) and then the version with "distinct."

select empno, mgr
from   scott.emp
start with empno in (7902, 7788)
connect by prior mgr = empno
;

     EMPNO        MGR
---------- ----------
      7788       7566
      7566       7839
      7839
      7902       7566
      7566       7839
      7839


select distinct empno, mgr
from   scott.emp
start with empno in (7902, 7788)
connect by prior mgr = empno
;

     EMPNO        MGR
---------- ----------
      7839
      7566       7839
      7902       7566
      7788       7566

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM