Build AST with recursive CTE in Postgres - sql

Given following table:
create table tree
(
id int,
parent_id int REFERENCES tree(id),
operator varchar,
primary key(id)
);
insert into tree values
(1, null, 'AND'),
(2, 1, 'NOT'),
(3, 1, 'OR'),
(4, 2, 'AND'),
(5, 3, 'Y'),
(6, 3, 'Z'),
(7, 4, 'K'),
(8, 4, 'OR'),
(9, 8, 'X'),
(10, 8, 'A'),
(11, 8, 'B')
;
how could I compute final AST: AND(NOT(AND(K, OR(X, A, B))), OR(Y, Z))
I tried different approaches with recursive CTE, but my problem is that CTE neither allows aggregations in recursive part of CTE nor subqueries where CTE is used.
Latest thing I tried was this:
with RECURSIVE tree1(id, parent_id, operator, n, cc) as (
select t.*, 0, cc.cc
from tree t, lateral(select count(*) as cc from tree tt where tt.parent_id = t.id) cc
where t.parent_id is null
union ALL
select t.*, n + 1, cc.cc
FROM tree t INNER JOIN tree1 a on t.parent_id = a.id, lateral(select count(*) as cc from tree tt where tt.parent_id = t.id) cc
), tree2(id, parent_id, operator, n, cc) AS (
select t.id, t.parent_id, t.operator, t.n, t.cc
from tree1 t
where t.n = (select max(n) from tree1)
union (
select p.id, p.parent_id, p.operator || '(' || string_agg(c.operator, ', ') || ')' as operator, p.n, p.cc
from tree1 p, tree2 c
where p.id = c.parent_id
group by p.id, p.parent_id, p.operator, p.n, p.cc
union
select p.id, p.parent_id, p.operator, p.n, p.cc
from tree1 p, tree2 c
where p.cc = 0 and p.n + 1 = c.n
)
)
select * from tree2
but it didn't work due to limitations of CTE.
The docs says CTE is Turing complete yet I can't find way to compute desired result. Am I missing something or is my understanding of Turing completeness wrong? :)
(I have Postgres 9.6)

Related

How to get a recursive tree for a single table element

I have a table of this type
| id | parent_id | | title |
parent_id refers to the id of the same table
I need to get a recursive tree for an element knowing only its parent.
it will be clearer what I mean in the picture
On the picture i need to get recursive parent tree for element E, (ะก id is known) i need get A - C - E tree without B and D and other elements, only for my element E
The nesting can be even greater, I just need to get all the parents in order without unnecessary elements.
This is needed for bread crumbs on my website
How i can do this in PostgreSQL?
Use RECURSIVE query
with recursive rec(id,parent_id, title) as (
select id,parent_id, title from t
where title = 'E'
union all
select t.*
from rec
join t on t.id = rec.parent_id
)
select * from rec
id|parent_id|title|
--+---------+-----+
5| 3|E |
3| 1|C |
1| |A |
Join your table on herself
SELECT t1.title, t2.title as parent, t3.title as great_parent, ...
FROM my_table t1
JOIN my_table t2 on t1.parent_id = t2.id
JOIN my_table t3 on t2.parent_id = t3.id
...
WHERE t1.title = 'curent'
if you don't know how many parent you have, use LEFT JOIN and do as mutch column as needed
thanks to Marmite Bomber
and with a small improvement to know the kinship level :
--drop table if exists recusive_test ;
create table recusive_test (id_parent integer, id integer, title varchar);
insert into recusive_test (id_parent , id , title) values
(1, 2, 'A')
,(2, 3, 'B')
,( 2, 4, 'C')
,( 4, 5, 'D')
,( 3, 6, 'E')
,( 3, 7, 'F')
,( 6, 8, 'G')
,( 6, 9, 'H')
,( 4, 10, 'I')
,( 4, 11, 'J');
WITH RECURSIVE search_tree(id, id_parent, title, step) AS (
SELECT t.id, t.id_parent, t.title ,1
FROM recusive_test t
where title = 'I'
UNION ALL
SELECT t.id, t.id_parent, t.title, st.step+1
FROM recusive_test t, search_tree st
WHERE t.id = st.id_parent
)
SELECT * FROM search_tree ORDER BY step DESC;

