Get total number of parents - sql-server

I am upgrading a legacy application and I need to find the number of parents certain rows in a table have.
I was thinking of using a declared procedure for this, but i'm having trouble figuring it out how to make it work.
Basically you have a table with id and parent
= id = parent =
= 0 = 0 =
= 1 = 0 =
= 2 = 1 =
= 3 = 1 =
= 4 = 2 =
ID is unique, and parent is variable depending on what was used to create that entry, but it will always match a row with matching ID
What i'd like to achieve is calling one procedure that returns all matching parent numbers as a simple iterable result set.
So if i were to do getAllParents(4) it should return me 2, 1, 0
My failed attempts at looping have brought me so far
CREATE PROCEDURE getNumberOfParents #start int #current int
as
begin
SELECT parent FROM test where id=#start
if(parent > 0)
begin
set #current = #current + 1;
set #current = #current + getNumberOfParents(parent,#current);
end
end
Due to restrictions I cannot use an extra table to achieve this, otherwise i'd be easy heh. i can however make temptables that can be cleaned up after the method exits.

You can do it without a while loop by the use of a recursive CTE:
DECLARE #lookupId INT = 4
;WITH ParentsCTE AS (
SELECT id, parent
FROM #mytable
WHERE id = #lookupId
UNION ALL
SELECT m.id, m.parent
FROM #mytable AS m
INNER JOIN ParentsCTE AS p ON m.id = p.parent
WHERE m.id <> m.parent
)
SELECT parent
FROM ParentsCTE
The anchor member of the above CTE:
SELECT id, parent
FROM #mytable
WHERE id = #lookupId
returns the immediate parent of the 'lookup id'.
The recursive member:
SELECT m.id, m.parent
FROM #mytable AS m
INNER JOIN ParentsCTE AS p ON m.id = p.parent
WHERE m.id <> m.parent
keeps adding parents up the hierarchy until a root node (m.id <> m.parent predicate detects this) has been reached.
SQL Fiddle Demo

Test Data
DECLARE #Table TABLE (id INT, parent INT)
INSERT INTO #Table VALUES
(0 , 0),
(1 , 0),
(2 , 1),
(3 , 1),
(4 , 2)
Query
-- Id you want all the parents for
DECLARE #ParentsFor INT = 4
;with parents as
(
select ID, parent
from #Table
where parent IS NOT NULL
union all
select p.ID, t.parent
from parents p
inner join #Table t on p.parent = t.ID
and t.ID <> t.parent
)
select Distinct
STUFF((SELECT ',' + Cast(parent AS VarChar(10))
FROM parents
WHERE ID = #ParentsFor
FOR XML PATH(''),TYPE).value('.','NVARCHAR(MAX)'),1,1,'')
FROM parents
Result
2,1,0
SQL FIDDLE

Fiddle Demo Here
try:
declare #id varchar(max)=''
declare #getid int=4
while #getid>0
begin
select #id=#id+cast(parent as varchar(10))+',' from tab_1
where id=#getid
select #getid=parent from tab_1 where id=#getid
end
select #id

Try to use while loop as presented in this example:
http://blog.sqlauthority.com/2007/10/24/sql-server-simple-example-of-while-loop-with-continue-and-break-keywords/

Related

Multiple select queries execution one after other

