SQL group with Recursive CTE - sql

I'm working on SQL Server 2008. I believe the answer to my Q lies in a recursive CTE but any solution would be greatly appreciated.
In the sam_DB.dbo.example table below where the PID is not null it links back to an ID
ID | PID
------ | ------
1 | NULL
2 | 1
3 | 2
4 | 3
5 | NULL
6 | 5
7 | 6
8 | NULL
9 | NULL
10 | 9
I want my output to have a new field (CID) that identifies each record in a chain of linkages from PID to ID as part of a group, as per below.
ID | PID | CID
------ | ------ | ------
1 | NULL | 1
2 | 1 | 1
3 | 2 | 1
4 | 3 | 1
5 | NULL | 2
6 | 5 | 2
7 | 6 | 2
8 | NULL | 3
9 | NULL | 4
10 | 9 | 4

You're correct that you'd need a CTE for this.
You need to define the first part of the query to select the top level records (i.e. those which have no parent):
select ID, PID, ID
from #t
where PID is null
Then, for every row added to the resulting CTE (i.e. first of all for those records returned by the above query, then again for each new row added by this second part of the query, repeated for each addition until no new additions are made) you should add all records from the source table for which the parent id matches the previously added row's id.
select t.ID, t.PID, c.CID
from cte c
inner join #t t
on t.PID = c.ID
Aside from this logic, the only other thing to be aware of is that the CID column for the first expression takes the record's ID, whilst for those records returned by the second expression it takes the parent record's CID.
Full Code
--set up the demo data
declare #t table (ID int not null, PID int null)
insert #t
values (1, null)
, (2,1)
, (3,2)
, (4,3)
, (5,null)
, (6,5)
, (7,6)
, (8,null)
, (9,null)
, (10,9)
--actual demo
;with cte (ID, PID, CID) as (
--select out top most (ancestor) records; setting CID to ID (since they're the oldest ancestor in their own chain, given they don't have parents)
select ID, PID, ID
from #t
where PID is null
union all
--select each record that is a child of the record we previously selected, holding the ancestor as the parent record's ancestor
select t.ID, t.PID, c.CID
from cte c
inner join #t t
on t.PID = c.ID
)
select *
from CTE
order by ID

you have to use Common text expression With Row_Number Window function
CREATE TABLE #TblTemp(ID int,PID int)
INSERT INTO #TblTemp(ID ,PID ) VALUES (1,NULL),(2,1),(3,1),(4,3),(5,NULL),(6,5),(7,6),(8,NULL),(9,NULL),(10,9)
;WITH CTE (ID, PID, CID) AS (
SELECT ID, PID, ROW_NUMBER() OVER(ORDER BY ID) RN
FROM #TBLTEMP
WHERE PID IS NULL
UNION ALL
SELECT T.ID, T.PID, C.CID
FROM CTE C
INNER JOIN #TBLTEMP T
ON T.PID = C.ID
)
SELECT *
FROM CTE
ORDER BY ID

I will post some simple example
-- shows how to create a recursive grouping for wrongly linked or corrupted pieces of a parent/child groups
declare #t table (item varchar(2), tr int null, rc int null)
insert #t select 'a',1,9 -- no links 'a' - is group parent
insert #t select 'b',2,1 -- links to 'a'
insert #t select 'c',3,2 -- links to 'b'
insert #t select 'd',4,3 -- links to 'd'
insert #t select 'e',6,7 -- no links 'e' - is a different group
insert #t select 'f',8,2 -- links to 'c'
-- grn-group name based on a parent item name;
-- gid-group name based on a parent item id;
-- tr-transactionID ; rc-recursiveID;
-- rc_New-new recursiveID to use; rc_Old - original recursiveID
;with cte as
(
select grn=s.item, gid=s.tr, s.item, s.tr, rc_New= t.tr, rc_Old=s.rc from #t s
left join #t t on t.tr=s.rc where (t.tr is NULL or s.rc is NULL)
union all
select c.grn, c.gid,s.item, s.tr, rc_New=s.rc, rc_Old=s.rc
from cte c join #t s on s.rc=c.tr where s.rc is not NULL
)
select * from cte order by 2,3
option (MAXRECURSION 32767, FAST 100)

