Oracle SQL recursive query to find parent of level one - sql

suppose i have a table service :
Name | ID | PARENT_ID | LEVEL |
-------------------------------------------
s1 | 1 | null | 0 |
s2 | 2 | 1 | 1 |
s3 | 3 | 1 | 2 |
s4 | 4 | 2 | 2 |
s5 | 5 | 3 | 3 |
s6 | 6 | 4 | 3 |
and i want to get the parent of level 1 for s6(id=6) which should return s2 , is there a way to make a recursive query until a level is reached ?

You can go UP the tree instead of going down - from leaf (id = 6) to root (which in this reverse case itself would be a leaf, connect_by_isleaf = 1), and take a "parent" of that leaf using prior operator.
upd: Misunderstood your requirement about LEVEL (in Oracle hierarchical queries it is a dynamic pseudocolumn specifying hierarchical depth of a row). If you want to limit your result set to rows with a specific value of your custom pre-populated LEVEL column - you can just add it to where condition.
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE t
("NAME" varchar2(2), "ID" int, "PARENT_ID" int, "LVL" int)
;
INSERT ALL
INTO t ("NAME", "ID", "PARENT_ID", "LVL")
VALUES ('s1', 1, NULL, 0)
INTO t ("NAME", "ID", "PARENT_ID", "LVL")
VALUES ('s2', 2, 1, 1)
INTO t ("NAME", "ID", "PARENT_ID", "LVL")
VALUES ('s3', 3, 1, 2)
INTO t ("NAME", "ID", "PARENT_ID", "LVL")
VALUES ('s4', 4, 2, 2)
INTO t ("NAME", "ID", "PARENT_ID", "LVL")
VALUES ('s5', 5, 3, 3)
INTO t ("NAME", "ID", "PARENT_ID", "LVL")
VALUES ('s6', 6, 4, 3)
SELECT * FROM dual
;
Query 1:
select id as id, name as name from t
where lvl = 1
connect by id = prior parent_id
start with id = 6
Results:
| ID | NAME |
|----|------|
| 2 | s2 |

This is possible with a hierarchical query:
create table tq84_h (
id number,
parent_id number,
level_ number
);
insert into tq84_h values (1, null, 0);
insert into tq84_h values (2, 1 , 1);
insert into tq84_h values (3, 1 , 2);
insert into tq84_h values (4, 2 , 2);
insert into tq84_h values (5, 3 , 3);
insert into tq84_h values (6, 4 , 3);
select
parent_id
from
tq84_h
where
level_ = 2
start with
id = 6
connect by
prior parent_id = id and
level_>1
;

Related

What is the best possible implementation for the following recursively query?