I am having six select queries with different where conditions if first select query returns null it should check the next select query and follows. what is the best approach to follow for writing it as stored procedure in SQL server.
You can use ##rowcount
DECLARE #OperatorID INT = 4, #CurrentCalendarView VARCHAR(50) = 'month';
declare #t table (operatorID int, CurrentCalendarView varchar(50));
insert into #t values (2, 'year');
select operatorID - 1, CurrentCalendarView from #t where 1 = 2
if (##ROWCOUNT = 0)
begin
select operatorID + 1, CurrentCalendarView from #t where 1 = 1
end
If I understand your question correctly then you can achieve this like below sample. You can go in this way.
if NOT EXISTS (SELECT TOP(1) 'x' FROM table WHERE id =#myId)
BEGIN
IF NOT EXISTS (SELECT TOP(1) 'x' FROM table2 WHERE id = #myId2)
BEGIN
IF NOT EXISTS (SELECT TOP(1) 'x' FROM table 3 WHERE id = #myID3)
BEGIN
END
END
END

T-SQL Foreach row if column has value 0

I'm new in working with SQL queries and I'm trying to check if for each parent from the Parents table I have the age for all children from Children table. For example:
Parents
Name Id
John Smith 7
Children
Name Age ParentId
Sasha Smith 10 7
Johnny Smith 0 7
This is what I have so far:
create function fnCheckChildren(
#parentId int
)
returns int
begin
declare #allOk int
if(select count(1) from Children where ParentId=#parentId ) = 0
BEGIN
SET #allOk =1 --default all are ok - parent has no children
END
else
BEGIN
--here is my missing part
END
return #allOk
end
go
If I'll call the function for John Smith (#parentId = 7) #allOk should have the value 0 because one of his children (Johnny) has Age 0. If all had Age > 0 then #allOk should have 1.
Can anyone help me with it? As I understand, T-SQL doesn't support For Each...Next, and so on, so what alternatives do I have?
Here is a relative simple and efficient method:
select p.*,
(case when exists (select 1 from children c where c.parentid = p.id and c.age = 0
then 1 else 0
end) as flag
from p;
You can make this a function by simply doing:
create function fnCheckChildren(
#parentId int
)
returns int
begin
if (exists (select 1 from children where parentid = #parentid and age = 0)
)
begin
return(0);
end;
return(1);
end;
There is no need for CURSOR at all:
SELECT *
FROM Parents p
OUTER APPLY (SELECT CASE WHEN
COUNT(*) = COUNT(CASE WHEN c.Age > 0 THEN 1 END) THEN 1
ELSE 0
END AS res
FROM Children c
WHERE p.id = c.parent_id
) sub
-- WHERE ....;
DBFiddle Demo
As I understand, T-SQL doesn't support For Each.
This is not true, For-Each is kind of correlated subquery for instance: using inline syntax like in Gordon's answer or OUTER APPLY.
You can try the following query
CREATE FUNCTION fnCheckChildren(
#parentId INT
)
RETURNS INT
BEGIN
DECLARE #allOk INT
SET #allOk = CASE WHEN (select count(*) from Children where ParentId=#parentId AND Age > 0) > 1 THEN 1
ELSE 0 END
RETURN #allOk
END
Thanks
You could also have a case of no children at all. This will indicate that.
declare #P table (id int identity primary key, name varchar(20));
insert into #P (name) values ('John Smith'), ('Pat Jones'), ('Will Smith');
select * from #P;
declare #C table (id int identity primary key, parID int, name varchar(20), age int);
insert into #C (parID, name, age) values (1, 'jim', 12), (1, 'sally', 0), (2, 'bruce', 10);
select * from #C;
select p.id, P.name
, count(c.id) as [child count]
, case when MIN(c.age) <= 0 then 'childen with zero' else 'no children with zero' end as status
from #P p
left join #C c
on p.id = c.parID
group by p.id, p.name;

Creating duplicates with a different ID for test in SQL

I have a table with 1000 unique records with one of the field as ID. For testing purpose, my requirement is that To update the last 200 records ID value to the first 200 records ID in the same table. Sequence isn't mandatory.
Appreciate help on this.
Typically I charge for doing other ppls homework, don't forget to cite your source ;)
declare #example as table (
exampleid int identity(1,1) not null
, color nvarchar(255) not null
);
insert into #example (color)
select 'black' union all
select 'green' union all
select 'purple' union all
select 'indigo' union all
select 'yellow' union all
select 'pink';
select *
from #example;
declare #max int = (select max(exampleId) from #example);
declare #min int = #max - 2
;with cte as (
select top 2 color
from #example
)
update #example
set color = a.color
from cte a
where exampleid <= #max and exampleid > #min;
select *
from #example
This script should solve the issue and will cover scenarios even if the id column is not sequential.I have included the comments to help you understand the joins and the flow of the script.
declare #test table
(
ID int not null,
Txt char(1)
)
declare #counter int = 1
/*******This variable is the top n or bottom n records in question it is 200 ,
for test purpose setting it to 20
************/
declare #delta int = 20
while(#counter <= 50)
begin
Insert into #test values(#counter * 5,CHAR(#counter+65))
set #counter = #counter + 1
end
/************Tag the records with a row id as we do not know if ID's are sequential or random ************/
Select ROW_NUMBER() over (order by ID) rownum,* into #tmp from #test
/************Logic to update the data ************/
/*Here we first do a self join on the tmp table with first 20 to last 20
then create another join to the test table based on the ID of the first 20
*/
update t
set t.ID = tid.lastID
--Select t.ID , tid.lastID
from #test t inner join
(
Select f20.rownum as first20 ,l20.rownum as last20,f20.ID as firstID, l20.ID lastID
from #tmp f20
inner join #tmp l20 on (f20.rownum + #delta) = l20.rownum
)tid on tid.firstID = t.ID and tid.first20 < = #delta
Select * from #test
drop table #tmp

Stored Procedure Syntax with CTE

This is probably trivial but I am just learning about CTE (thanks to help here).
I have a procedure that is used to determine totals.
The first part is the totals are the sum of a position at their level an below. So I needed a way to retrieve records that (1) determined the level of the record (hierarchy) and (2) returned all records at and below. That was asked and answered here.
Now want to take the CTE table from the answer above and use it in the second part of my procedure (get the totals)
CREATE PROCEDURE [dbo].[GetProgramTotals]
#programId nvarchar(10) = null,
#owner int = null,
#totalAmount money OUT,
#usedAmount money OUT,
#remainingAmount money OUT
AS
BEGIN
WITH rCTE AS
(
SELECT
*, 0 AS Level
FROM Forecasting.dbo.Addressbook
WHERE Addressbook = #owner
UNION ALL
SELECT
t.*, r.Level + 1 AS Level
FROM Addressbook t
INNER JOIN rCTE r ON t.ParentAddressbook = r.Addressbook
)
Select #totalAmount = (Select Sum(Amount) from dbo.Budget where
(#programId IS NULL or (ProgramId = #programId)) and (#owner IS NULL or (BudgetOwner in (SELECT Addressbook from rCTE))))
Select #usedAmount = (Select Sum(SubTotal) from dbo.OrderLine where
(#programId IS NULL or (ProgramId = #programId) and (#owner IS NULL) or (Budget in (SELECT Addressbook from rCTE))))
if (#totalAmount is null)
set #totalAmount = 0
if (#usedAmount is null)
set #usedAmount = 0
Set #remainingAmount = (#totalAmount - #usedAmount)
END
The idea of this procedure is the dynamically calculate an individual (or all) programs based an a users position in a hierarchy.
So a regional managers totals would be the sum of all districts and district reps.
UPDATE: I updated this based on squillman (thank you) comment below.
Now I have a different problem. When I execute the proc - I get 'Invalid object name rCTE'.
You can't use SET in the middle of a query like that. Change it to a SELECT and it should remedy your syntax error.
CREATE PROCEDURE [dbo].[GetProgramTotals]
#programId nvarchar(10) = null,
#owner int = null,
#totalAmount money OUT,
#usedAmount money OUT,
#remainingAmount money OUT
AS
BEGIN
WITH rCTE AS(
SELECT *, 0 AS Level FROM Forecasting.dbo.Addressbook WHERE Addressbook = #owner
UNION ALL
SELECT t.*, r.Level + 1 AS Level
FROM Addressbook t
INNER JOIN rCTE r ON t.ParentAddressbook = r.Addressbook)
SELECT #totalAmount = (Select Sum(Amount) from dbo.Budget where
(#programId IS NULL or (ProgramId = #programId)) and (#owner IS NULL or (BudgetOwner in (SELECT Addressbook from rCTE))))
, #usedAmount = (Select Sum(SubTotal) from dbo.OrderLine where
(#programId IS NULL or (ProgramId = #programId) and (#owner IS NULL) or (Budget in (SELECT Addressbook from rCTE))))
if (#totalAmount is null)
set #totalAmount = 0
if (#usedAmount is null)
set #usedAmount = 0
Set #remainingAmount = (#totalAmount - #usedAmount)
END
CTE's can be a bit confusing at first, but they are really quite simple once they make sense. For me it clicked when I began thinking of them as just another temp table syntax (pro-tip: they're not in reality, just conceptually). So basically:
Create one or more "temp tables". These are your CTE expressions, and there can be more than one.
Perform a standard operation using one or more of the CTE expressions in the statement immediately following your CTE(s).
As Martin mentioned in comments below, the CTE(s) are only scoped for the next immediate statement and fall out of scope after that.
So,
;WITH cte1 AS
(
SELECT Col1 FROM Table1
),
cte2 AS
(
SELECT Col1 FROM Table2
)
SELECT Col1 FROM cte1 //In scope here
UNION
SELECT Col1 FROM cte1; //Still in scope since we're still in the first statement
SELECT Col1 FROM cte1; //Fails. cte1 is now out of scope (as is cte2)
In your case you're using the recursive CTE to form a parent/child hierarchy and then setting variables based on the results. Your CTE syntax is pretty close after the edit, you just need the comma to bring things back together into one statement.
//Variable assignment example
;WITH cte1 AS
(
SELECT Col1 FROM Table1
),
cte2 AS
(
SELECT Col1 FROM Table2
)
SELECT #var1 = (SELECT TOP 1 Col1 FROM cte1)
,#var2 = (SELECT TOP 1 Col1 FROM cte2) //You're missing the comma at the start of this line
Change Select #usedAmount=... to , #usedAmount=...

Common Table Expressions to retrieve path

I'm trying to use a recursive query to find a path through a table structured like this:
RelatedEntities
FromKey TINYINT
ToKey TINYINT
...more....
I thought I could do something like this:
DECLARE #startKey UNIQUEIDENTIFIER, #endKey UNIQUEIDENTIFIER;
SET #startKey = 0;
SET #endKey = 3;
;With findPath
AS
(
SELECT FromKey, ToKey
FROM RelatedEntities
WHERE FromKey = #startKey
UNION ALL
SELECT FromKey, ToKey
FROM RelatedEntities r
JOIN findPath b
ON r.FromKey = b.ToKey
AND r.FromKey NOT IN (SELECT FromKey FROM b)
)
SELECT * FROM findPath;
This code fails because I cannot use a subquery within a CTE. It also seems to be a rule that the recursive query can only contain one reference to the CTE. (true?) Maybe this is a job for a cursor, or procedural code, but I thought I would put it out here in case I'm missing a way to find a path through a table with a CTE?
The parameters are:
Start with a beginning and ending key
Base query uses the beginning key
Recursive query should stop when it contains the ending key, (have not been able to figure that one out) and should not repeat start keys.
A MAXRECURSION option could be used to stop after a certain number of iterations.
Thanks to all of you CTE gurus out there. Set me straight.
Changing this from UNIQUEIDENTIFIERS to TINYINT for readability. The SQL constructs are the same. Here's some code to test it.
CREATE TABLE RelatedEntities(FromKey TINYINT, ToKey TINYINT);
INSERT RelatedEntities(FromKey, ToKey)
VALUES
(1, 0),
(0, 1),
(1, 7),
(7, 1),
(3, 4),
(4, 3)
;With FindPath
AS
(
SELECT FromKey, ToKey, 0 AS recursionLevel
FROM RelatedEntities
WHERE FromKey = 1
UNION ALL
SELECT r.FromKey, r.ToKey, recursionLevel = recursionLevel + 1
FROM RelatedEntities r
INNER JOIN FindPath b ON r.FromKey = b.ToKey
WHERE b.ToKey <> 3 AND RecursionLevel < 10
)
SELECT * FROM FindPath
ORDER BY recursionLevel
Note that this returns from 1, to 0, then from 0, to 1, and repeats until I run out of recursion levels.
You need to modify your query like this:
DECLARE #startKey UNIQUEIDENTIFIER, #endKey UNIQUEIDENTIFIER;
DECLARE #maxRecursion INT = 100
SET #startKey = '00000000-0000-0000-0000-000000000000';
SET #endKey = 'F7801327-C037-AA93-67D1-B7892F6093A7';
;With FindPath
AS
(
SELECT FromKey, ToKey, 0 AS recursionLevel
FROM RelatedEntities
WHERE FromKey = #startKey
UNION ALL
SELECT r.FromKey, r.ToKey, recursionLevel = recursionLevel +1
FROM RelatedEntities r
INNER JOIN FindPath b ON r.FromKey = b.ToKey
WHERE b.ToKey <> #endKey AND recursionLevel < #maxRecursion
)
SELECT * FROM FindPath;
The anchor member of the above CTE:
SELECT FromKey, ToKey, 0 AS recursionLevel
FROM RelatedEntities
WHERE FromKey = #startKey
will select the starting record, T0, of the (From, To) chain of records.
The recursive member of the CTE:
SELECT r.FromKey, r.ToKey, recursionLevel = recursionLevel +1
FROM RelatedEntities r
INNER JOIN FindPath b ON r.FromKey = b.ToKey
WHERE b.ToKey <> #endKey AND recursionLevel < #maxRecursion
will be executed with T0, T1, ... as an input and T1, T2, ... respectively as an output.
This process will continue adding records to the final result set until an empty set is returned from the recursive member, i.e. until a record with ToKey=#endKey has been added to the result set, or #maxRecursion level has been reached.
EDIT:
You can use the following query in order the effectively handle any circular paths:
;With FindPath
AS
(
SELECT FromKey, ToKey,
0 AS recursionLevel,
CAST(FromKey AS VARCHAR(MAX)) AS FromKeys
FROM RelatedEntities
WHERE FromKey = 1
UNION ALL
SELECT r.FromKey, r.ToKey,
recursionLevel = recursionLevel + 1,
FromKeys = FromKeys + ',' + CAST(r.FromKey AS VARCHAR(MAX))
FROM RelatedEntities r
INNER JOIN FindPath b ON r.FromKey = b.ToKey
WHERE (b.ToKey <> 3)
AND (RecursionLevel < 10)
AND PATINDEX('%,' + CAST(r.ToKey AS VARCHAR(MAX)) + ',%', ',' + FromKeys + ',') = 0
)
SELECT * FROM FindPath
ORDER BY recursionLevel
Calculated field FromKeys is used to carry on FromKey on to the next recursion level. This way any keys from previous recursion levels are accumulated from level to level using string concatenation. PATINDEX is then used to check whether a circular path has been met.
SQL Fiddle Demo here

Resources