How to generate Tree path using traverse CTE - sql

Suppose my data in table like in attached picture without having path column.
i want to generate a column, like "Path" in picture using traverse CTE in sql
Picture example:

Use a recursive CTE to solve this:
WITH recCTE
AS (
SELECT id,
parentid,
id AS original_id,
parentid AS original_parentid,
name as original_name,
1 AS depth,
CAST(name AS VARCHAR(5000)) AS path
FROM yourtable
UNION ALL
SELECT yourtable.id,
yourtable.parentid,
recCTE.original_id,
recCTE.original_parentID,
recCTE.original_name,
recCTE.depth + 1,
CAST(recCTE.path + '-' + yourtable.name as VARCHAR(5000))
FROM recCTE
INNER JOIN yourtable
ON recCTE.parentid = yourtable.id
WHERE depth < 20 /*prevent cycling*/
)
SELECT original_id as id, original_parentid as parentid, original_name as name, depth, path
FROM recCTE t1
WHERE depth = (SELECT max(depth) FROM recCTE WHERE t1.original_id = recCTE.original_id)
sqlfiddle example
That CTE has two parts:
The "Anchor Member" which is the first selection from the table. This defines the output (which columns and column type are in the output).
The "Recursive Member" which selects from the CTE in which it's contained at is performed iteratively until the join fails.
In this example we capture the path by concatenating the path to the name over and over again in the recursive member. We also track Depth (how many recursions have been performed) and track the current id and parentid as well as the original id and original parentid so they can be selected in the final SELECT statement.

try this,
;with cte(id,parentId,name,path,cnt)
AS
(
select id,parentid,name,cast(name as VARCHAR(1024)) as path, 1 as cnt from test_cte
union all
select a.id,a.parentid,a.name,CAST((a.name + '-' +path ) as VARCHAR(1024)), case when a.parentid is null then 0 else cnt + 1 end as cnt from test_cte a join cte c on c.id = a.parentid where c.cnt is not null
)
select id,parentid,name,path from (select id,parentid,name,path, row_number() over(partition by id order by cnt desc) as rank from cte) a where a.rank = 1 order by 1 asc ;

Related

How to get last record from Master-Details tables

I have a table that has 3 columns.
create table myTable
(
ID int Primary key,
Detail_ID int references myTable(ID) null, -- reference to self
Master_Value varchar(50) -- references to master table
)
this table has the follow records:
insert into myTable select 100,null,'aaaa'
insert into myTable select 101,100,'aaaa'
insert into myTable select 102,101,'aaaa'
insert into myTable select 103,102,'aaaa' ---> last record
insert into myTable select 200,null,'bbbb'
insert into myTable select 201,200,'bbbb'
insert into myTable select 202,201,'bbbb' ---> last record
the records is saved In the form of relational with ID and Detail_ID columns.
I need to select the last record each Master_Value column. follow output:
lastRecordID Master_Value Path
202 bbbb 200=>201=>202
103 aaaa 100=>101=>102=>103
tips:
The records are not listed in order in the table.
I can not use the max(ID) keyword. beacuse data is not sorted.(may
be the id column updated manually.)
attempts:
I was able to Prepare follow query and is working well:
with Q as
(
select ID ,Detail_ID, Master_Value , 1 RowOrder, CAST(id as varchar(max)) [Path] from myTable where Detail_ID is null
union all
select R.id,R.Detail_ID , r.Master_Value , (q.RowOrder + 1) RowOrder , (q.[Path]+'=>'+CAST(r.id as varchar(max))) [Path] from myTable R inner join Q ON Q.ID=R.Detail_ID --where r.Dom_ID_RowType=1010
)
select * into #q from Q
select Master_Value, MAX(RowOrder) lastRecord into #temp from #Q group by Master_Value
select
q.ID lastRecordID,
q.Master_Value,
q.[Path]
from #temp t
join #q q on q.RowOrder = t.lastRecord
where
q.Master_Value = t.Master_Value
but I need to simple way (one select) and optimal method.
Can anyone help me?
One method uses a correlated subquery to get the last value (which is how I interpreted your question):
select t.*
from mytable t
where not exists (select 1
from mytable t2
where t2.master_value = t.master_value and
t2.id = t.detail_id
);
This returns rows that are not referred to by another row.
For the path, you need a recursive CTE:
with cte as (
select master_value, id as first_id, id as child_id, convert(varchar(max), id) as path, 1 as lev
from mytable t
where detail_id is null
union all
select cte.master_value, cte.first_id, t.id, concat(path, '->', t.id), lev + 1
from cte join
mytable t
on t.detail_id = cte.child_id and t.master_value = cte.master_value
)
select cte.*
from (select cte.*, max(lev) over (partition by master_value) as max_lev
from cte
) cte
where max_lev = lev
Here is a db<>fiddle.