Recursive select that selects rows based own plus childrens values

I need to select rows in a table like this:
Select all rows in the table where both conditions are met:
Condition 1: the value column should not match with any value in table v
Condition 2: no decendent (on any level, ie: child or sub child, sub- sub- child etc) has a value that matches with any value in table v
Table v looks like this:
Expected result from example table. Should [row] be selected/returned?
a1: No - condition 2
a2: No - condition 2
a3: No - condition 1
a4: No - condition 1
a5: Yes - (value does not match in v and no decendents that match in v)
a6: Yes - (value does not match in v and no decendents that match in v)
a7: Yes - (value does not match in v and no decendents that match in v)
a8: Yes - (value does not match in v and no decendents that match in v)
Here is an sqlfiddle where the tables are set up together with a recursive function that shows all rows and their level in the tree, but that I don't know how to procede with:
http://sqlfiddle.com/#!18/736a28/15/0
Check this solution:
--------------------------- DDL+DML
drop table if exists a
drop table if exists v
GO
CREATE TABLE a
([id] varchar(13), [parentId] varchar(57), [value] varchar(57))
;
CREATE TABLE v
([id] varchar(13), [value] varchar(57))
;
INSERT INTO a
([id], [parentId], [value])
VALUES
('a1', NULL, NULL),
('a2', 'a1', NULL),
('a3', 'a2', '1'),
('a4', NULL, '5'),
('a5', 'a1', '8'),
('a6', 'a2', NULL),
('a7', NULL, NULL),
('a8', NULL, '3'),
('a9', 'a8', '7')
;
INSERT INTO v
([id], [value])
VALUES
('v1', '1'),
('v2', '5'),
('v3', '10'),
('v4', '15'),
('v5', '20'),
('v6', '25'),
('v7', '30'),
('v8', '35'),
('v9', '40')
;
SELECT * FROM a
SELECT * FROM v
GO
-------------------- Solution
WITH MyRecCTE AS(
SELECT a.id, a.parentId, a.[value], Res = 'NO'
FROM a
INNER JOIN v ON a.[value] = v.[value]
UNION ALL
SELECT
a.id, a.parentId, a.[value], Res = 'NO'
FROM a
INNER JOIN MyRecCTE c ON c.parentId = a.id
)
SELECT DISTINCT a.id, a.parentId,a.[value], ISNULL(CONVERT(VARCHAR(3),c.Res),'YES')
FROM a
LEFT JOIN MyRecCTE c ON a.id = c.id
ORDER BY id
GO
Result Set (fits requested)):
For the sake of the discussion let's add another row which lead rows with id a8 and a9 to be "NO" since it is child of a9 and has value from the second table
INSERT INTO a
([id], [parentId], [value])
VALUES
('a10', 'a9', 35)
GO
test 2 Result set (fits expected)
This got somewhat complicated, but I created a CTE where there is a record that contains a Path for every combination of ancestor and descendant (transitive closure). Then, I create a second CTE where I extract the parent id from the beginning of Path and the descendant id from the end of Path and look up the descendant's value. Then, finally, I query the second CTE and use NOT EXISTS to filter the rows.
WITH tree
AS
(
SELECT a.id, a.parentId, a.value,
CAST('/' + a.id as varchar(1000)) as Path
FROM a
UNION ALL
SELECT a.id, a.parentId, a.value,
CAST(t.Path + '/' + a.id as varchar(1000)) as Path
FROM a
INNER JOIN tree t
ON Path LIKE '%/' + a.parentId
),
DT
AS
(
SELECT t.Path,
RIGHT(LEFT(t.Path,3),2) as parent_id,
RIGHT(t.Path,2) as descendant_id,
(SELECT q.[value]
FROM a q
WHERE q.id = RIGHT(t.Path,2)
) as [descendant_value]
FROM tree t
)
SELECT *
FROM DT dt_outer
WHERE NOT EXISTS (SELECT 1 FROM DT dt_inner WHERE dt_inner.parent_id = dt_outer.parent_id AND
dt_inner.descendant_value IN (SELECT [value] FROM v))
ORDER BY 2,3
I left the result set with duplicates to get a clearer picture of what's going on. You can finish up with a DISTINCT parent_id to get the unique ids.
SQL Fiddle

