How to traverse a path in a table with id & parentId? - sql

Suppose I have a table like:
id | parentId | name
1 NULL A
2 1 B
3 2 C
4 1 E
5 3 E
I am trying to write a scalar function I can call as:
SELECT dbo.GetId('A/B/C/E') which would produce "5" if we use the above reference table. The function would do the following steps:
Find the ID of 'A' which is 1
Find the ID of 'B' whose parent is 'A' (id:1) which would be id:2
Find the ID of 'C' whose parent is 'B' (id:2) which would be id:3
Find the ID of 'E' whose parent is 'C' (id:3) which would be id:5
I was trying to do it with a WHILE loop but it was getting very complicated very fast... Just thinking there must be a simple way to do this.

CTE version is not optimized way to get the hierarchical data. (Refer MSDN Blog)
You should do something like as mentioned below. It's tested for 10 millions of records and is 300 times faster than CTE version :)
Declare #table table(Id int, ParentId int, Name varchar(10))
insert into #table values(1,NULL,'A')
insert into #table values(2,1,'B')
insert into #table values(3,2,'C')
insert into #table values(4,1,'E')
insert into #table values(5,3,'E')
DECLARE #Counter tinyint = 0;
IF OBJECT_ID('TEMPDB..#ITEM') IS NOT NULL
DROP TABLE #ITEM
CREATE TABLE #ITEM
(
ID int not null
,ParentID int
,Name VARCHAR(MAX)
,lvl int not null
,RootID int not null
)
INSERT INTO #ITEM
(ID,lvl,ParentID,Name,RootID)
SELECT Id
,0 AS LVL
,ParentId
,Name
,Id AS RootID
FROM
#table
WHERE
ISNULL(ParentId,-1) = -1
WHILE ##ROWCOUNT > 0
BEGIN
SET #Counter += 1
insert into #ITEM(ID,ParentId,Name,lvl,RootID)
SELECT ci.ID
,ci.ParentId
,ci.Name
,#Counter as cntr
,ch.RootID
FROM
#table AS ci
INNER JOIN
#ITEM AS pr
ON
CI.ParentId=PR.ID
LEFT OUTER JOIN
#ITEM AS ch
ON ch.ID=pr.ID
WHERE
ISNULL(ci.ParentId, -1) > 0
AND PR.lvl = #Counter - 1
END
select * from #ITEM

Here is an example of functional rcte based on your sample data and requirements as I understand them.
if OBJECT_ID('tempdb..#Something') is not null
drop table #Something
create table #Something
(
id int
, parentId int
, name char(1)
)
insert #Something
select 1, NULL, 'A' union all
select 2, 1, 'B' union all
select 3, 2, 'C' union all
select 4, 1, 'E' union all
select 5, 3, 'E'
declare #Root char(1) = 'A';
with MyData as
(
select *
from #Something
where name = #Root
union all
select s.*
from #Something s
join MyData d on d.id = s.parentId
)
select *
from MyData
Note that if you change the value of your variable the output will adjust. I would make this an inline table valued function.

I think I have it based on #SeanLange's recommendation to use a recursive CTE (above in the comments):
CREATE FUNCTION GetID
(
#path VARCHAR(MAX)
)
/* TEST:
SELECT dbo.GetID('A/B/C/E')
*/
RETURNS INT
AS
BEGIN
DECLARE #ID INT;
WITH cte AS (
SELECT p.id ,
p.parentId ,
CAST(p.name AS VARCHAR(MAX)) AS name
FROM tblT p
WHERE parentId IS NULL
UNION ALL
SELECT p.id ,
p.parentId ,
CAST(pcte.name + '/' + p.name AS VARCHAR(MAX)) AS name
FROM dbo.tblT p
INNER JOIN cte pcte ON
pcte.id = p.parentId
)
SELECT #ID = id
FROM cte
WHERE name = #path
RETURN #ID
END

Related

Search list of values including range in SQL using WHERE IN clause with SQL variable?