SQL Server 2012 - Convert a From-To columned table into a numbered sequence

I am trying to write a SQL query in SQL Server 2012 to convert a table that has 2 columns FROM and TO into a table that has a numerical sequence of the described route taken if you logically follow the from-to direction. I have been struggling for hours on this and any hints would be greatly appreciated. Thanks
EXAMPLE:
To answer this I'll have to assume either a sort order or a "base record". I.e. I need to know which record should be the starting point. In this example I have simply hardcoded the value of the base record.
WITH RecursiveCTE AS(
SELECT
[FROM], [TO],
1 AS SequenceNo
FROM InputTable
WHERE [FROM] = 'B' --Hardcoded value to select a base record
UNION ALL
SELECT
t.[FROM], t.[TO],
SequenceNo + 1 AS SequenceNo
FROM RecursiveCTE e
INNER JOIN InputTable t ON e.[TO] = t.[FROM]
)
--Get all records except the last one via the Recursive CTE
SELECT
[FROM] AS [Node],
SequenceNo
FROM RecursiveCTE
UNION ALL
--Get the last record in a separate query
SELECT TOP 1
[TO] AS [Node],
SequenceNo + 1 AS SequenceNo
FROM RecursiveCTE
WHERE SequenceNo = (SELECT MAX(SequenceNo) FROM RecursiveCTE)
The query uses a recursive CTE to get all records except the last one. The last record is added by the UNION ALL statement at the end.
Assuming no cycles, you can use a recursive cte:
with cte as (
select e.from, e.to, 1 as lev
from example e
where not exists (select 1 from example e2 where e2.to = e.from)
union all
select e.from, e.to, cte.lev + 1
from cte join
example e
on cte.to = e.from
)
select e.from, lev
from cte;
I assumed that you have random data on [from] or [to] column - (may be there more data in [from] column not in [to] column and vise-versa, so
if there is no problem with making an order using [from] and [to] column use the below query
with dataTest as
(
select [From], [to] , ROW_NUMBER() over(order by [From], [to]) lvl from InputTable e
)
,dataD as
(
select [From] node,(lvl*2)-1 lvl from dataTest
union all
select [To] node,(lvl*2) lvl from dataTest
)
,lastD as
(
select node, lvl, ROW_NUMBER() over(partition by node order by lvl) rn from dataD
)
select node, ROW_NUMBER() over(order by lvl) squence from lastD where rn=1 order by lvl
if your data is typically as you set in the sample you can use
with dataTest as
(
select [From], [to] ,1 lvl from InputTable e
where not exists (select 1 from InputTable e2 where e2.[to] = e.[from])
union all
select e.[from], e.[to], dataTest.lvl + 1
from dataTest join
InputTable e on dataTest.[to] = e.[from]
)
,dataD as
(
select [From] node,(lvl*2)-1 lvl from dataTest
union all
select [To] node,(lvl*2) lvl from dataTest
)
,lastD as
(
select node, lvl, ROW_NUMBER() over(partition by node order by lvl) rn
from dataD
)
select node, ROW_NUMBER() over(order by lvl) squence from lastD where rn=1 order by lvl
Hope these help you or give you an idea.

concatenate recursive cross join

I need to concatenate the name in a recursive cross join way. I don't know how to do this, I have tried a CTE using WITH RECURSIVE but no success.
I have a table like this:
group_id | name
---------------
13 | A
13 | B
19 | C
19 | D
31 | E
31 | F
31 | G
Desired output:
combinations
------------
ACE
ACF
ACG
ADE
ADF
ADG
BCE
BCF
BCG
BDE
BDF
BDG
Of course, the results should multiply if I add a 4th (or more) group.
Native Postgresql Syntax:
SqlFiddleDemo
WITH RECURSIVE cte1 AS
(
SELECT *, DENSE_RANK() OVER (ORDER BY group_id) AS rn
FROM mytable
),cte2 AS
(
SELECT
CAST(name AS VARCHAR(4000)) AS name,
rn
FROM cte1
WHERE rn = 1
UNION ALL
SELECT
CAST(CONCAT(c2.name,c1.name) AS VARCHAR(4000)) AS name
,c1.rn
FROM cte1 c1
JOIN cte2 c2
ON c1.rn = c2.rn + 1
)
SELECT name as combinations
FROM cte2
WHERE LENGTH(name) = (SELECT MAX(rn) FROM cte1)
ORDER BY name;
Before:
I hope if you don't mind that I use SQL Server Syntax:
Sample:
CREATE TABLE #mytable(
ID INTEGER NOT NULL
,TYPE VARCHAR(MAX) NOT NULL
);
INSERT INTO #mytable(ID,TYPE) VALUES (13,'A');
INSERT INTO #mytable(ID,TYPE) VALUES (13,'B');
INSERT INTO #mytable(ID,TYPE) VALUES (19,'C');
INSERT INTO #mytable(ID,TYPE) VALUES (19,'D');
INSERT INTO #mytable(ID,TYPE) VALUES (31,'E');
INSERT INTO #mytable(ID,TYPE) VALUES (31,'F');
INSERT INTO #mytable(ID,TYPE) VALUES (31,'G');
Main query:
WITH cte1 AS
(
SELECT *, rn = DENSE_RANK() OVER (ORDER BY ID)
FROM #mytable
),cte2 AS
(
SELECT
TYPE = CAST(TYPE AS VARCHAR(MAX)),
rn
FROM cte1
WHERE rn = 1
UNION ALL
SELECT
[Type] = CAST(CONCAT(c2.TYPE,c1.TYPE) AS VARCHAR(MAX))
,c1.rn
FROM cte1 c1
JOIN cte2 c2
ON c1.rn = c2.rn + 1
)
SELECT *
FROM cte2
WHERE LEN(Type) = (SELECT MAX(rn) FROM cte1)
ORDER BY Type;
LiveDemo
I've assumed that the order of "cross join" is dependent on ascending ID.
cte1 generate DENSE_RANK() because your IDs contain gaps
cte2 recursive part with CONCAT
main query just filter out required length and sort string
The recursive query is a bit simpler in Postgres:
WITH RECURSIVE t AS ( -- to produce gapless group numbers
SELECT dense_rank() OVER (ORDER BY group_id) AS grp, name
FROM tbl
)
, cte AS (
SELECT grp, name
FROM t
WHERE grp = 1
UNION ALL
SELECT t.grp, c.name || t.name
FROM cte c
JOIN t ON t.grp = c.grp + 1
)
SELECT name AS combi
FROM cte
WHERE grp = (SELECT max(grp) FROM t)
ORDER BY 1;
The basic logic is the same as in the SQL Server version provided by #lad2025, I added a couple of minor improvements.
Or you can use a simple version if your maximum number of groups is not too big (can't be very big, really, since the result set grows exponentially). For a maximum of 5 groups:
WITH t AS ( -- to produce gapless group numbers
SELECT dense_rank() OVER (ORDER BY group_id) AS grp, name AS n
FROM tbl
)
SELECT concat(t1.n, t2.n, t3.n, t4.n, t5.n) AS combi
FROM (SELECT n FROM t WHERE grp = 1) t1
LEFT JOIN (SELECT n FROM t WHERE grp = 2) t2 ON true
LEFT JOIN (SELECT n FROM t WHERE grp = 3) t3 ON true
LEFT JOIN (SELECT n FROM t WHERE grp = 4) t4 ON true
LEFT JOIN (SELECT n FROM t WHERE grp = 5) t5 ON true
ORDER BY 1;
Probably faster for few groups. LEFT JOIN .. ON true makes this work even if higher levels are missing. concat() ignores NULL values. Test with EXPLAIN ANALYZE to be sure.
SQL Fiddle showing both.

how to make a sql loop?

here is the simplified table
filesystem (id, name, parentId);
and some entries
(1, 'root', NULL)
(2, 'folder', 1)
(3, 'subfolder', 2)
(4, 'subsubfolder', 3)
is there a way using native SQL to print the absolute path of one entry ?
for instance, the last entry would print 'root/folder/subfolder/subsubfolder'. the entry 2 would print 'root/folder' and so on.
You can do something like this
with tree(id, Level, Hierarchy) as
(
select id, 0, cast(Name as varchar(max))
from filesystem
union all
select a.id, b.Level+1,
b.Hierarchy+'/'+a.Name
from filesystem a
inner join tree b on a.parentid=b.id
)
select top(1) id, Hierarchy
from tree
where id=4
order by Level desc
It will give you id with full file path.
TO read in details you can check this
You didn't state your DBMS, the following is standard (ANSI) SQL:
with recursive folder_tree as (
select id, name, parentid, name as fullpath
from filesystem
where parentid is null
union all
select c.id, c.name, c.parentid, p.fullpath||'/'||c.name
from filesystem c
join folder_tree p on c.parentid = p.id
)
select *
from folder_tree
SQLFiddle: http://sqlfiddle.com/#!15/91332/7
Recursive CTE solution for SQL Server:
WITH FileSystem(id,name,parentID)
AS
(
SELECT 1,'root',NULL
UNION ALL
SELECT 2,'folder',1
UNION ALL
SELECT 3,'subFolder',2
UNION ALL
SELECT 4,'subSubFolder',3
),
CTE_Recursion
AS
(
SELECT ROW_NUMBER() OVER (ORDER BY ID) filePath_id,ID,CAST(name AS VARCHAR(100)) name,parentID
FROM FileSystem
WHERE parentID IS NULL
UNION ALL
SELECT A.filePath_id,B.id,CAST(A.name + '\' + B.name AS VARCHAR(100)),B.parentID
FROM CTE_Recursion A
INNER JOIN FileSystem B
ON A.ID = B.parentID
)
SELECT filePath_id,MAX(name) filePath
FROM CTE_Recursion
GROUP BY filepath_id
Results:
filePath_id filePath
-------------------- -----------------------------------
1 root\folder\subFolder\subSubFolder

How do I get records before and after given one?

I have the following table structure:
Id, Message
1, John Doe
2, Jane Smith
3, Error
4, Jane Smith
Is there a way to get the error record and the surrounding records? i.e. find all Errors and the record before and after them.
;WITH numberedlogtable AS
(
SELECT Id,Message,
ROW_NUMBER() OVER (ORDER BY ID) AS RN
FROM logtable
)
SELECT Id,Message
FROM numberedlogtable
WHERE RN IN (SELECT RN+i
FROM numberedlogtable
CROSS JOIN (SELECT -1 AS i UNION ALL SELECT 0 UNION ALL SELECT 1) n
WHERE Message='Error')
WITH err AS
(
SELECT TOP 1 *
FROM log
WHERE message = 'Error'
ORDER BY
id
),
p AS
(
SELECT TOP 1 l.*
FROM log
WHERE id <
(
SELECT id
FROM err
)
ORDER BY
id DESC
)
SELECT TOP 3 *
FROM log
WHERE id >
(
SELECT id
FROM p
)
ORDER BY
id
Adapt this routine to pick out your target.
DECLARE #TargetId int
SET #TargetId = 3
select *
from LogTable
where Id in (-- "before"
select max(Id)
from LogTable
where Id < #TargetId
-- target
union all select #TargetId
-- "after"
union all select min(Id)
from LogTable
where Id > #TargetId)
select id,messag from
(Select (Row_Number() over (order by ID)) as RNO, * from #Temp) as A,
(select SubRNO-1 as A,
SubRNO as B,
SubRNO+1 as C
from (Select (Row_Number() over (order by ID)) as SubRNO, * from #Temp) as C
where messag = 'Error') as B
where A.RNO = B.A or A.RNO = B.B or A.RNO = B.C
;WITH Logs AS
(
SELECT ROW_NUMBER() OVER (ORDER BY id), id, message as rownum FROM LogTable lt
)
SELECT curr.id, prev.id, next.id
FROM Logs curr
LEFT OUTER JOIN Logs prev ON curr.rownum+1=prev.rownum
RIGHT OUTER JOIN Logs next ON curr.rownum-1=next.rownum
WHERE curr.message = 'Error'
select id, message from tbl where id in (
select id from tbl where message = "error"
union
select id-1 from tbl where message = "error"
union
select id+1 from tbl where message = "error"
)
Get fixed number of rows before & after target
Using UNION for a simple, high performance query (I found selected answer WITH query above to be extremely slow)
Here is a high performance alternative to the WITH top selected answer, when you know an ID or specific identifier for a given record, and you want to select a fixed number of records BEFORE and AFTER that record. Requires a number field for ID, or something like date that can be sorted ascending / descending.
Example: You want to select the 10 records before and after a specific error was recorded, you know the error ID, and can sort by date or ID.
The following query gets (inclusive) the 1 result above, the identified record itself, and the 1 record below. After the UNION, the results are sorted again in descending order.
SELECT q.*
FROM(
SELECT TOP 2
id, content
FROM
the_table
WHERE
id >= [ID]
ORDER BY id ASC
UNION
SELECT TOP 1
id, content
FROM
the_table
WHERE
id < [ID]
ORDER BY id DESC
) q
ORDER BY q.id DESC