Get root and top level from SQL tree

I have a tree table. And, I am going to get root and top level on this tree.
Help with the solution you can use anything you want
declare #disc table (
id int,
parent int,
label varchar(50)
)
insert into #disc
select *
from (
values (1, null, 'q_1'),
(2, 1, 'a_1_1'),
(3, 2, 'a_1_1_1'),
(4, 1, 'a_1_2'),
(5, null, 'q_5'),
(6, 5, 'a_5_1'),
(7, 5, 'a_5_2')
) x (id, parent, label);
1. q_1
2. a_1_1
3. a_1_1_1
4. a_1_2
5. q_5
6. a_5_1
7. a_5_2
And, my result should be like this:
1: 1, null, q_1
2: 2, 1, a_1_1
3: 5, null, q_5
4: 6, 5, a_5_1
or
1: 1, null, q_1
2: 5, null, q_5
3: 2, 1, a_1_1
4: 6, 5, a_5_1
I only found one way, but I believe there is a better solution:
with rec as (
select id, parent, label,
row_number() over(order by id) rnk,
1 lvl
from #disc
where parent is null
union all
select d.id, d.parent, d.label,
row_number() over(order by d.id) rnk,
r.lvl + 1
from rec r
join #disc d on r.id = d.parent
)
select *
from rec
where parent is null or (rnk = 1 and lvl = 2)
If I understand this, the parent value will be null in the root nodes. The next level down will have a root node as parent. So ...
;with roots as
(
select id, parent, label
from #disc
where parent is null
)
select id, parent, label
from roots
union
select id, parent, label
from #disc
where parent in (select id from roots)
It doesn't look like you actually want to recurse here.
You can just do a self-join inside an apply.
select
row_number() over (order by isnull(c.parent, c.id), c.id),
c.id,
c.parent,
c.label
from #disc p
cross apply (
select p.id, p.parent, p.label
union all
select top 1 c.id, c.parent, c.label
from #disc c
where p.id = c.parent
order by c.id
) c
where p.parent is null;
db<>fiddle

Joining two tables and finding the earliest date SQL