Related

SQL query to get list of all records that are placed higher in hierarchy

Table:
+-----+------------+-------------+
| Id | DocumentNo | ParentCCID |
+-----+------------+-------------+
| 10 | CC001 | NULL |
| 20 | CC002 | CC001 |
| 33 | CC003 | CC002 |
+-----+-------------+-------------+
Value passed to the query: CC003
Expected Output:
CC003
CC002
CC001
Failed Attempt:
select b2.documentno,b2.ParentCCID from basicdetails b1
inner join basicdetails b2 on b1.documentno = b2.ParentCCID
where b2.documentno='CC003'
Note: DocumentNo is unique primary key. ParentCCID could have null values if there is no parent record.
EDIT:
create table basicdetails2
(
id int identity,
documentno varchar(30),
parentccid varchar(30)
)
insert into basicdetails2 values('CC001', null)
insert into basicdetails2 values('CC002', 'CC001')
insert into basicdetails2 values('CC003', 'CC002')
insert into basicdetails2 values('CC004', 'CC003')
You want a recursive cte:
with cte as (
select bd.documentno, bd.ParentCCID
from basicdetails bd
where bd.documentno = 'CC003'
union all
select cte.documentno, cte.ParentCCID
from cte join
basicdetails bd
on bd.documentno = cte.ParentCCID
)
select bd.documentno
from cte;
Just a minor twist on Gordon's answer (already +1).
I like to track the level and see the parents for each record
Example
Declare #Fetch varchar(25) = 'CC003'
;with cte as (
Select DocumentNo
,ParentCCDocumentNo
,Lvl=1
From YourTable
Where DocumentNo=#Fetch
Union All
Select R.DocumentNo
,R.ParentCCDocumentNo
,P.Lvl+1
From YourTable R
Join cte P on P.ParentCCDocumentNo = R.DocumentNo)
Select Lvl = Row_Number() over (Order By Lvl Desc)
,DocumentNo
,ParentCCDocumentNo
From cte
Order By 1 desc
Returns
Lvl DocumentNo ParentCCDocumentNo
3 CC003 CC002
2 CC002 CC001
1 CC001 NULL

Two rows with the same id and two different values, getting the second value into another column

I have two rows with the same id but different values. I want a query to get the second value and display it in the first row.
There are only two rows for each productId and 2 different values.
I've tried looking for this for the solution everywhere.
What I have, example:
+-----+-------+
| ID | Value |
+-----+-------+
| 123 | 1 |
| 123 | 2 |
+-----+-------+
What I want
+------+-------+---------+
| ID | Value | Value 1 |
+------+-------+---------+
| 123 | 1 | 2 |
+------+-------+---------+
Not sure whether order matters to you. Here is one way:
SELECT MIN(Value), MAX(Value), ID
FROM Table
GROUP BY ID;
This is a self-join:
SELECT a.ID, a.Value, b.Value
FROM table a
JOIN table b on a.ID = b.ID
and a.Value <> b.Value
You can use a LEFT JOIN instead if there are IDs that only have one value and would be lost by the above JOIN
May be you may try this
DECLARE #T TABLE
(
Id INT,
Val INT
)
INSERT INTO #T
VALUES(123,1),(123,2),
(456,1),(789,1),(789,2)
;WITH CTE
AS
(
SELECT
RN = ROW_NUMBER() OVER(PARTITION BY Id ORDER BY Val),
*
FROM #T
)
SELECT
*
FROM CTE
PIVOT
(
MAX(Val)
FOR
RN IN
(
[1],[2]--Add More Numbers here if there are more values
)
)Q

SQL SELECT Convert Min/Max into Separate Rows

