This question already has an answer here:
SQL Server recursive self join
(1 answer)
Closed 5 years ago.
I have the followign table
CREATE TABLE [dbo].[MyTable2](
[ID] [int] IDENTITY(1,1) NOT NULL,
[ParentID] [int] NOT NULL,
)
I try to create a query which will return a list of pairs ID, ParentID. For example I have the followign data
ID ParentID
1 0
2 0
3 1
4 3
5 3
15 8
I want when I search by ID = 5 to have the following list:
ID ParentID
5 3
3 1
1 0
If I search by ID = 15 it should see that the sequence is boken and I will get the followign list.
ID ParentID
15 8
I used a temporary table in order to make it work and my code is the following:
if object_id('tempdb..#Pairs') is not null
DROP TABLE #Pairs
create table #Pairs
(
ID INT,
ParentID INT
)
Declare #ID integer = 5;
Declare #ParentID integer;
while (#ID > 0)
BEGIN
SET #ParentID = null; -- I set it to null so that I will be able to check in case the sequence is broken
select #ID=ID, #ParentID=ParentID
from MyTable
where ID = #ID;
if #ParentID IS NOT null
begin
Insert into #Pairs (ID, ParentID) Values (#ID, #ParentID)
SET #ID = #ParentID;
end
else
SET #ID = 0;
END
SELECT * from #Pairs
It works but I am sure that there is a better way to do it. I found some strange queries which was suspposed to do something similar but I was not able to convert it in order to cover my needs.
For example I found the following Question but I was not able to convert it to work with my table. All the queries that I found had similar answers.
You are looking for recursive queries. See following example:
SELECT * INTO tab FROM (VALUES
(1, 0),
(2, 0),
(3, 1),
(4, 3),
(5, 3),
(15, 8)) T(ID, ParentID);
DECLARE #whatAreYouLookingFor int = 5;
WITH Rec AS
(
SELECT * FROM tab WHERE ID=#whatAreYouLookingFor
UNION ALL
SELECT T.* FROM tab T JOIN Rec R ON R.ParentID=T.ID
)
SELECT * FROM Rec;
DROP TABLE tab
Output:
ID ParentID
-- --------
5 3
3 1
1 0
Related
This question already has answers here:
How to concatenate text from multiple rows into a single text string in SQL Server
(47 answers)
Closed 1 year ago.
I have a table in a SQL-Server database (SQL-Server 2016) with 2 fields:
ConcateCode
ID int,
Random nchar(5)
ID Random
1 A2dd4
2 2f4mR
3 dhu9q
4 0lpms
Now I need a query where I get all the Random-fields concatenated in one field from Start-ID to End-ID, e.g. something like
SELECT ConcateCode FROM Table WHERE ID >= 1 AND ID < 5
and returns A2dd42f4mRdhu9q0lpms.
The problem is that I can't use a StoredProcedure, because my Programming-Language doesn't support Stored-Procedures, but only direct table access or queries.
The question sounds stupid easy, but I try to solve the problem now for a week and get no solution. Hope someone is more intelligent than me.
DECLARE #Result VARCHAR(MAX)
DECLARE #StartID int = 1
, #EndID int = 5
SELECT #Result= COALESCE(#Result, '') + ConcateCode FROM Table
WHERE ID BETWEEN #StartID AND #EndID
SELECT #Result
Try this:
DECLARE #MyTable TABLE (ID int, Random nchar(5))
INSERT INTO #MyTable VALUES
( 1, 'A2dd4')
, ( 2, '2f4mR')
, ( 3, 'dhu9q')
, ( 4, '0lpms')
;
DECLARE #StartID int = 1
, #EndID int = 5
;
DECLARE #String varchar(max) = (SELECT ''+Random FROM #MyTable WHERE ID BETWEEN #StartID AND #EndID FOR XML PATH('') );
SELECT #String;
e.g.
in table I have following records
a
b
c
d
e
f
g
h
I want to retrieved records by page
say 4 records per page
user x picks
a
b
c
d
Now user y should not pick any of the above
e
f
g
h
user x processes a record say record b
now he should see
a
e
c
d
and user y should see
f
g
h
i
how can I accomplish this, is there any built in way in mssql?
UPDATE
Here's what I have accomplished so far using auxilary table
http://sqlfiddle.com/#!18/e96f1/1
AllocateRecords2 2, 5, 1
GO
AllocateRecords2 2, 5, 2
both the queries are returning same set of results
I think you can use an auxiliary table UserData
CREATE TABLE Data(
ID int NOT NULL IDENTITY PRIMARY KEY,
Value varchar(1) NOT NULL
)
INSERT Data(Value)VALUES
('a'),
('b'),
('c'),
('d'),
('e'),
('f'),
('g'),
('h')
-- auxiliary table
CREATE TABLE UserData(
UserID int NOT NULL,
DataID int NOT NULL,
PRIMARY KEY(UserID,DataID),
FOREIGN KEY(DataID) REFERENCES Data(ID)
)
GO
And fill this table using the following procedure
CREATE PROC AddDataToUserData
#UserID int
AS
INSERT UserData(DataID,UserID)
SELECT TOP 4 ID,#UserID
FROM Data d
WHERE ID NOT IN(SELECT DataID FROM UserData)
ORDER BY ID
GO
Execute procedure for each other users
--TRUNCATE TABLE UserData
EXEC AddDataToUserData 1
EXEC AddDataToUserData 2
EXEC AddDataToUserData 3
...
Select data for a specific user
SELECT d.*
FROM Data d
JOIN UserData u ON u.DataID=d.ID
WHERE u.UserID=1
SELECT d.*
FROM Data d
JOIN UserData u ON u.DataID=d.ID
WHERE u.UserID=2
You can also create procedure for it
CREATE PROC GetDataForUser
#UserID int
AS
SELECT d.*
FROM Data d
JOIN UserData u ON u.DataID=d.ID
WHERE u.UserID=#UserID
GO
And then use it
EXEC GetDataForUser 1
EXEC GetDataForUser 2
Hope I understood your question correctly. But if I'm wrong you may use it as an idea.
I've added one column PageNumber into AllocatedRecords
/*
DROP TABLE Alphabets
DROP TABLE AllocatedRecords
GO
*/
CREATE TABLE Alphabets
(
ID INT IDENTITY(1,1) PRIMARY KEY,
Record varchar(1)
)
GO
CREATE TABLE [dbo].[AllocatedRecords](
[ID] [bigint] IDENTITY(1,1) NOT NULL primary key,
[ReferenceID] [int] NULL,
[IsProcessed] [bit] NULL,
[AllocatedToUser] [int] NULL,
[AllocatedDate] [datetime] NULL,
[ProcessedDate] [datetime] NULL,
PageNumber int -- new column
)
GO
INSERT Alphabets VALUES('a'),('b'),('c'),('d'),('e'),('f'),('g'),('h'),('i'),('j'),('k'),('l'),('m'),('n'),('o'),('p'),('q'),('r'),('s'),('t'),('u'),('v'),('w'),('x'),('y'),('z')
GO
And changed your procedure
DROP PROC AllocateRecords2
GO
CREATE PROC AllocateRecords2
(
#UserID INT,
#PageSize INT,
#PageNumber INT
)
AS
BEGIN
DECLARE #Today DATETIME
SET #Today = GETDATE()
--deallocated expired items
--TRUNCATE TABLE AllocatedRecords
DELETE AllocatedRecords
WHERE IsProcessed = 0 AND
(
(DATEDIFF(minute, #Today, AllocatedDate) > 5)
OR (AllocatedToUser = #UserID AND PageNumber <> #PageNumber)
)
DECLARE
#Draw INT = 10,
#PoolSize INT = #PageSize,
#CurrentRecords INT = (SELECT Count(*) from AllocatedRecords WHERE AllocatedToUser = #UserID AND IsProcessed = 0)
IF #CurrentRecords = 0
BEGIN
SET #Draw = #PoolSize
END
ELSE IF #CurrentRecords < #PoolSize
BEGIN
SET #Draw = #PoolSize - #CurrentRecords
END
ELSE IF #CurrentRecords >= #PoolSize
BEGIN
SET #Draw = 0
END
IF #Draw>0
BEGIN
INSERT AllocatedRecords(ReferenceID,IsProcessed,AllocatedToUser,AllocatedDate,ProcessedDate,PageNumber)
SELECT ID, 0, #UserID, GETDATE(), NULL, #PageNumber
FROM Alphabets
WHERE ID NOT IN (SELECT ReferenceID FROM AllocatedRecords)
ORDER BY ID
OFFSET (#PageNumber - 1) * #PageSize ROWS
FETCH NEXT #Draw ROWS ONLY
END
SELECT x.ID, x.Record
FROM AllocatedRecords A
JOIN Alphabets x ON A.ReferenceID = x.ID
WHERE AllocatedToUser = #UserID
AND IsProcessed = 0
SELECT COUNT(*) as TotalRecords
FROM AllocatedRecords
WHERE AllocatedToUser = #UserID
AND IsProcessed = 0
END
GO
Test
TRUNCATE TABLE AllocatedRecords
GO
-- user 2
EXEC AllocateRecords2 2, 5, 1
EXEC AllocateRecords2 2, 5, 2
EXEC AllocateRecords2 2, 5, 2 -- the same page
EXEC AllocateRecords2 1, 5, 1 -- user 1
EXEC AllocateRecords2 3, 5, 1 -- user 3
I have Parent and Child table.
The goal is to duplicate the records, except with new primary keys.
Original Tables
Parent(id)
1
Child(id,parentId, data)
1,1
2,1
After insert:
Parent
1
2
Child
1,1
2,1
3,2
4,2
How do I do that? The part I am having trouble with is getting the new parent key for use with the child records.
This is what I have come up with so far.
--DECLARE VARS
declare #currentMetadataDocumentSetId int = 1, --Ohio
#newMetadataDocumentSetid int = 3; --PA
--CLEANUP
IF OBJECT_ID('tempdb..#tempFileRowMap') IS NOT NULL
/*Then it exists*/
DROP TABLE #tempFileRowMap
--Remove existing file row maps.
delete from file_row_map where metadata_document_set_id = #newMetadataDocumentSetid;
--Create a temptable to hold data to be copied.
Select [edi_document_code],
[functional_group],
[description],
3 as [metadata_document_set_id],
[document_name],
[incoming_file_row_subtype],
[metadata_document_id],
[document_subcode],
[outgoing_file_row_subtype],
[asi_type_code],
[asi_action_code],
[metadata_document_set],
file_row_map_id as orig_file_row_map_id
into #tempFileRowMap
from file_row_map fileRowMap
where metadata_document_set_id = #currentMetadataDocumentSetId;
--Select * from #tempFileRowMap;
Insert into file_row_map select
[edi_document_code],
[functional_group],
[description],
[metadata_document_set_id],
[document_name],
[incoming_file_row_subtype],
[metadata_document_id],
[document_subcode],
[outgoing_file_row_subtype],
[asi_type_code],
[asi_action_code],
[metadata_document_set]
from #tempFileRowMap
--Show Results
Select * from file_row_map fileRowMap where fileRowMap.metadata_document_set_id = #newMetadataDocumentSetid
--Update Detail
Select
[file_row_map_id],
[file_row_column],
[element_code],
[element_metadata_id],
[col_description],
[example],
[translate],
[is_used],
[is_mapped],
[page_num],
[subcode],
[qualifier],
[loop_code],
[loop_subcode],
[default_value],
[delete_flag]
into #tempFileRowMapDetail
from [dbo].[file_row_map_detail] d
left join #tempFileRowMap m
on m.orig_file_row_map_id = d.file_row_map_id
select * from #tempFileRowMapDetail
Simply use OUTPUT clause for getting exact Parent Table Primary Key values.
Lets build Example Schema for your case
--For Capturing inserted ID
CREATE TABLE #ID_CAPTURE (PARENT_ID INT,ORDER_NME VARCHAR(20));
--Your Intermidiate Data To insert into Actual Tables
CREATE TABLE #DUMMY_TABLE (ORDER_NME VARCHAR(20), ITEM_NME VARCHAR(20));
--Actual Tables
CREATE TABLE #ORDER_PARENT (ORDER_ID INT IDENTITY,ORDER_NME VARCHAR(20))
CREATE TABLE #ORDER_CHILD (CHILD_ID INT IDENTITY ,ORDER_ID INT, ORDER_NME VARCHAR(20))
INSERT INTO #DUMMY_TABLE
SELECT 'BILL1','Oil'
UNION ALL
SELECT 'BILL1', 'Gas'
UNION ALL
SELECT 'BILL2', 'Diesel'
Now do Inserts in Parent & Child Tables
INSERT INTO #ORDER_PARENT
OUTPUT inserted.ORDER_ID, inserted.ORDER_NME into #ID_CAPTURE
SELECT DISTINCT ORDER_NME FROM #DUMMY_TABLE
INSERT INTO #ORDER_CHILD
SELECT C.PARENT_ID, ITEM_NME FROM #DUMMY_TABLE D
INNER JOIN #ID_CAPTURE C ON D.ORDER_NME = C.ORDER_NME
SELECT * FROM #ID_CAPTURE
SELECT * FROM #ORDER_CHILD
There are other ways to get Inserted Identity values.
See documentation ##IDENTITY (Transact-SQL) , SCOPE_IDENTITY
Try following approach:
DECLARE #Table1 TABLE (
ID INT NOT NULL PRIMARY KEY,
ParentID INT NULL, -- FK
[Desc] VARCHAR(50) NOT NULL
);
INSERT #Table1 (ID, ParentID, [Desc])
VALUES
(1, NULL, 'A'),
(2, 1, 'AA.1'),
(3, 1, 'AA.2'),
(4, NULL, 'B'),
(5, 4, 'BB.1'),
(6, 4, 'BB.2'),
(7, 4, 'BB.3'),
(8, 7, 'BBB.1');
DECLARE #ParentID INT = 4;
DECLARE #LastID INT = (SELECT TOP(1) ID FROM #Table1 x ORDER BY x.ID DESC)
IF #LastID IS NULL
BEGIN
RAISERROR('Invalid call', 16, 1)
--RETURN ?
END
SELECT #LastID AS LastID;
/*
LastID
-----------
8
*/
DECLARE #RemapIDs TABLE (
OldID INT NOT NULL PRIMARY KEY,
[NewID] INT NOT NULL UNIQUE
);
WITH CteRecursion
AS (
SELECT 1 AS Lvl, crt.ID, crt.ParentID --, crt.[Desc]
FROM #Table1 crt
WHERE crt.ID = #ParentID
UNION ALL
SELECT cld.Lvl + 1 AS Lvl, crt.ID, crt.ParentID --, crt.[Desc]
FROM #Table1 crt
JOIN CteRecursion cld ON crt.ParentID = cld.ID
)
INSERT #RemapIDs (OldID, [NewID])
SELECT r.ID, #LastID + ROW_NUMBER() OVER(ORDER BY r.Lvl) AS [NewID]
FROM CteRecursion r;
--INSERT #Table1 (ID, ParentID, [Desc])
SELECT nc.[NewID] AS ID, np.[NewID] AS ParentID, o.[Desc]
FROM #Table1 o -- old
JOIN #RemapIDs nc /*new child ID*/ ON o.ID = nc.OldID
LEFT JOIN #RemapIDs np /*new parent ID*/ ON o.ParentID = np.OldID
/*
ID ParentID Desc
----------- ----------- --------------------------------------------------
9 NULL B
10 9 BB.1
11 9 BB.2
12 9 BB.3
13 12 BBB.1
*/
Note: with some minor changes should work w. many ParentIDs values.
I have a table with the following columns
idRelationshipType int,
idPerson1 int,
idPerson2 int
This table allows me to indicate records in a database that should be linked together.
I need to do a query returning all the unique ids where a person's id exists in idPerson1 or idPerson2 columns. Additionally, I need the query to be recursive so that the if I a match is found in idPerson1, the value for idPerson2 is included in the result set and used to repeat the query recursively until no more matches are found.
Example data:
CREATE TABLE [dbo].[tbRelationships]
(
[idRelationshipType] [int],
[idPerson1] [int] ,
[idPerson2] [int]
)
INSERT INTO tbRelationships (idRelationshipType, idPerson1, idPerson2)
VALUES (1, 1, 2)
INSERT INTO tbRelationships (idRelationshipType, idPerson1, idPerson2)
VALUES (1, 2, 3)
INSERT INTO tbRelationships (idRelationshipType, idPerson1, idPerson2)
VALUES (1, 3, 4)
INSERT INTO tbRelationships (idRelationshipType, idPerson1, idPerson2)
VALUES (1, 5, 1)
Four 'Relationships' are defined here. For this query, I will only know one of the ids to begin with. I need a query that in concept works like
SELECT idPerson
FROM [some query]
WHERE [the id i have to start with] = #idPerson
AND idRelationshipType = #idRelationshipType
The returned result should be a 5 rows with one column 'idPerson', with 1, 2, 3, 4, and 5 as the row values.
I have tried various combinations of UNPIVOT and recursive CTEs but I am not making much progress.
Any help would be greatly appreciated.
Thanks,
Daniel
I think this is what you want:
DECLARE #RelationshipType int
DECLARE #PersonId int
SELECT #RelationshipType = 1, #PersonId = 1
;WITH Hierachy (idPerson1, IdPerson2)
AS
(
--root
SELECT R.idPerson1, R.idPerson2
FROM tbRelationships R
WHERE R.idRelationshipType = #RelationshipType
AND (R.idPerson1 = #PersonId OR R.idPerson2 = #PersonId)
--recurse
UNION ALL
SELECT R.idPerson1, R.idPerson2
FROM Hierachy H
JOIN tbRelationships R
ON (R.idPerson1 = H.idPerson2
OR R.idPerson2 = H.idPerson1)
AND R.idRelationshipType = #RelationshipType
)
SELECT DISTINCT idPerson
FROM
(
SELECT idPerson1 AS idPerson FROM Hierachy
UNION
SELECT idPerson2 AS idPerson FROM Hierachy
) H
Essentially, get the first rows where the required id is in either column, and then recurse getting all of the child ids based on id column 2
Let's say I have a table with 2 columns ID and ParentID. My data looks like this:
ID ParentID
1 Null
2 1
3 1
4 2
4 2
So to find all relationships based on a given ID my query simplified looks like this:
WITH links ([ID], [ParentID], Depth)
AS
(
--Get the starting link
SELECT
[ID],
[ParentID],
[Depth] = 1
FROM
[MyTable]
WHERE
[ID] = #StartID
UNION ALL
--Recursively get links that are parented to links already in the CTE
SELECT
mt.[ID],
mt.[ParentID],
[Depth] = l.[Depth] + 1
FROM
[MyTable] mt
JOIN
links l ON mt.ParentID = l.ID
WHERE
Depth < 99
)
SELECT
[Depth],
[ID],
[ParentID]
FROM
[links]
Now let's say the data in my table creates a cyclical relationship (4 is parented to 2 and 2 is parented to 4. Forgetting for a moment that there should likely be constraints on the database to prevent this, the above recursive CTE query produce duplicate records (99 of them) because it will recursively evaluate that cyclical relationship between 2 and 4.
ID ParentID
1 Null
2 1
3 1
4 2
2 4
2 4
How can I alter my query to prevent that, assuming that I have no control over preventing the actual data from representing that cyclical relationship. Normally I would put a distinct on the final select but I want the Depth value, which makes every record distinct. I'm also hoping to account for it within the CTE, as a distinct operates on the final select, and is probably not as efficient.
You could create a tree path variable in the CTE which shows your entire path from the top of the recursive query, then check to see if the number in question is in the tree path, if it is then abort at that point.
USE Master;
GO
CREATE DATABASE [QueryTraining];
GO
USE [QueryTraining];
GO
CREATE TABLE [MyTable] (
ID int, --would normally be an INT IDENTITY
ParentID int
);
INSERT INTO [MyTable] (ID, ParentID)
VALUES (1, NULL),
(2, 1),
(3, 1),
(4, 2),
(2, 4),
(2, 4);
DECLARE #StartID AS INTEGER;
SET #StartID = 1;
;WITH links (ID, ParentID, Depth, treePath)
AS
(
--Get the starting link
SELECT [ID],
[ParentID],
[Depth] = 1,
CAST(':' + CAST([ID] AS VARCHAR(MAX)) AS VARCHAR(MAX)) AS treePath
FROM [MyTable]
WHERE [ID] = #StartID
UNION ALL
--Recursively get links that are parented to links already in the CTE
SELECT mt.[ID],
mt.[ParentID],
[Depth] = l.[Depth] + 1,
CAST(l.treePath + CAST(mt.[ID] AS VARCHAR(MAX)) + ':' AS VARCHAR(MAX)) AS treePath
FROM [MyTable] mt
INNER JOIN links l ON mt.ParentID = l.ID
AND CHARINDEX(':' + CAST(mt.[ID] AS VARCHAR(MAX)) + ':', l.[treePath]) = 0
WHERE Depth < 10
)
SELECT
[Depth],
[ID],
[ParentID],
[treePath]
FROM
[links];
The line on the INNER JOIN that says
AND CHARINDEX(':' + CAST(mt.[ID] AS VARCHAR(MAX)) + ':', l.[treePath]) = 0
Is where the previous numbers in the path get filtered out.
Just copy and paste the example and give it a try.
One note, the way that I am using CHARINDEX on the CTE may not scale well, but it does accomplish what I think you are looking for.