I have the following two tables and I need to get the following result:
Table 1
(A, 1, 01/01/2015),
(A, 1, 10/01/2015),
(A, 2, 20/01/2015),
(A, 2, 01/05/2015),
(B, 1, 20/02/2014),
(B, 1, 20/02/2015),
(B, 2, 20/02/2016),
(B, 2, 06/05/2015)
Table 2
(A, 1, 123),
(A, 1, 123),
(A, 2, 234),
(A, 2, 234),
(B, 1, 123),
(B, 2, 123),
I want to return the earliest date of each distinct combo:
(A, 123, 01/01/2015),
(A, 234, 20/01/2015),
(B, 123, 20/02/2014)
Code I have tried:
DECLARE #table1 TABLE (letter1 CHAR(1), num1 INT, date1 INT)
DECLARE #table2 TABLE (letter1 CHAR(1), num1 INT, num2 INT)
INSERT INTO #table1 VALUES
('A', 1, 01012015),
('A', 1, 10012015),
('A', 2, 20012015),
('A', 2, 01052015),
('B', 1, 20022014),
('B', 1, 20022015),
('B', 2, 20022016),
('B', 2, 06052015)
INSERT INTO #table2 VALUES
('A', 1, 123),
('A', 1, 123),
('A', 2, 234),
('A', 2, 234),
('B', 1, 123),
('B', 2, 123)
SELECT DISTINCT [#table1].letter1, num2, MIN(date1) FROM #table1
INNER JOIN #table2 ON [#table1].letter1 = [#table2].letter1 AND [#table1].num1 = [#table2].num1
GROUP BY [#table1].letter1, [#table1].num1, num2
You can use row_number() function :
select top (1) with ties t.letter1, t2.num2, t.date1
from table1 t inner join
table2 t2
on t2.letter1 = t.letter1 AND t2.num1 = t.num1
order by row_number() over (partition by t2.letter1, t2.num2 order by t.date1 desc);
Just giving a try . may be add date1 in group by cluase
SELECT DISTINCT [#table1].letter1, num2, MIN(date1) FROM #table1 INNER JOIN #table2 ON [#table1].letter1 = [#table2].letter1 AND [#table1].num1 = [#table2].num1 GROUP BY [#table1].letter1, [#table1].num1, num2,date1
;with cte as
(
select name, IIF(c = 1, 0, id) id, value from --- Here We separet the whole into two groups.
(
select name, id, value, count(*) c from #table2 group by name, id, value
) ct ---- Here one group (B) has same value (123).
---- And another group (A) have diff values (123,234))
)select c.name, c.value, min(t1.yyymmdd) from cte c
join #table1 t1
on c.name = t1.name
and c.id = t1.id ------ Id's join must. Because it has two different ids
and c.id <> 0 ------ 'A' group has been joined
group by c.name, value union
select c.name, c.value, min(t1.yyymmdd) Earlier_date from cte c
join #table1 t1
on c.name = t1.name ------ Id's join is no need. Because it has one id.
and c.id = 0 ------ 'B' group has been joined
group by c.name, value

Find the users having more than two elements and one of those elements must be A

I want to extract the users having more than two elements and one of those elements must be A.
This my table:
CREATE TABLE #myTable(
ID_element nvarchar(30),
Element nvarchar(10),
ID_client nvarchar(20)
)
This is the data of my table:
INSERT INTO #myTable VALUES
(13 ,'A', 1),(14 ,'B', 1),(15 ,NULL, 1),(16 ,NULL, 1),
(17 ,NULL, 1),(18 ,NULL, 1),(19 ,NULL, 1),(7, 'A', 2),
(8, 'B', 2),(9, 'C', 2),(10 ,'D', 2),(11 ,'F', 2),
(12 ,'G', 2),(1, 'A', 3),(2, 'B', 3),(3, 'C', 3),
(4, 'D', 3),(5, 'F', 3),(6, 'G', 3),(20 ,'Z', 4),
(22 ,'R', 4),(23 ,'D', 4),(24 ,'F', 5),(25 ,'G', 5),
(21 ,'x', 5)
And this is my query:
Select Distinct ID_client
from #myTable
Group by ID_client
Having Count(Element) > 2
Add to your query CROSS APPLY with id_clients that have element A
SELECT m.ID_client
FROM #myTable m
CROSS APPLY (
SELECT ID_client
FROM #myTable
WHERE ID_client = m.ID_client
AND Element = 'A'
) s
GROUP BY m.ID_client
HAVING COUNT(DISTINCT m.Element) > 2
Output:
ID_client
2
3
I think this is what you are looking for:
SELECT * FROM
(SELECT *, RANK() OVER (PARTITION BY element ORDER by id_client) AS grouped FROM #myTable) t
wHERE grouped > 1
AND Element = 'A'
ORDER by t.element
which brings back
ID_element Element ID_client grouped
7 A 2 2
1 A 3 3
You can select the ID_client values which have an 'A' as an Element and join your table with the result of that:
SELECT m.ID_Client
FROM #myTable AS m
JOIN (
SELECT a.ID_Client FROM #myTable AS a
WHERE a.Element = 'A') AS filteredClients
ON m.ID_client = filteredClients.ID_client
GROUP BY m.ID_client
HAVING COUNT(m.Element) > 2
Outputs:
ID_Client
2
3
However, this is not necessarily the best way to do it: When should I use Cross Apply over Inner Join?