I have a table that has a min and max value that I'd like create a row for each valid number in a SELECT statement.
Original table:
| Foobar_ID | Min_Period | Max_Period |
---------------------------------------
| 1 | 0 | 2 |
| 2 | 1 | 4 |
I'd like to turn that into:
| Foobar_ID | Period_Num |
--------------------------
| 1 | 0 |
| 1 | 1 |
| 1 | 2 |
| 2 | 1 |
| 2 | 2 |
| 2 | 3 |
| 2 | 4 |
The SELECT results need to come out as one result-set, so I'm not sure if a WHILE loop would work in my case.
If you expect just a handful of rows per foobar, then this is a good opportunity to learn about recursive CTEs:
with cte as (
select foobar_id, min_period as period_num, max_period
from original t
union all
select foobar_id, min_period + 1 as period_num, max_period
from cte
where period_num < max_period
)
select foobar_id, period_num
from cte
order by foobar_id, period_num;
You can extend this to any number of periods by setting the MAXRECURSION option to 0.
One method would be to use a Tally table, ther's plenty of examples out there, but I'm going to create a very small one in this example. Then you can JOIN onto that and return your result set.
--Create the Tally Table
CREATE TABLE #Tally (I int);
WITH ints AS(
SELECT 0 AS i
UNION ALL
SELECT i + 1
FROM ints
WHERE i + 1 <= 10)
--And in the numbers go!
INSERT INTO #Tally
SELECT i
FROM ints;
GO
--Create the sample table
CREATE TABLE #Sample (ID int IDENTITY(1,1),
MinP int,
MaxP int);
--Sample data
INSERT INTO #Sample (Minp, MaxP)
VALUES (0,2),
(1,4);
GO
--And the solution
SELECT S.ID,
T.I AS P
FROM #Sample S
JOIN #Tally T ON T.I BETWEEN S.MinP AND S.MaxP
ORDER BY S.ID, T.I;
GO
--Clean up
DROP TABLE #Sample;
DROP TABLE #Tally;
Depending on the size of the data and the range of the period, the easiest way to do this is to use a dynamic number fact table, as follows:
WITH rn AS (SELECT ROW_NUMBER() OVER (ORDER BY object_id) -1 as period_num FROM sys.objects)
SELECT f.foobar_id, rn.period_num
FROM foobar f
INNER JOIN rn ON rn.period_num BETWEEN f.min_period AND f.max_period
However, if you're working with a larger volume of data, it will be worth creating a number fact table with an index. You can even use a TVV for this:
-- Declare the number fact table
DECLARE #rn TABLE (period_num INT IDENTITY(0, 1) primary key, dummy int)
-- Populate the fact table so that all periods are covered
WHILE (SELECT COUNT(1) FROM #rn) < (SELECT MAX(max_period) FROM foobar)
INSERT #rn select 1 from sys.objects
-- Select using a join to the fact table
SELECT f.foo_id, rn.period_num
FROM foobar f
inner join #rn rn on rn.period_num between f.min_period and f.max_period
Just Create a function sample date and use it
CREATE FUNCTION [dbo].[Ufn_GetMInToMaxVal] (#Min_Period INT,#Max_Period INT )
RETURNS #OutTable TABLE
(
DATA INT
)
AS
BEGIN
;WIth cte
AS
(
SELECT #Min_Period As Min_Period
UNION ALL
SELECT Min_Period+1 FRom
cte
WHERE Min_Period < #Max_Period
)
INSERT INTO #OutTable
SELECT * FROM cte
RETURN
END
Get the result by executing sql statement
DECLARE #Temp AS TABLE(
Foobar_ID INT,
Min_Period INT,
Max_Period INT
)
INSERT INTO #Temp
SELECT 1, 0,2 UNION ALL
SELECT 2, 1,4
SELECT Foobar_ID ,
DATA
FROM #Temp
CROSS APPLY
[dbo].[Ufn_GetMInToMaxVal] (Min_Period,Max_Period)
Result
Foobar_ID DATA
----------------
1 0
1 1
1 2
2 1
2 2
2 3
2 4

How to use recursive logic to return only the Root row in a sql table (SQL Server 2008 R2)

This is for SQL Server 2008 R2, I'm a novice at SQL so please be as specific as you can.
Table1 has some recursive structure built into it, where the ParentId is either Null meaning it's the root, or ParentId is the Id of another row in Table1 which denotes it as a child.
Example data set:
Table1Id ParentId
--------------------------------------------
1 NULL
2 1
3 1
4 2
5 NULL
6 2
7 6
8 NULL
9 8
With the above example the table then has the following tree structure with 3 root nodes:
Root 1 5 8
Child(teir1) 2 3 9
Child(teir2) 4 6
Child(tier3) 7
....
Is there a way to return only the Root row given any of the row Ids? For example:
InputId ReturnedRowId
----------------------------
1 1
2 1
3 1
4 1
5 5
6 1
7 1
8 8
9 8
Any help would be appreciated.
You can use a CTE and traverse the hierarchy
IF OBJECT_ID('tempdb..#testData') IS NOT NULL
DROP TABLE #testData
CREATE TABLE #testData (
Table1Id INT
,ParentId INT NULL
)
INSERT INTO #testData ( Table1Id, ParentId )
VALUES
(1, NULL )
,(2, 1 )
,(3, 1 )
,(4, 2 )
,(5, NULL )
,(6, 2 )
,(7, 6 )
,(8, NULL )
,(9, 8 )
DECLARE #InputId INT
SET #InputId = 2 --<<--Change this as appropriate
;WITH cteTraverse
AS
(
SELECT
T.Table1Id, T.ParentId
FROM
#testData T
WHERE
Table1Id = #InputId
UNION ALL
SELECT
T1.Table1Id, T1.ParentId
FROM
#testData T1
INNER JOIN
cteTraverse T2 ON T1.Table1Id = T2.ParentId
)
SELECT
#InputId '#InputId', Table1Id 'ReturnedRowId'
FROM
cteTraverse
WHERE
ParentId IS NULL
This query do the job.
with CTE as
(
Select Table1ID as ID, Table1ID as Ancestor, 0 as level
from Table1
UNION ALL
Select ID, ParentID, level + 1
from Table1
inner join CTE on CTE.Ancestor = Table1.Table1ID
where ParentID is not NULL
)
,
R_only as
(
Select ID as ID, MAX(level) as max_level
from CTE
group by ID
)
select CTE.ID, Ancestor
from CTE inner join R_only on CTE.ID = R_only.ID and CTE.level = R_only.max_level
order by CTE.ID
Here's a script that finds the root nodes for the nodes table you have. What it does:
In the first CTE, recurses the nodes until the parent is found. Recursion keeps track of the depth in column level.
In a second CTE, determines the maximum depth for each node: max_level. This is the depth where the parent was determined.
Select the nodes with maximum depth.
CREATE TABLE #tree(table1_id INT PRIMARY KEY,parent_id INT);
INSERT INTO #tree(table1_id,parent_id)VALUES
(1,NULL),(2,1),(3,1),(4,2),(5,NULL),(6,2),(7,6),(8,NULL),(9,8);
;WITH cte_tr AS (
SELECT table1_id, parent_id, level=0
FROM #tree
UNION ALL
SELECT t_c.table1_id, t_p.parent_id, level=t_c.level+1
FROM cte_tr AS t_c
INNER JOIN #tree AS t_p ON
t_p.table1_id=t_c.parent_id
WHERE t_p.parent_id IS NOT NULL
),
cte_ml AS (
SELECT table1_id, max_level=MAX(level)
FROM cte_tr
GROUP BY table1_id
)
SELECT cte_tr.table1_id, root_node=ISNULL(cte_tr.parent_id,cte_tr.table1_id)
FROM cte_tr
INNER JOIN cte_ml ON
cte_ml.table1_id=cte_tr.table1_id AND
cte_ml.max_level=cte_tr.level
ORDER BY cte_tr.table1_id
DROP TABLE #tree;
Result:
+-----------+-----------+
| table1_id | root_node |
+-----------+-----------+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 1 |
| 5 | 5 |
| 6 | 1 |
| 7 | 1 |
| 8 | 8 |
| 9 | 8 |
+-----------+-----------+
Use start with and connect by prior. Check this documentation here
http://psoug.org/reference/connectby.html
I am not sure if "connect by" is available in other databases other than Oracle. Check it out.
You could also try with clause. Someone has tried here.
Simulation of CONNECT BY PRIOR of ORACLE in SQL SERVER
With works somewhat like this
with query1 as (select .... from .... where ....),
query2 as (select .... from .... where ....)
select ...
from query1 q1,
query2 q2
where q1.xxxxx = q2.xxxx