I got a table with the following struct representing a file system.
Every item, might be a file or a folder, has a unique id. If it is a category(folder), it contains other files.
level indicates the directory depth.
|id |parent_id|is_category|level|
|:-:|: - :|: - :|: - :|
|0 | -1 | true | 0 |
|1 | 0 | true | 1 |
|2 | 0 | true | 1 |
|3 | 1 | true | 2 |
|4 | 2 | false | 2 |
|5 | 3 | true | 3 |
|6 | 5 | false | 4 |
|7 | 5 | false | 4 |
|8 | 5 | true | 4 |
|9 | 5 | false | 4 |
Task:
Fetch all subitems levels <= 3 in the folder id == 1.
The result ids should be [1,3,5]
My current implementation is recursively queries, which means, for the example above, my program would fetch id == 1 first and then find all items with is_categorh == true and level <= 3.
It doesn't feel like a efficient way.
Any advice will be appreciated.
You don't mention the database you are using so I'll assume PostgreSQL.
You can retrieve the rows you want using a single query that uses a "Recursive CTE". Recursive CTEs are implemented by several database engines, such as Oracle, DB2, PostgreSQL, SQL Server, MariaDB, MySQL, HyperSQL, H2, Teradata, etc.
The query should take a for similar to:
with recursive x as (
select * from t where id = 1
union all
select t.*
from x
join t on t.parent_id = x.id and t.level <= 3
)
select id from x
For the record, the data script I used to test it is:
create table t (
id int,
parent_id int,
level int
);
insert into t (id, parent_id, level) values (0, -1, 0);
insert into t (id, parent_id, level) values (1, 0, 1);
insert into t (id, parent_id, level) values (2, 0, 1);
insert into t (id, parent_id, level) values (3, 1, 2);
insert into t (id, parent_id, level) values (4, 2, 2);
insert into t (id, parent_id, level) values (5, 3, 3);
insert into t (id, parent_id, level) values (6, 5, 4);
insert into t (id, parent_id, level) values (7, 5, 4);
insert into t (id, parent_id, level) values (8, 5, 4);
insert into t (id, parent_id, level) values (9, 5, 4);
As others have said, recursive CTE's are a fast, and typically efficient method to retrieve the data you're looking for. If you wanted to avoid recursive CTE's, since they aren't infinitely scalable, and thus prone to erratic behavior given certain use cases, you could also take a more direct approach by implementing the recursive search via a WHILE loop. Note that this is not more efficient than the recursive CTE, but it is something that gives you more control over what happens in the recursion. In my sample, I am using Transact-SQL.
First, setup code, like #The Impaler provided:
drop table if exists
dbo.folder_tree;
create table dbo.folder_tree
(
id int not null constraint [PK_folder_tree] primary key clustered,
parent_id int not null,
fs_level int not null,
is_category bit not null constraint [DF_folder_tree_is_category] default(0),
constraint [UQ_folder_tree_parent_id] unique(parent_id, id)
);
insert into dbo.folder_tree
(id, parent_id, fs_level, is_category)
values
(0, -1, 0, 1), --|0 | -1 | true | 0 |
(1, 0, 1, 1), --|1 | 0 | true | 1 |
(2, 0, 1, 1), --|2 | 0 | true | 1 |
(3, 1, 2, 1), --|3 | 1 | true | 2 |
(4, 2, 2, 0), --|4 | 2 | false | 2 |
(5, 3, 3, 1), --|5 | 3 | true | 3 |
(6, 5, 4, 0), --|6 | 5 | false | 4 |
(7, 5, 4, 0), --|7 | 5 | false | 4 |
(8, 5, 4, 1), --|8 | 5 | true | 4 |
(9, 5, 4, 0); --|9 | 5 | false | 4 |
And then the code for implementing a recursive search of the table via WHILE loop:
drop function if exists
dbo.folder_traverse;
go
create function dbo.folder_traverse
(
#start_id int,
#max_level int = null
)
returns #result table
(
id int not null primary key,
parent_id int not null,
fs_level int not null,
is_category bit not null
)
as
begin
insert into
#result
select
id,
parent_id,
fs_level,
is_category
from
dbo.folder_tree
where
id = #start_id;
while ##ROWCOUNT > 0
begin
insert into
#result
select
f.id,
f.parent_id,
f.fs_level,
f.is_category
from
#result r
inner join dbo.folder_tree f on
r.id = f.parent_id
where
f.is_category = 1 and
(
#max_level is null or
f.fs_level <= #max_level
)
except
select
id,
parent_id,
fs_level,
is_category
from
#result;
end;
return;
end;
go
In closing, the only reason I'd recommend this approach is if you have a large number of recursive members, or need to add logging or some other process in between actions. This approach is slower in most use cases, and adds complexity to the code, but is an alternative to the recursive CTE and meets your required criteria.

SQL aggregates over 3 tables

