Excel Quartile function in SQL Server SQL Query - sql-server

I have an set of values like this:
40
50
50
66
83
100
100
100
100
100
100
100
100
100
100
100
100
When I do the quartile function in excel, i get these four values for my first quartile (25), second quartile(50), third quartile(75) and max (100)
Quartile =
83.33, 100, 100, 100
So when I compare a sales rep who got 80% then they will fall in the bottom quartile according to excel calculation.
I need to redo same functionality in sql and have given my code below.
declare #sales table(
salesRepId int,
percentageSales int)
insert into #sales(salesRepId, percentageSales)
values(1,40)
,(2,50)
,(3,50)
,(4,66.7)
,(5,83.33)
,(6,100)
,(7,100)
,(8,100)
,(9,100)
,(10,100)
,(11,100)
,(12,100)
,(13,100)
,(14,100)
,(15,100)
,(16,100)
,(17,100);
with quintile as(
select percentagesales, ntile(4) over(order by percentagesales)
as quintile
from (select distinct percentagesales from #sales) as s
)
select salesrepid, r.percentagesales, q.quintile
from #sales r
join quintile q on r.percentagesales = q.percentagesales
order by q.quintile, percentagesales
When I run this i get the following result set:
Query results
salesrepid percentagesales quintile
1 40 1
2 50 1
3 50 1
4 66 2
5 83 3
6 100 4
7 100 4
8 100 4
9 100 4
10 100 4
11 100 4
12 100 4
13 100 4
14 100 4
15 100 4
16 100 4
17 100 4
Accoridng to sql, the 80% will fall in the medium quartile.
How can I get the four percentile values similar to excel in SQL query

Change int to decimal in percentageSales:
declare #sales table(
salesRepId int,
percentageSales decimal(8,2))
insert into #sales(salesRepId, percentageSales)
values(1,40)
,(2,50)
,(3,50)
,(4,66.7)
,(5,83.33)
,(6,100)
,(7,100)
,(8,100)
,(9,100)
,(10,100)
,(11,100)
,(12,100)
,(13,100)
,(14,100)
,(15,100)
,(16,100)
,(17,100);
with quintile as(
select percentagesales, ntile(4) over(order by percentagesales)
as quintile
from (select distinct percentagesales from #sales) as s
)
select salesrepid, r.percentagesales, q.quintile
from #sales r
join quintile q on r.percentagesales = q.percentagesales
order by q.quintile, percentagesales

I tried percentile_cont suggested Larnu, I get the four quartile values as
83,100,100,100
but technically it should be 83.33,100,100,100
Here is the query i have so far
declare #sales table(
salesRepId int,
percentageSales int)
insert into #sales(salesRepId, percentageSales)
values(1,40.00)
,(2,50.00)
,(3,50.00)
,(4,66.77)
,(5,83.33)
,(6,100.00)
,(7,100.00)
,(8,100.00)
,(9,100.00)
,(10,100.00)
,(11,100.00)
,(12,100.00)
,(13,100.00)
,(14,100.00)
,(15,100.00)
,(16,100.00)
,(17,100.00);
with quintile as(
select percentagesales, ntile(100) over(order by percentagesales) as quintile
from (select distinct percentagesales from #sales) as s
)
select salesrepid, r.percentagesales, q.quintile
from #sales r
join quintile q on r.percentagesales = q.percentagesales
order by q.quintile, percentagesales;
declare #p Decimal(2,2) = 0.25;
with quartile as(
select
salesRepId,
percentageSales
,cast(percentile_cont(.25) within group(order by percentagesales) over() as decimal(36,2)) as quartile25th
,cast(percentile_cont(.5) within group(order by percentagesales) over() as decimal(36,2)) as quartile50th
,cast(percentile_cont(.75) within group(order by percentagesales) over() as decimal(36,2)) as quartile75th
,cast(percentile_cont(1) within group(order by percentagesales) over() as decimal(36,2)) as quartile100th
from #sales
)
select
s.salesRepId
,s.percentageSales
, case
when s.percentageSales < q.quartile25th then 'red'
when s.percentageSales >=q.quartile25th and s.percentageSales <q.quartile75th then 'yellow'
when s.percentageSales >= q.quartile75th then 'green'
end as color
From #sales s
join quartile q on q.percentageSales = s.percentageSales and q.salesRepId = s.salesRepId
How do I achieve quartile values exactly like I get in excel.
Thank you

Related

How to get previous record if current record does not exist in table

my table like this
Id Date type quantity
1 29/04/2019 APPLE 2
2 29/04/2019 Banana 15
3 29/04/2019 Mango 100
4 29/04/2019 Grapes 50
5 29/04/2019 Fish 80
6 30/04/2019 APPLE 4
7 30/04/2019 Grapes 100
8 30/04/2019 Fish 90
9 01/05/2019 APPLE 6
10 01/05/2019 Banana 30
11 01/05/2019 Grapes 150
12 01/05/2019 Fish 100
13 02/05/2019 Mango 200
14 02/05/2019 Grapes 200
15 02/05/2019 Fish 110
16 03/05/2019 APPLE 8
17 03/05/2019 Banana 45
18 03/05/2019 Mango 300
19 04/05/2019 APPLE 10
20 04/05/2019 Grapes 300
21 04/05/2019 Fish 120
22 05/05/2019 APPLE 12
23 05/05/2019 Fish 130
i miss some inputs every day,But i need to fill the gaps with previous row of the same "Type" on 30/04/2019 i missed "Banana & Mango" bu i need like
Id Date type quantity
1 29/04/2019 APPLE 2
2 29/04/2019 Banana 15
3 29/04/2019 Mango 100
4 29/04/2019 Grapes 50
5 29/04/2019 Fish 80
6 30/04/2019 APPLE 4
7 30/04/2019 Grapes 100
8 30/04/2019 Fish 90
9 30/04/2019 Banana 15
10 30/04/2019 Mango 100
actually last two rows are null but it should updated same on 29/04/2019
I think the easiets way might be this:
DECLARE #PDate DATE = SELECT TOP 1 Date FROM YourTable ORDER BY Date ASC --Previous Date
DECLARE #NDate DATE = SELECT TOP 1 Date FROM YourTable WHERE DATE>#PDate --Next Date
WHILE (#NDate IS NOT NULL)
BEGIN
WITH X AS
(
SELECT T1.Date AS Date1, T1.Type AS Type1, T1.Quantity AS Q1
T2.Date AS Date2, T2.Type AS Type2, T2.Quantity AS Q2
FROM YourTable T1
LEFT JOIN YourTable T2 ON T1.Type = T2.Type
WHERE T1.Date = #PDate AND T2.Date = #NDate
)
INSERT INTO YourTable (Date,Type,Quantity)
SELECT #NDate,Type1,Q1
WHERE X.Type2 IS NULL
SET #PDate = #NDate
SET #NDate = NULL -- If next result wasnt found this stays null for while condition
SET #NDate = SELECT TOP 1 Date FROM YourTable WHERE Date>#PDate
END
I think this is the way that may work and I wish so
( if there is any syntax or ... mistakes its because I didnt have SSMS installed to test. Sorry)
try this :
declare #date date
and for initiate #date you can use select #date=max(date) from table1 or pass static value set #date='02/01/2019'
and then find input
select input,max(date) as MaxDate into #temp
from table1
where input not in (select input from table1 where date=#date )
group by input
then :
select t.* from Table1 t join #temp on Table1.input=#temp.Input and Table1.date=#temp.MaxDate
OK, after the goal posts are settled, this is one method. Note that this solution builds both a Types and Dates dataset. Really the Types dataset should already exist somewhere in your database, and you should create a Calendar Table if you're going to be doing this type of work often.
Any way, I've left comments in the code for you. I've assumed you're using SQL Server 2012+, as 2008 is literally about to run out of support.
CREATE TABLE dbo.MyTable (ID int IDENTITY(1,1),
[date] date,
[type] varchar(10),
Quantity int);
INSERT INTO dbo.MyTable
SELECT CONVERT(date,[date],103),
RTRIM([Type]),
Quantity
FROM (VALUES('29/04/2019','APPLE ',2),
('29/04/2019','Banana',15),
('29/04/2019','Mango ',100),
('29/04/2019','Grapes',50),
('29/04/2019','Fish ',80),
('30/04/2019','APPLE ',4),
('30/04/2019','Grapes',100),
('30/04/2019','Fish ',90),
('01/05/2019','APPLE ',6),
('01/05/2019','Banana',30),
('01/05/2019','Grapes',150),
('01/05/2019','Fish ',100),
('02/05/2019','Mango ',200),
('02/05/2019','Grapes',200),
('02/05/2019','Fish ',110),
('03/05/2019','APPLE ',8),
('03/05/2019','Banana',45),
('03/05/2019','Mango ',300),
('04/05/2019','APPLE ',10),
('04/05/2019','Grapes',300),
('04/05/2019','Fish ',120),
('05/05/2019','APPLE ',12),
('05/05/2019','Fish ',130)) V([date],[Type],Quantity);
GO
--SELECT *
--FROM dbo.MyTable;
GO
--Create a calendar table
WITH N AS (
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) N(N)),
Tally AS(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS I
FROM N N1, N N2, N N3), --1000 days shuld be enough
Dates AS(
SELECT DATEADD(DAY, T.I, MIN(MT.[date])) AS [Date]
FROM Tally T
CROSS JOIN dbo.MyTable MT
GROUP BY T.I
HAVING DATEADD(DAY, T.I, MIN(MT.[date])) <= MAX([Date])),
--Get Types
Types AS (
SELECT DISTINCT [Type]
FROM dbo.MyTable MT),
--Create islands
Grps AS(
SELECT MT.ID,
D.[Date],
T.[Type],
MT.Quantity,
COUNT(MT.Quantity) OVER (PARTITION BY T.[Type] ORDER BY D.[date]
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS Grp
FROM Dates D
CROSS JOIN Types T
LEFT JOIN dbo.MyTable MT ON D.[Date] = MT.[date]
AND T.[type] = MT.[type])
SELECT G.ID AS ID,
ROW_NUMBER() OVER (ORDER BY G.[Date], G.[Type]) AS RN,
G.[Date],
G.[Type],
MAX(G.Quantity) OVER (PARTITION BY G.[Type], G.Grp) AS Quantity
FROM Grps G
ORDER BY G.[Date],
G.[Type];
GO
DROP TABLE dbo.MyTable;
db<>fiddle
I think using cursor is a good option to insert your missing entries in the table. By cursor you will be able to check date wise missing types and insert them with the previous quantity.
You can also use this following script to find the missing records in your table. To create the script I consider the table name = 'add_missing_records'
SELECT AA.date AS [Date],
AA.type AS [Type],
BB.quantity AS [Original Quantity] ,
CASE
WHEN BB.quantity IS NULL THEN
(
SELECT quantity
FROM add_missing_records C
WHERE C.date = (
SELECT MAX([date])
FROM add_missing_records B
WHERE B.date < AA.date
AND B.type = AA.type
)
AND C.type = AA.type
)
ELSE BB.quantity
END AS [New Quantuty]
FROM (
SELECT date,type
FROM (
SELECT DISTINCT 'A' AS common,date
FROM add_missing_records
)A
FULL JOIN (
SELECT DISTINCT 'A' as common, type
FROM add_missing_records
)B
ON a.common = b.common
) AA
LEFT JOIN add_missing_records BB
ON AA.date = BB.date
AND AA.type = BB.type
WHERE BB.quantity IS NULL
ORDER BY 1,2

SQL Server: fill a range with dates from overlapping intervals with priority

I need to fill the range from 2017-04-01 to 2017-04-30 with the data from this table, knowing that the highest priority records should prevail over those with lower priorities
id startValidity endValidity priority
-------------------------------------------
1004 2017-04-03 2017-04-30 1
1005 2017-04-10 2017-04-22 2
1010 2017-04-19 2017-04-23 3
1006 2017-04-24 2017-04-28 2
1008 2017-04-26 2017-04-28 3
In practice I would need to get a result like this:
id startValidity endValidity priority
--------------------------------------------
1004 2017-04-03 2017-04-09 1
1005 2017-04-10 2017-04-18 2
1010 2017-04-19 2017-04-23 3
1006 2017-04-24 2017-04-25 2
1008 2017-04-26 2017-04-28 3
1004 2017-04-29 2017-04-30 1
can't think of anything elegant or more efficient solution right now . . .
-- Sample Table
declare #tbl table
(
id int,
startValidity date,
endValidty date,
priority int
)
-- Sample Data
insert into #tbl select 1004, '2017-04-03', '2017-04-30', 1
insert into #tbl select 1005, '2017-04-10', '2017-04-22', 2
insert into #tbl select 1010, '2017-04-19', '2017-04-23', 3
insert into #tbl select 1006, '2017-04-24', '2017-04-28', 2
insert into #tbl select 1008, '2017-04-26', '2017-04-28', 3
-- Query
; with
date_range as -- find the min and max date for generating list of dates
(
select start_date = min(startValidity), end_date = max(endValidty)
from #tbl
),
dates as -- gen the list of dates using recursive CTE
(
select rn = 1, date = start_date
from date_range
union all
select rn = rn + 1, date = dateadd(day, 1, d.date)
from dates d
where d.date < (select end_date from date_range)
),
cte as -- for each date, get the ID based on priority
(
select *, grp = row_number() over(order by id) - rn
from dates d
outer apply
(
select top 1 x.id, x.priority
from #tbl x
where x.startValidity <= d.date
and x.endValidty >= d.date
order by x.priority desc
) t
)
-- final result
select id, startValidity = min(date), endValidty = max(date), priority
from cte
group by grp, id, priority
order by startValidity
I do not understand the purpose of Calendar CTE or table.
So I am not using any REcursive CTE or calendar.
May be I hvn't understood the requirement completly.
Try this with diff sample data,
declare #tbl table
(
id int,
startValidity date,
endValidty date,
priority int
)
-- Sample Data
insert into #tbl select 1004, '2017-04-03', '2017-04-30', 1
insert into #tbl select 1005, '2017-04-10', '2017-04-22', 2
insert into #tbl select 1010, '2017-04-19', '2017-04-23', 3
insert into #tbl select 1006, '2017-04-24', '2017-04-28', 2
insert into #tbl select 1008, '2017-04-26', '2017-04-28', 3
;With CTE as
(
select * ,ROW_NUMBER()over(order by startValidity)rn
from #tbl
)
,CTE1 as
(
select c.id,c.startvalidity,isnull(dateadd(day,-1, c1.startvalidity)
,c.endValidty) Endvalidity
,c.[priority],c.rn
from cte c
left join cte c1
on c.rn+1=c1.rn
)
select id,startvalidity,Endvalidity,priority from cte1
union ALL
select id,startvalidity,Endvalidity,priority from
(
select top 1 id,ca.startvalidity,ca.Endvalidity,priority from cte1
cross apply(
select top 1
dateadd(day,1,endvalidity) startvalidity
,dateadd(day,-1,dateadd(month, datediff(month,0,endvalidity)+1,0)) Endvalidity
from cte1
order by rn desc)CA
order by priority
)t4
--order by startvalidity --if req

Select top 2 distinct for each id and date

I have a table like this :
Table1:
[Id] [TDate] [Score]
1 1.1.00 50
1 1.1.00 60
2 1.1.01 50
2 1.1.01 70
2 1.3.01 40
3 1.1.00 80
3 1.1.00 30
3 1.2.00 40
My desired output should be like this:
[ID] [TDate] [Score]
1 1.1.00 60
2 1.1.01 70
2 1.3.01 40
3 1.1.00 80
3 1.2.00 40
So fare, I have written this:
SELECT DISTINCT TOP 2 Id, TDate, Score
FROM
( SELECT Id, TDate, Score, ROW_NUMBER() over(partition by TDate order by Score) Od
FROM Table1
) A
WHERE A.Od = 1
ORDER BY Score
But it gives me :
[ID] [TDate] [Score]
2 1.1.01 70
3 1.1.00 80
of course I can do this:
"select top 2 ...where ID = 1"
and then:
union
`"Select top 2 ... where ID = 2"`
etc..
but I have a 100,000 of this..
Any way to generalize it to any Id?
Thank you.
WITH TOPTWO AS (
SELECT Id, TDate, Score, ROW_NUMBER()
over (
PARTITION BY TDate
order by SCORE
) AS RowNo
FROM [table_name]
)
SELECT * FROM TOPTWO WHERE RowNo <= 2
Your output doesn't make sense. Let me assume you want two rows per id. Then the query would look like:
SELECT TOP 2 Id, TDate, Score
FROM (SELECT Id, TDate, Score,
ROW_NUMBER() over (partition by id order by Score DESC) as seqnum
FROM Table1
) t
WHERE seqnum <= 2
ORDER BY Score;
Notes:
This assumes that you want two rows per id. Hence, id is in the PARTITION BY.
The WHERE now selects two rows per group in the PARTITION BY.
There is no need for SELECT DISTINCT in the outer query -- at least for this question.
Try this : Make partition by ID and TDate and sort by score in descending order
ROW_NUMBER() over(partition by ID,TDate order by Score DESC) Od
Complete script
WITH CTE AS(
SELECT *,
ROW_NUMBER() over(partition by ID,TDate order by Score DESC) RN
FROM TableName
)
SELECT *
FROM CTE
WHERE RN = 1
Unless I am missing something this can be done with a simple group by
First I prepare a temp table for testing :
declare #table table (ID int, TDate varchar(10), Score int)
insert into #Table values(1, '1.1.00', 50)
insert into #Table values(1, '1.1.00', 60)
insert into #Table values(2, '1.1.01', 50)
insert into #Table values(2, '1.1.01', 70)
insert into #Table values(2, '1.3.01', 40)
insert into #Table values(3, '1.1.00', 80)
insert into #Table values(3, '1.1.00', 30)
insert into #Table values(3, '1.2.00', 40)
Now lets do a select on this table
select ID, TDate, max(Score) as Score
from #table
group by ID, TDate
order by ID, TDate
The result is this :
ID TDate Score
1 1.1.00 60
2 1.1.01 70
2 1.3.01 40
3 1.1.00 80
3 1.2.00 40
So all you need to do is change #table to your table name and you are done

How to get all numbers between a range

I have a table as below
Id RFrom RTo
.... ....... .....
1 10 14
1 22 25
2 100 102
2 176 180
I want to get all numbers between each RFrom and RTo for each Id. My expected result is as follows
Id NUMS
.... ......
1 10
1 11
1 12
1 13
1 14
1 22
1 23
1 24
1 25
2 100
2 101
2 102
2 176
2 177
2 178
2 179
2 180
Do I have to use cursor to achieve this?
Here is your sample table
SELECT * INTO #TEMP FROM
(
SELECT 1 ID, 10 RFROM, 14 RTO
UNION ALL
SELECT 1, 22, 25
UNION ALL
SELECT 2, 100, 102
UNION ALL
SELECT 2, 176, 180
)TAB
You need to use recursion for each Id to get the result
;WITH CTE AS
(
SELECT ID,RFROM RFROM1,RTO RTO1
FROM #TEMP
UNION ALL
SELECT T.ID,RFROM1+1,RTO1
FROM #TEMP T
JOIN CTE ON CTE.ID = T.ID
WHERE RFROM1 < RTO1
)
SELECT DISTINCT ID,RFROM1 NUMS
FROM CTE
SQL FIDDLE
Another option would be to use a numbers table with a join -- recursion can be time consuming.
There are several options to create a numbers table (I'd recommend creating a permanent one), but here's a temp one created with a common-table-expression:
with numberstable as (
select top 10000 row_number() over(order by t1.number) as number
from master..spt_values t1
cross join master..spt_values t2
)
select yt.id,
nt.number
from yourtable yt
join numberstable nt on nt.number between yt.rfrom and yt.rto
SQL Fiddle Demo
Create a tally table using stacked CTE which will have better performance when compared to recursive CTE
declare #min int
select #min= min(RFrom) from yourtable
;WITH e1(n) AS
(
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), -- 10
e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b), -- 10*10
e3(n) AS (SELECT 1 FROM e1 CROSS JOIN e2) -- 10*100
SELECT b.id,
a.n
FROM yourtable b
JOIN (SELECT n = Row_number()OVER (ORDER BY n)+ #min-1
FROM e3)a
ON a.n BETWEEN b.RFrom AND b.RTo
ORDER BY n;
Check here for info

how to find previous & forward relation of data in single query

I have table :
ID Person_ID Person_Relative_ID
1 10 20
2 20 30
3 13 15
4 30 40
5 55 56
6 40 50
Here we can see the person & person_relative_id chain is going like 10 - 20 - 30 - 40 - 50
Now if user search Person_Relative_ID of person_id = 20 then result would be like: [All its relation]
Person_Relative_ID
10
30
40
50
Or user want to search Person_Relative_ID for person_id = 40 . then result like
Person_Relative_ID
10
20
30
50
Or user want to search Person_Relative_ID for person_id = 50 . then result like
Person_Relative_ID
10
20
30
40
Any suggestion really appreciated.
Well I'm not sure this is the most performance effective solution, and I'd have liked to deal with a single recursive CTE instead of two, but this works at least.
Just replace the references to the temporary table I used to test this, with your real table. But what it does, is it uses two recursive CTE's to find all references above (CTEUp) and below (CTEDown) your ID, and then shows them in order, except for your searched ID.
Note: This is for SQL Server, not Oracle.
-- Creating dummy variables for testing
DECLARE #PERSONS TABLE (ID INT IDENTITY(1,1), Person_ID INT, Person_Relative_ID INT)
INSERT INTO #PERSONS VALUES (10,20), (20,30), (13,15), (30,40), (55,56), (40,50)
-- Variable for searched ID, the actual script begins here
DECLARE #SEARCHED_ID INT
SET #SEARCHED_ID = 20
;WITH CTEUp AS
-- Fetching all relations above the ID
(SELECT Person_ID
FROM #PERSONS
WHERE Person_ID = #SEARCHED_ID
UNION ALL
SELECT Person_Relative_ID
FROM #PERSONS P
JOIN CTEUp C ON C.Person_ID = P.Person_ID
AND P.Person_Relative_ID > C.Person_ID)
, CTEDown AS
-- Fetching all relations below the ID
(SELECT Person_ID
FROM #PERSONS
WHERE Person_Relative_ID = #SEARCHED_ID
UNION ALL
SELECT P.Person_ID
FROM #PERSONS P
JOIN CTEDown C ON C.Person_ID = P.Person_Relative_ID
AND P.Person_ID < C.Person_ID)
-- Showing results
SELECT Person_ID
FROM
(SELECT *
FROM CTEDown
UNION ALL
SELECT *
FROM CTEUp) SRC
WHERE Person_ID <> #SEARCHED_ID --... minus the ID, as per your example
ORDER BY Person_ID ASC
Just gave a try in Oracle, could be better but still it works
with tab(ID, Person_ID, Person_Relative_ID) as (
SELECT 1, 10, 20 from dual union all
SELECT 2, 20, 30 from dual union all
SELECT 3, 13, 15 from dual union all
SELECT 4, 30, 40 from dual union all
SELECT 5, 55, 56 from dual union all
SELECT 6, 40, 50 from dual),
---------
--End of data preparation
---------
filter_tab as (
select 40 as id from dual), --> Put the search id here
final_tab(person_id) as (
select person_id
from tab
start with person_relative_id = (select id from filter_tab)
connect by prior person_id = person_relative_id
union
select person_relative_id
from tab
start with person_id = (select id from filter_tab)
connect by prior person_relative_id = person_id)
select *
from final_tab
where not exists (select 'x'
from filter_tab
where id = person_id )
order by 1;
Output:
PERSON_ID
---------
10
20
30
50
I just traversed to both ends from the start point, did union of the result and excluded the search id.
In SQL Server 2012 and above, you can use LAG and LEAD function.
Here is a sample:
SELECT TOP 1000 [n], LAG([n]) OVER(ORDER BY [n] ), LEAD([n]) OVER (ORDER BY [n])
FROM [dbo].[Nums]
it generates results:
n LAG LEAD
1 NULL 2
2 1 3
3 2 4
4 3 5
5 4 6
If you are using SQL2005 and above, SQL Recursive CTE structured SQL query can be used for querying hierarchical data models like in your case
Below query uses also multiple CTE queries to prevent usage of unnecessary temp tables or table variables
declare #id smallint = 50
;with cte as (
select Person_ID, Person_Relative_ID
from PersonRelative
where Person_ID = #id OR Person_Relative_ID = #id
union all
select P.Person_ID, P.Person_Relative_ID
from PersonRelative P
inner join CTE on CTE.Person_ID = P.Person_Relative_ID
), cte2 as (
select Person_ID from cte
union
select Person_Relative_ID from cte
), cte3 as (
select Person_ID, Person_Relative_ID
from PersonRelative
where Person_ID = #id OR Person_Relative_ID = #id
union all
select P.Person_ID, P.Person_Relative_ID
from PersonRelative P
inner join CTE3 on CTE3.Person_Relative_ID = P.Person_ID
), cte4 as (
select Person_ID from cte3
union
select Person_Relative_ID from cte3
)
select * from cte4 where Person_ID <> #id
union
select * from cte2 where Person_ID <> #id

Resources