SQL - Clone a record and its descendants

I would like to be able to clone a record and its descendants in the same table. An example of my table would be the following:
Table1
id | parentid | name
---------------------
1 | 0 | 'Food'
2 | 1 | 'Taste'
3 | 1 | 'Price'
4 | 2 | 'Taste Requirements'
The "id" column is the primary key and auto-increments. The 'Food' record (i.e. where id = 1) has two records underneath it called 'Taste' and 'Price'. The 'Taste' record has a record underneath it called 'Taste Requirements'. I would like to be able to clone the 'Food' record so that Table1 would look like the following:
Table1
id | parentid | name
---------------------
1 | 0 | 'Food'
2 | 1 | 'Taste'
3 | 1 | 'Price'
4 | 2 | 'Taste Requirements'
5 | 0 | 'Cookies'
6 | 5 | 'Taste'
7 | 5 | 'Price'
8 | 6 | 'Taste Requirements'
(where 'Cookies' is the name of the new category that I want to create). I am able to select all the descendants of 'Food' using:
with Table1_CTE( id, parentid, name )
as
(
select t.id, t.parentid, t.name from Table1 t
where t.id = 1
union all
select t.id, t.parentid,t. name from Table1 t
inner join Table1_CTE as tc
on t.parentid = tc.id
)
select id, parentid, name from Table1_CTE
and I am able to clone just the 'Food' record (i.e. where id = 1) using:
insert into Table1 ( parentid, name )
select ( parentid, 'Cookies' )
from Table1 where id = 1
but I am having problems trying to combine the two queries to clone the descendants of 'Food'. Also, I am trying to avoid using stored procedures, triggers, curosrs, etc. Is what I am trying to do possible? I have seen some examples on the web but have been unable to apply them to my requirements.
As Martin suggested, you need to enable IDENTITY_INSERT so that you can push your own identity values. You may also need to acquire a table lock to ensure that Max( Id ) returns the correct value.
If object_id('tempdb..#TestData') is not null
Drop Table #TestData
GO
Create Table #TestData
(
Id int not null identity(1,1) Primary Key
, ParentId int not null
, Name varchar(50) not null
)
GO
Set Identity_Insert #TestData On
GO
Insert #TestData( Id, ParentId, Name )
Values( 1,0,'Food' )
, ( 2,1,'Taste' )
, ( 3,1,'Price' )
, ( 4,2,'Taste Requirement' );
With Data As
(
Select Cast(MaxId.Id + 1 As int) As Id
, T.ParentId
, 'Copy Of ' + T.name As Name
, T.Id As OldId
, 0 As OldParentId
From #TestData As T
Cross Join( Select Max( id ) As Id From #TestData ) As MaxId
Where T.Name = 'Food'
Union All
Select Cast(Parent.id + Row_Number() Over( Order By Child.Id ) + 1 As int)
, Parent.Id
, 'Copy of ' + Child.Name
, Child.Id
, Child.ParentId
From Data As Parent
Join #TestData As Child
On Child.ParentId = Parent.OldId
)
Insert #TestData( Id, ParentId, Name )
Select Id, ParentId, Name
From Data
GO
Set Identity_Insert #TestData Off
GO
Results
id | parentid | name
-- | -------- | -----------------
1 | 0 | Food
2 | 1 | Taste
3 | 1 | Price
4 | 2 | Taste Requirement
5 | 0 | Copy Of Food
7 | 5 | Copy of Taste
8 | 5 | Copy of Price
9 | 7 | Copy of Taste Requirement
Assuming your CTE picks a root record and all it's descendents (it didn't seem to when I reproduced using your data above), then you can clone all selected records and insert like this:
with Table1_CTE( id, parentid, name )
as
(
select t.id, t.parentid, t.name from Table1 t
where c.icategoryid = 1
union all
select t.id, t.parentid,t. name from Table1
inner join Table1_CTE as tc
on t.parentid = tc.id
)
insert into dbo.testinsertheirarchy ( parentid, name )
select parentid, name from Table1_CTE