Well, this is annoying the hell out of me. Any help would be much appreciated.
I'm trying to get a count of how many project Ids and Steps there are. The relationships are:
Projects (n-1) Pages
Pages (n-1) Status Steps
Sample Project Data
id name
1 est et
2 quia nihil
Sample Pages Data
id project_id workflow_step_id
1 1 1
2 1 1
3 1 2
4 1 1
5 2 3
6 2 3
7 2 4
Sample Steps Data
id name
1 a
2 b
3 c
4 d
Expected Output
project_id name count_steps
1 a 3
1 b 1
2 c 2
2 d 1
Thanks!
An approach to meet the expected result. See it also at SQL Fiddle
CREATE TABLE Pages
("id" int, "project_id" int, "workflow_step_id" int)
;
INSERT INTO Pages
("id", "project_id", "workflow_step_id")
VALUES
(1, 1, 1),
(2, 1, 1),
(3, 1, 2),
(4, 1, 1),
(5, 2, 3),
(6, 2, 3),
(7, 2, 4)
;
CREATE TABLE workflow_steps
("id" int, "name" varchar(1))
;
INSERT INTO workflow_steps
("id", "name")
VALUES
(1, 'a'),
(2, 'b'),
(3, 'c'),
(4, 'd')
;
CREATE TABLE Projects
("id" int, "name" varchar(10))
;
INSERT INTO Projects
("id", "name")
VALUES
(1, 'est et'),
(2, 'quia nihil')
;
Query 1:
select pg.project_id, s.name, pg.workflow_step_id, ws.count_steps
from (
select distinct project_id, workflow_step_id
from pages ) pg
inner join (
select workflow_step_id, count(*) count_steps
from pages
group by workflow_step_id
) ws on pg.workflow_step_id = ws.workflow_step_id
inner join workflow_steps s on pg.workflow_step_id = s.id
order by project_id, name, workflow_step_id
Results:
| project_id | name | workflow_step_id | count_steps |
|------------|------|------------------|-------------|
| 1 | a | 1 | 3 |
| 1 | b | 2 | 1 |
| 2 | c | 3 | 2 |
| 2 | d | 4 | 1 |

Select objects from table where ids in set and sort result the same way as those ids are sorted

Let's assume I have a table table_data with serial id and text name.
select * from table_data where id in (3, 1, 5, 6, 2);
Result
id | name
6 | name6
5 | name5
1 | name1
3 | name3
2 | name2
But I wanted the result to be sorted as these ids.
id | name
3 | name3
1 | name1
5 | name5
6 | name6
2 | name2
These ids can be anything, they are retrieved dynamically before this query.
I would appreciate your help and advice.
Should work with a CASE in the order by:
SELECT *
FROM table_data
ORDER BY case id when 3 then 1
when 1 then 2
when 5 then 3
when 6 then 4
when 2 then 5
end
CREATE TABLE table_data (id INTEGER NOT NULL PRIMARY KEY , ztext text);
INSERT INTO table_data(id, ztext) VALUES
(1, 'One'), (2, 'Two'), (3, 'Three'), (4, 'Four'), (5, 'Five'), (6, 'Six');
WITH vals(val) AS (
VALUES (3), ( 1), ( 5), ( 6), ( 2)
)
, list(id,rnk) AS (
SELECT val
, row_number() OVER () AS rnk
FROM vals
)
SELECT t.id,t.ztext
FROM table_data t
JOIN list l ON l.id = t.id
ORDER BY l.rnk
;
Note: this relies on the ordering of items within a VALUES() set, which is probably not guaranteed. A better solution would be to use a set of pairs.

Change Postgres select with multiple array_agg and group by