I am trying to implement search functionality with list of values in SQL variable, including range. Appreciate any guidance/links pointing to correct approach for this.
Below is the dataset:
CREATE TABLE [dbo].[Books]
(
[ID] [NCHAR](10) NOT NULL,
[AUTHCODE] [NCHAR](10) NULL,
[TITLE] [NCHAR](10) NULL
) ON [PRIMARY]
GO
INSERT [dbo].[Books] ([ID], [AUTHCODE], [TITLE])
VALUES (N'1', N'nk', N'Book1'),
(N'2', N'an', N'Book2'),
(N'3', N'mn', N'Book3'),
(N'4', N'ra', N'Book4'),
(N'5', N'kd', N'Book5'),
(N'6', N'nk', N'Book6'),
(N'7', N'an', N'Book7'),
(N'8', N'ra', N'Book8'),
(N'9', N'kd', N'Book9'),
(N'10', N'mn', N'Book10 ')
GO
Below I am trying to filter using the SQL IN clause but this does not return desired result.
select * from books
declare #List1 varchar(max) = '2,4,6,7,8,9' --simple list
select *
from books
where id in (#List1)
declare #List2 varchar(max) = '2,4-7,9' --list with range
select *
from books
where id in (#List2)
You cannot directly use strings as lists, but you can do use STRING_SPLIT (Transact-SQL) if you really need to pass filtering parameters as strings:
declare #list varchar(max) = '2,4,6-8,9'
declare #filter table (id1 int, id2 int)
insert into #filter (id1,id2)
select
case when b.pos > 0 then left(a.[value], pos - 1) else a.[value] end as id1,
case when b.pos > 0 then right(a.[value], len(a.[value]) - pos) else a.[value] end as id2
from string_split(#list, ',') as a
cross apply (select charindex('-', a.[value]) as pos) as b
select *
from [dbo].[Books] as b
where
exists (select * from #filter as tt where b.id between tt.id1 and tt.id2)
Also it might be an idea to pass your filter as json and OPENJSON (Transact-SQL) so you can make parsing part simplier:
declare #list varchar(max) = '[2,4,[6,8],9]'
select
case when a.[type] = 4 then json_value(a.[value], '$[0]') else a.[value] end,
case when a.[type] = 4 then json_value(a.[value], '$[1]') else a.[value] end
from openjson(#list) as a
All above, of course, only applicable if you have Sql Server 2016 or higher
IN() operator determines whether a specified value matches any value in a subquery or a list. not a string and that what you are doing.
What you are trying to do can be done as
DECLARE #List1 AS TABLE (ID INT);
INSERT INTO #List1
SELECT 2 UNION SELECT 4 UNION SELECT 6 UNION
SELECT 7 UNION SELECT 8 UNION SELECT 9;
SELECT *
FROM Books
WHERE ID IN (SELECT ID FROM #List1);
DECLARE #List2 As TABLE (AFrom INT, ATo INT);
INSERT INTO #List2
SELECT 2, 4 UNION SELECT 7, 9;
SELECT *
FROM Books B CROSS APPLY #List2 L
WHERE B.ID BETWEEN L.AFrom AND L.ATo;
Live Demo

SQL Server: Replace values in field using lookup other table

Let's say I have below table script
DECLARE #result TABLE
(
[ID] Int
,[Data] Varchar(500)
)
DECLARE #codes TABLE
(
[ID] Varchar(500)
,[FullNames] Varchar(500)
)
INSERT INTO #result
SELECT 1
,'[A]-[B]'
INSERT INTO #result
SELECT 2
,'[D]-[A]'
INSERT INTO #result
SELECT 3
,'[A]+[C]'
INSERT INTO #codes
SELECT 'A'
,'10'
INSERT INTO #codes
SELECT 'B'
,'20'
INSERT INTO #codes
SELECT 'C'
,'30'
INSERT INTO #codes
SELECT 'D'
,'40'
SELECT * FROM #result
SELECT * FROM #codes
Output of those are as below:
#result
ID Data
-- -------
1 [A]-[B]
2 [D]-[A]
3 [A]+[C]
#codes
ID FullNames
-- -------
A 10
B 20
C 30
D 40
Now I want output as below also:
ID Data
-- -----
1 10-20
2 40-10
3 10+30
Please help me.
Please note: Data columns also contains ([A]-[B]+[D])*[C]
I found similar solution on https://stackoverflow.com/a/26650255/8454103 which is for your reference.
Try like this;
select Output from (
select Data, c1.FullNames as LeftSideName, c2.FullNames as RightSideName, LeftSide, RightSide, REPLACE(REPLACE(Data,'[' + LeftSide + ']',c1.FullNames),'[' + RightSide + ']',c2.FullNames) as Output from (
select r.ID, Data,SUBSTRING(Data, 2, 1) LeftSide ,SUBSTRING(Data, 6, 1) RightSide from #result r ) Result
inner join #codes c1 ON Result.LeftSide = c1.ID
inner join #codes c2 ON Result.RightSide = c2.ID)
Records
Output:
10-20
10+30
40-10
The query can perform replacing dynamically A,B,C,D,E according to #codes table etc.
Try this:
select ID, REPLACE(REPLACE(REPLACE(REPLACE(Data,
'[A]', (select FullNames from #codes where ID = 'A')),
'[B]', (select FullNames from #codes where ID = 'B')),
'[C]', (select FullNames from #codes where ID = 'C')),
'[D]', (select FullNames from #codes where ID = 'D'))
from #result

SQL Server: Sum of childs column (sum only until no negative value left for childs)

again i have got a small (i hope small) problem.
I have got a parent child hirarchy where one parent can have multiple childs and a child can have again multiple childs and so on.
every parent and child has a amount (value) and a parent can compensate for any missing amount of the childrens.
Here my Table:
CREATE TABLE #Test
(
ID INTEGER NOT NULL
,ParentID INTEGER
,NAME VARCHAR(20)
,value INTEGER
)
Testdata:
INSERT INTO #Test
( ID, ParentID, NAME, value )
VALUES ( 1, NULL, 'MainStore', 1 )
, ( 2, 1, 'Substore1', 3 )
, ( 3, 1, 'Substore2', 10 )
, ( 4, 2, 'Sub1Substore1', -1 )
, ( 5, 2, 'Sub1Substore2', 1 )
, ( 6, 3, 'Sub2Substore1', 10 )
To get the parentchild realationship displayed ive tried it with an CTE:
;WITH CTE
AS ( SELECT ID
,ParentID
,Name
,Value
,0 AS LEVEL
,CAST('' AS INTEGER) AS ID_Parent
FROM #Test
WHERE ParentID IS NULL
UNION ALL
SELECT child.ID
,child.ParentID
,child.Name
,child.Value
,parent.Level + 1
,parent.ID
FROM CTE parent
JOIN #Test child ON child.ParentID = parent.ID
)
As you can see Substore1 has 2 childrens (Sub1Substore1 and Sub1Substore2) Substore1 hast a value of 3, Sub1Substore1 -1 and Sub1Substore2 has 1.
Sub1Substore1 is a child of Substore1 and the parent can compensate for missing values of the childs.
My desired output should look like this:
ID ParentID Name Value LEVEL ID_Parent FreeValues
----------- ----------- -------------------- ----------- ----------- ----------- -----------
1 NULL MainStore 1 0 0 1
2 1 Substore1 3 1 1 2
3 1 Substore2 10 1 1 8
4 2 Sub1Substore1 -1 2 2 0
5 2 Sub1Substore2 1 2 2 1
6 3 Sub2Substore1 -2 2 3 0
Sadly the SQL Fiddle Website is not working for me at the moment but i will provide this sample later on SQL Fiddle.
EDIT: Rewrote whole answer due to misunderstandment of the task.
This might be solvable elegantly with common table expression, but because CTE lacks support for multiple recursive references, this task seemed to became way too complex for me to handle.
However, here's a bit less elegant solution that should do the trick for you. Note that I made an assumption that parent's ID is always smaller than it's direct childrens'. This might become an issue if you should be able to change already inserted row's parent "on-the-fly". Anyway, here you go:
--Declare temp table.
DECLARE #Temp TABLE
(
ID INTEGER NOT NULL
,ParentID INTEGER
,NAME VARCHAR(20)
,value INTEGER
,FreeValues INTEGER
,NeedFromParent INTEGER
,ChildrenNeed INTEGER
);
--Other variables
DECLARE #ID INTEGER
DECLARE #ParentID INTEGER
DECLARE #Name VARCHAR(20)
DECLARE #value INTEGER
DECLARE #FreeValues INTEGER
DECLARE #NeedFromParent INTEGER
DECLARE #ChildrenNeed INTEGER
--Loop with cursor to calculate FreeValues
DECLARE cur CURSOR FOR SELECT id, parentId, Name, Value FROM #test
ORDER BY ID DESC -- NOTE! Assumed that Parent's ID < Child's ID.
OPEN cur
FETCH NEXT FROM cur INTO #ID, #ParentID, #Name, #value
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT #ChildrenNeed = CASE
WHEN SUM(temp.NeedFromParent) IS NULL THEN 0
ELSE SUM(temp.NeedFromParent) END
FROM #temp temp WHERE temp.ParentID=#ID AND temp.NeedFromParent > 0
IF #ChildrenNeed IS NULL SET #ChildrenNeed = 0
IF #Value - #ChildrenNeed < 0
SET #NeedFromParent = #Value - #ChildrenNeed
ELSE
SET #NeedFromParent = 0
SET #NeedFromParent = -#NeedFromParent
IF #NeedFromParent = 0
SET #FreeValues = #value - #ChildrenNeed
ELSE
SET #FreeValues = 0
INSERT INTO #Temp
VALUES(#ID, #ParentID, #Name, #value, #FreeValues, #NeedFromParent, #ChildrenNeed)
FETCH NEXT FROM cur INTO #ID, #ParentID, #Name, #value
END
CLOSE cur;
DEALLOCATE cur;
-- Join with recursively calculated Level.
;WITH CTE
AS ( SELECT ID ,ParentID,0 AS [Level]
FROM #Test WHERE ParentID IS NULL
UNION ALL
SELECT child.ID,child.ParentID,parent.Level + 1
FROM CTE parent INNER JOIN #Test child ON child.ParentID = parent.ID
)
SELECT t1.ID, t1.ParentID, t1.Name, t1.Value, cte.[Level], t1.FreeValues
FROM CTE cte LEFT JOIN #temp t1 ON t1.ID = cte.ID
ORDER BY ID

How can I "Delete All" but only when a column has a certain value?

I need to MERGE a set of records into a table. The UpdateType column in the source determines if I should DELETE rows in the target that are not in the source.
So UpdateType will equal D or R... D=Delta, R=Refresh
If D, do NOT DELETE non matching from target
If R, DO DELETE non matching from target.
I have a WHILE that iterates over a single table, to just better mimic how the process works.
Can I accomplish this in a MERGE? Or what other options do I have?
SQL Fiddle: http://www.sqlfiddle.com/#!3/9cdfe/16
Here is my example, the only problem is... I can't use a source value in the WHEN NOT MATCHED BY SOURCE clause.
DECLARE #BaseTable TABLE
( RN int
,Store int
,UpdateType char(1)
,ItemNumber int
,Name varchar(50)
)
INSERT INTO #BaseTable
SELECT *
FROM
(
SELECT 1 RN, 1 Store, 'D' UpdateType, 1 ItemNumber, 'Wheel' Name
UNION ALL
SELECT 2, 1, 'D', 1, 'Big Wheel'
UNION ALL
SELECT 3, 1, 'D', 2, 'Light'
UNION ALL
SELECT 4, 1, 'R', 1, 'Wide Wheel'
UNION ALL
SELECT 5, 1, 'D', 1, 'Small Wheel'
UNION ALL
SELECT 5, 1, 'D', 4, 'Trunk'
)B
SELECT bt.* FROM #BaseTable bt
DECLARE #Tab TABLE
( Store int
,UpdateType char(1)
,ItemNumber int
,Name varchar(50)
)
DECLARE #count int = 1
--Loop over each row to mimic how the merge will be called.
WHILE #count <= 5
BEGIN
MERGE INTO #Tab T
USING
(
SELECT bt.RN,
bt.Store,
bt.UpdateType,
bt.ItemNumber,
bt.Name,
tab.Store IsRefresh
FROM #BaseTable bt
LEFT JOIN
( --If ANY previous ITERATION was a 'R' then, all subsequent UpdateType MUST = 'R'
--I'm hoping there is a better way to accomplish this.
SELECT Store
FROM #Tab
WHERE UpdateType = 'R'
GROUP BY Store
HAVING COUNT(Store) > 1
)tab
ON bt.Store = tab.Store
WHERE bt.RN = #count
)S
ON S.Store = T.Store AND S.ItemNumber = T.ItemNumber
WHEN MATCHED THEN
UPDATE
SET T.UpdateType = CASE WHEN S.IsRefresh IS NOT NULL THEN 'R' ELSE S.UpdateType END,
T.Name = S.Name
WHEN NOT MATCHED BY TARGET THEN
INSERT(Store,UpdateType,ItemNumber,Name) VALUES(S.Store,S.UpdateType,S.ItemNumber,S.Name)
--WHEN NOT MATCHED BY SOURCE AND S.UpdateType = 'R' THEN
-- DELETE
;
SET #count = #count + 1
END
SELECT * FROM #Tab
--#Tab Expected Result:
-- 1 'R' 1 'Small Wheel'
-- 1 'R' 4 'Trunk'
The following code appears to do what you want:
CREATE TABLE #BaseTable
( RN int
,Store int
,UpdateType char(1)
,ItemNumber int
,Name varchar(50)
)
INSERT INTO #BaseTable
SELECT *
FROM
(
SELECT 1 RN, 1 Store, 'D' UpdateType, 1 ItemNumber, 'Wheel' Name
UNION ALL
SELECT 2, 1, 'D', 1, 'Big Wheel'
UNION ALL
SELECT 3, 1, 'D', 2, 'Light'
UNION ALL
SELECT 4, 1, 'R', 1, 'Wide Wheel'
UNION ALL
SELECT 5, 1, 'D', 1, 'Small Wheel'
UNION ALL
SELECT 5, 1, 'D', 4, 'Trunk'
)B
CREATE TABLE #Tab
( Store int
,UpdateType char(1)
,ItemNumber int
,Name varchar(50)
)
SELECT bt.* FROM #BaseTable bt -- Output for debugging - delete in production.
DECLARE #count int = 1
DECLARE #Input TABLE (Store int, UpdateType char(1), ItemNumber int, Name varchar(50))
--Loop over each row to mimick how the merge will be called.
WHILE #count <= 5
BEGIN
DELETE FROM #Input
INSERT INTO #Input SELECT Store, UpdateType, ItemNumber, Name FROM #BaseTable WHERE RN = #Count
SELECT * FROM #Input -- Output for debugging - delete in production.
-- Procedure Body
DECLARE #Store int, #UpdateType char(1), #ItemNumber int, #Name varchar(50)
DECLARE csrInput CURSOR FOR SELECT Store, UpdateType, ItemNumber, Name FROM #Input
OPEN csrInput
WHILE 1=1
BEGIN
FETCH NEXT FROM csrInput INTO #Store, #UpdateType, #ItemNumber, #Name
IF ##FETCH_STATUS<>0 BREAK
IF #UpdateType = 'D'
MERGE INTO #Tab Dest
USING (SELECT * FROM #Input WHERE Store = #Store AND ItemNumber = #ItemNumber) Src
ON Dest.Store = Src.Store AND Dest.ItemNumber = Src.ItemNumber
WHEN MATCHED THEN UPDATE SET Dest.UpdateType = Src.UpdateType, Dest.Name = Src.Name
WHEN NOT MATCHED BY TARGET THEN INSERT (Store, UpdateType, ItemNumber, Name) VALUES (Src.Store, Src.UpdateType, Src.ItemNumber, Src.Name);
ELSE -- Assuming that #UpdateType can only be 'D' or 'R'...
MERGE INTO #Tab Dest
USING (SELECT * FROM #Input WHERE Store = #Store AND ItemNumber = #ItemNumber) Src
ON Dest.Store = Src.Store AND Dest.ItemNumber = Src.ItemNumber
WHEN MATCHED THEN UPDATE SET Dest.UpdateType = Src.UpdateType, Dest.Name = Src.Name
WHEN NOT MATCHED BY TARGET THEN INSERT (Store, UpdateType, ItemNumber, Name) VALUES (Src.Store, Src.UpdateType, Src.ItemNumber, Src.Name)
WHEN NOT MATCHED BY SOURCE THEN DELETE;
SELECT * FROM #Tab -- Output for debugging - delete in production.
END
CLOSE csrInput
DEALLOCATE csrInput
-- End Procedure Body.
SET #count += 1
END
Final output:
Store UpdateType ItemNumber Name
1 D 1 Small Wheel
1 D 4 Trunk

Add null's for numerics using CTE in Sql Server 2005

I have some data as below
ID Data
1 a
1 2
1 b
1 1
2 d
2 3
2 r
Desired Output
ID Data
1 a
1 NULL
1 NULL
1 b
1 NULL
2 d
2 NULL
2 NULL
2 NULL
2 r
What the output is , for the numerics it is replaced with those many null values. E.g. for a numeric value of 2 , it will be 2 null values.
The ddl is as under
Declare #t table(ID int, data varchar(10))
Insert into #t
Select 1, 'a' union all select 1, '2' union all select 1, 'b' union all select 1, '1' union all select 2,'d' union all
select 2,'3' union all select 2, 'r'
select * from #t
Please give a CTE based solution. I already have the procedural approach but I need to have a feel of CTE based solution.
Solution I am using at present
CREATE FUNCTION [dbo].[SPLIT] (
#str_in VARCHAR(8000)
)
RETURNS #strtable TABLE (id int identity(1,1), strval VARCHAR(8000))
AS
BEGIN
DECLARE #tmpStr VARCHAR(8000), #tmpChr VARCHAR(5), #ind INT = 1, #nullcnt INT = 0
SELECT #tmpStr = #str_in
WHILE LEN(#tmpStr) >= #ind
BEGIN
SET #tmpChr = SUBSTRING(#tmpStr,#ind,1)
IF ISNUMERIC(#tmpChr) = 0
INSERT INTO #strtable SELECT #tmpChr
ELSE
WHILE #nullcnt < #tmpChr
BEGIN
INSERT INTO #strtable SELECT NULL
SET #nullcnt = #nullcnt + 1
END
SELECT #ind = #ind + 1, #nullcnt = 0
END
RETURN
END
GO
Invocation
SELECT * from dbo.SPLIT('abc2e3g')
I donot want to do it using function but a CTE solution.
Reason: I am learning CTE and trying to solve problems using it. Trying to come out of procedural/rBar approach.
Thanks
Setup:
declare #t table(UniqueID int identity(1,1), ID int, data varchar(10))
insert into #t
select 1, 'a' union all
select 1, '2' union all
select 1, 'b' union all
select 1, '1' union all
select 2, 'd' union all
select 2, '3' union all
select 2, 'r'
Query:
;with cte
as
(
select UniqueId, id, data, case when isnumeric(data) = 1 then cast(data as int) end Level
from #t
union all
select UniqueId, id, null, Level - 1
from cte
where Level > 0
)
select id, data
from cte
where Level is null or data is null
order by UniqueID, Level
Output:
id data
----------- ----------
1 a
1 NULL
1 NULL
1 b
1 NULL
2 d
2 NULL
2 NULL
2 NULL
2 r
I added UniqueId as there should be some kind of field that specifies order in the original table.
If you have a numbers table, this becomes quite simple.
;WITH Nbrs ( Number ) AS (
SELECT 1 UNION ALL
SELECT 1 + Number FROM Nbrs WHERE Number < 8000 )
SELECT
M.UniqueID, M.ID, Data
FROM
#t M
WHERE
ISNUMERIC(M.Data) = 0
UNION ALL
SELECT
M.UniqueID, M.ID, NULL
FROM
#t M
JOIN --the <= JOIN gives us (M.Data) rows
Nbrs N ON N.Number <= CASE WHEN ISNUMERIC(M.Data) = 1 THEN M.Data ELSE NULL END
ORDER BY
UniqueID
OPTION (MAXRECURSION 0)
Edit: JOIN was wrong way around. Oops.
Numbers CTE taken from here