I write SQL in postgres 9.3 which works almost perfectly:
SELECT type_id, to_json(array_agg(row(value, id))) AS json FROM sub_types GROUP BY type_id
The result table looks:
type_id | json
1 | [{"f1":"something", "f2":7}, ...]
2 | [{"f1":"something new", "f2":2}, ...]
I am trying to do that the result looks like:
type_id | json
1 | [{"value":"something", "id":7}, ...]
2 | [{"value":"something new", "id":2}, ...]
Basic idea is to to write code (PHP) something close to this:
rows = pdo_call_select
rows = pdo_call_select
foreach (rows as row)
{
print '<span data-id="row->id">'
foreach (row->json as otherfields)
print '<input value="otherfields->value" ...'
...
and my table is:
id | type_id | value
1 3 something
2 2 blabla
3 3 something new
4 1 ok
...
create table sub_types (
id int, type_id int, value text
);
insert into sub_types (id, type_id, value) values
(1, 3, 'something'),
(2, 2, 'blabla'),
(3, 3, 'something new'),
(4, 1, 'ok');
select type_id, json_agg(row_to_json(cj)) as json
from
sub_types st
cross join lateral
(select value, id) cj
group by type_id
;
type_id | json
---------+------------------------------------------------------------------
1 | [{"value":"ok","id":4}]
3 | [{"value":"something","id":1}, {"value":"something new","id":3}]
2 | [{"value":"blabla","id":2}]
I create types for all my json results and cast the rows to the type.
create table sub_types (
id int, type_id int, value text
);
create type sub_json_type as (value text, id integer);
insert into sub_types (id, type_id, value) values
(1, 3, 'something'),
(2, 2, 'blabla'),
(3, 3, 'something new'),
(4, 1, 'ok');
SELECT type_id, to_json(array_agg(row(value, id)::sub_json_type)) AS json FROM sub_types GROUP BY type_id;
type_id | json
---------+-----------------------------------------------------------------
1 | [{"value":"ok","id":4}]
2 | [{"value":"blabla","id":2}]
3 | [{"value":"something","id":1},{"value":"something new","id":3}]
(3 rows)

Get id of max value in group

I have a table and i would like to gather the id of the items from each group with the max value on a column but i have a problem.
SELECT group_id, MAX(time)
FROM mytable
GROUP BY group_id
This way i get the correct rows but i need the id:
SELECT id,group_id,MAX(time)
FROM mytable
GROUP BY id,group_id
This way i got all the rows. How could i achieve to get the ID of max value row for time from each group?
Sample Data
id = 1, group_id = 1, time = 2014.01.03
id = 2, group_id = 1, time = 2014.01.04
id = 3, group_id = 2, time = 2014.01.04
id = 4, group_id = 2, time = 2014.01.02
id = 5, group_id = 3, time = 2014.01.01
and from that i should get id: 2,3,5
Thanks!
Use your working query as a sub-query, like this:
SELECT `id`
FROM `mytable`
WHERE (`group_id`, `time`) IN (
SELECT `group_id`, MAX(`time`) as `time`
FROM `mytable`
GROUP BY `group_id`
)
Have a look at the below demo
DROP TABLE IF EXISTS mytable;
CREATE TABLE mytable(id INT , group_id INT , time_st DATE);
INSERT INTO mytable VALUES(1, 1, '2014-01-03'),(2, 1, '2014-01-04'),(3, 2, '2014-01-04'),(4, 2, '2014-01-02'),(5, 3, '2014-01-01');
/** Check all data **/
SELECT * FROM mytable;
+------+----------+------------+
| id | group_id | time_st |
+------+----------+------------+
| 1 | 1 | 2014-01-03 |
| 2 | 1 | 2014-01-04 |
| 3 | 2 | 2014-01-04 |
| 4 | 2 | 2014-01-02 |
| 5 | 3 | 2014-01-01 |
+------+----------+------------+
/** Query for Actual output**/
SELECT
id
FROM
mytable
JOIN
(
SELECT group_id, MAX(time_st) as max_time
FROM mytable GROUP BY group_id
) max_time_table
ON mytable.group_id = max_time_table.group_id AND mytable.time_st = max_time_table.max_time;
+------+
| id |
+------+
| 2 |
| 3 |
| 5 |
+------+
When multiple groups may contain the same value, you could use
SELECT subq.id
FROM (SELECT id,
value,
MAX(time) OVER (PARTITION BY group_id) as max_time
FROM mytable) as subq
WHERE subq.time = subq.max_time
The subquery here generates a new column (max_time) that contains the maximum time per group. We can then filter on time and max_time being identical. Note that this still returns multiple rows per group if the maximum value occurs multiple time within the same group.
Full example:
CREATE TABLE test (
id INT,
group_id INT,
value INT
);
INSERT INTO test (id, group_id, value) VALUES (1, 1, 100);
INSERT INTO test (id, group_id, value) VALUES (2, 1, 200);
INSERT INTO test (id, group_id, value) VALUES (3, 1, 300);
INSERT INTO test (id, group_id, value) VALUES (4, 2, 100);
INSERT INTO test (id, group_id, value) VALUES (5, 2, 300);
INSERT INTO test (id, group_id, value) VALUES (6, 2, 200);
INSERT INTO test (id, group_id, value) VALUES (7, 3, 300);
INSERT INTO test (id, group_id, value) VALUES (8, 3, 200);
INSERT INTO test (id, group_id, value) VALUES (9, 3, 100);
select * from test;
id | group_id | value
----+----------+-------
1 | 1 | 100
2 | 1 | 200
3 | 1 | 300
4 | 2 | 100
5 | 2 | 300
6 | 2 | 200
7 | 3 | 300
8 | 3 | 200
9 | 3 | 100
(9 rows)
SELECT subq.id
FROM (SELECT id,
value,
MAX(value) OVER (partition by group_id) as max_value
FROM test) as subq
WHERE subq.value = subq.max_value;
id
----
3
5
7
(3 rows)