Find all descendents of the oldest ancestor in SQL - sql

I have been trying for several days to figure out a solution to this issue but have not been able to come up with an answer. What I have is a data set that looks like this:
Id ParentId Name
16 NULL i_ss_16_Grommets
25 16 ss_25_Grommets
26 NULL inactive_Grommets Clone
27 NULL inactive_Grommets Clone Clone
46 25 ss_46_Grommets
47 46 ss_47_Grommets
48 47 Grommets
What I need to come up with is a function where I can pass an Id and then get the correct Name. The way that I need to find the name involves a sort of reverse hierarchy since it is the youngest child in a branch that will be used. For example, if I pass in Id 46, I need the function to return 'Grommets'. If I pass in Id 47, I need to see 'Grommets', if I pass in Id 26, I would see 'inactive_Grommets Clone' since there are no descendents.
Even though it looks like I could just strip off anything with an underscore after it, I would not be able to since there is no guarantee that the child will be named the same.
Hopefully this makes sense. Any help would be greatly appreciated.

Option with recursive CTE
DECLARE #Id int = 46
;WITH cte AS
(
SELECT Id, ParentId, Name
FROM dbo.test60
WHERE Id = #Id
UNION ALL
SELECT t.Id, t.ParentId, t.Name
FROM dbo.test60 t JOIN cte c ON t.ParentId = c.Id
)
SELECT TOP 1 *
FROM cte
ORDER BY Id DESC
Demo on SQLFiddle

Related

How to apply CMS HCC Hierarchy / trumping logic in SQL?

I've spent a lot of time trying to see if this exists elsewhere, but unfortunately it doesn't. I think I've solved it, but am looking for any advice on how to make this a bit more elegant/streamlined. Hopefully this will help someone else!
CMS publishes a Risk Adjustment model where diseases are grouped into hierarchies. Only the most severe form of the disease is counted toward a patient's Risk Adjustment Score. CMS does publish the model in SAS, but not in SQL. Every other aspect of the model is straightforward apart from applying the hierarchy / trumping logic below.
There are two tables, one containing member/patient IDs and their hierarchical condition categories (HCCs). The other table is the hierarchy table, where only the most severe form of the HCC is meant to be kept:
Members:
memberId
HCC
A
17
A
18
A
19
B
18
B
19
C
19
Hierarchy
HCC
dropHCC
17
18
17
19
18
19
If a member has 17, 18, and 19, only 17 would be kept as a result. If a member only has 19, then 19 would remain. 17 is considered a more severe form of the condition category which includes 18 and 19, but for scoring purposes we'd only want to count 17.
So, applying the Hierarchy to the Members table, the results should be:
memberId
HCC
A
17
B
18
C
19
As mentioned, I've already solved this. I'm wondering if there are any other ways that are more efficient/elegant?
;with members as (
select 123456 as memberID, 17 as hcc
UNION
select 123456 as memberID, 18 as hcc
UNION
select 123456 as memberID, 19 as hcc
UNION
select 2222222 as memberID, 19 as hcc
UNION
select 9999999 as memberID, 18 as hcc
UNION
select 9999999 as memberID, 19 as hcc
)
, Hierarchy as
(
Select 17 as hcc, 18 as dropHCC, 'diabetes1' as hccCategory
UNION
Select 17 as hcc, 19 as dropHCC, 'diabetes1' as hccCategory
UNION
Select 18 as hcc, 19 as dropHCC, 'diabetes2' as hccCategory
)
select m.*--, h2.dropHCC as hccRemovedBy
from members m
left join(
select m.*, r.drophcc
from members m
inner join ( select memberid, m.hcc, h.drophcc
from members m
inner join hierarchy h on h.hcc = m.hcc) r on
r.memberid = m.memberid
and r.dropHCC = m.hcc) h2
on h2.memberID = m.memberID
and h2.hcc = m.hcc
where h2.dropHCC is null --remove this criteria in the event you want to see what dropped
Why can't you just use MIN(HCC)?
If this is simplified data and the real HCC codes are not integers but are truly hierarchical then you might need to look at a recursive CTE. These can be tricky to write, but basically it is a CTE that has two parts, the first part generates a base dataset (the top of the hierarchy) and then UNIONS a second query that references the CTE you are creating, normally with some sort of additive value (Hierarchy depth or concatenated string). You can then match the correct hierarcy record to the rows in your dataset and use them as a filter (e.g. MIN(HierarchyDepth) or LEFT(HierarchyString, LEN(HCC) = HCC)
Depending on the context of where you use it there are recursion limits. By default these are 100, but you can explicity set an option to change that, BUT NOT IF YOU ARE USING IT IN A TABLE FUNCTION!!!!. It is computationally quite expensive so if the data is slow to change you may want to persist it to the database so it can be indexed.

Two aggregates in one cross apply possible?

I'm very much new to SQL and I'm trying to use CROSS APPLY, something I know very little about.
I'm trying to pull two SUMs of items sorted by an ID from two different tables. One SUM of all items dispensed by a cartridge, one SUM of all items refilled into a cartridge. The dispenses and refills are in separate tables. In Sample 1 you can see a piece of code that works for one of these two SUMs, currently its for the Dispensed SUM, but it also works if I change everything for the refilled SUM. Point being I can only do one SUM in this CROSS APPLY, regardless which one of the two.
So it goes wrong when I try to pull both SUMs in this one CROSS APPLY, probably cause I don't really know what I'm doing. I try to do this with the code seen in Sample 2 (which is pretty much the same code).
Some extra context:
There are two ID's here that are important:
The CartridgeRefill.FK_CartridgeRegistration_Id (or ID) is the ID for a cartridge itself. The FK_CartridgeRefill_Id is the ID for a refill, a cartridge can go through multiple refills and dispenses are registered by what refill they were dispensed from. That's why you can see the same ID multiple times in the output.
Sample 1:
SELECT CartridgeRefill.FK_CartridgeRegistration_Id AS ID, Sums.Dispensed
FROM CartridgeRefillItem
CROSS APPLY (
SELECT SUM(CartridgeDispenseAttempt.Amount) AS Dispensed
FROM CartridgeDispenseAttempt
WHERE CartridgeRefillItem.FK_CartridgeRefill_Id = CartridgeDispenseAttempt.FK_CartridgeRefill_Id
) AS Sums
JOIN CartridgeRefill ON CartridgeRefillItem.FK_CartridgeRefill_Id = CartridgeRefill.FK_CartridgeRefill_Id
Sample 2:
SELECT CartridgeRefill.FK_CartridgeRegistration_Id AS ID, Sums.Dispensed, Sums.Refilled
FROM CartridgeRefillItem
CROSS APPLY (
SELECT SUM(CartridgeDispenseAttempt.Amount) AS Dispensed
,SUM(CartridgeRefillItem.Amount) AS Refilled
FROM CartridgeDispenseAttempt
WHERE CartridgeRefillItem.FK_CartridgeRefill_Id = CartridgeDispenseAttempt.FK_CartridgeRefill_Id
) AS Sums
JOIN CartridgeRefill ON CartridgeRefillItem.FK_CartridgeRefill_Id = CartridgeRefill.FK_CartridgeRefill_Id
When I run sample 1 I get this output:
ID Dispensed
10 95
8 143
6 143
11 70
11 312
11 354
8 19
8 24
8 3
8 33
This output is correct, it displays the number of dispensed items next to the ID it belongs to.
This is the error I get when I run sample 2:
Msg 4101, Level 15, State 1, Line 15
Aggregates on the right side of an APPLY cannot reference columns from the left side.
But what I want to see is:
ID Dispensed Refilled (example)
10 95 143
8 143 12
6 143 etc...
11 70
11 312
11 354
8 19
8 24
8 3
8 33
I think it has something to do with CROSS APPLY running line by line? But again, I still don't exactly know what I'm doing yet. Any help would be really appreciated and please ask whatever you need to know :)
Error is quite self explanatory, you cannot run an aggregate using a reference that's outside of CROSS APPLY. You'll need to rewrite your query by adding a additional subquery to calculate SUM or use a GROUP BY clause. I've quickly scraped this:
SELECT CartridgeRefill.FK_CartridgeRegistration_Id AS ID, Sums.Dispensed, SUM(CartridgeRefillMedication.Amount) AS Refilled
FROM CartridgeRefillItem
CROSS APPLY (
SELECT SUM(CartridgeDispenseAttempt.Amount) AS Dispensed
FROM CartridgeDispenseAttempt
WHERE CartridgeRefillItem.FK_CartridgeRefill_Id = CartridgeDispenseAttempt.FK_CartridgeRefill_Id
) AS Sums
JOIN CartridgeRefill ON CartridgeRefillMedication.FK_CartridgeRefill_Id = CartridgeRefill.FK_CartridgeRefill_Id
GROUP BY CartridgeRefill.FK_CartridgeRegistration_Id;
Hopefully this works.
You may not want aggregation at all. The number of rows is not being reduced, so this may be what you want:
SELECT cr.FK_CartridgeRegistration_Id AS ID,
d.Dispensed, cr.Amount AS Refilled
FROM CartridgeRefillItem cr CROSS APPLY
(SELECT SUM(cd.Amount) AS Dispensed
FROM CartridgeDispenseAttempt c
WHERE cr.FK_CartridgeRefill_Id = cd.FK_CartridgeRefill_Id
) d;
I would expect that you want separate totals for each id. If so, then your sample results are not sensible because ids are repeated. But this would seem to do something useful:
select id, sum(refill_amount) as refill_amount,
sum(dispensed_amount) as dispensed_amount
from ((select cr.FK_CartridgeRegistration_Id as id,
cr.Amount as refill_amount,
0 as dispensed_amount
from CartridgeRefillItem cr
) union all
(select cd.FK_CartridgeRegistration_Id as id,
0, cd.Amount
from CartridgeDispenseAttempt cd
)
) c
group by id

asp.net mvc c# changing slug in child records

i have these records coming from my stored procedure which i am calling in linq to sql
int_PostTypeId vcr_PostType int_PostTypeId_fk vcr_Slug HLevel
49 c 36 c 1
77 e 49 c/e 2
78 f 77 c/e/f 3
79 g 77 c/e/g 3
i have these set of records.
suppose while editing the int_PostTypeId 49 i changed the slug to c1
1) now the slug in the child records also ought to be changed.
slug in 77 will become c1/e
slug in 78 will become c1/e/f
slug in 79 will become c1/e/g
2) if i edit the record 77 and change the slug to c/e2 then the slug 78 and 79 should also be changed to c/e2/f and c/e2/g.
so editing the slug in the record will change the child slug if exists. what is the most appropriate and efficient way of doing it in linq. i am taking the recursive loop path but i think that is highly inefficient. any idea for more general approach? or any other approach.
If I'm understanding you correctly, you're actually updating a column called vcr_Slug in a record somewhere, rather than building the column's value in your stored procedure. Since you're actually using a stored procedure, why not calculate the column's value? I'm not sure if you're using a recursive common table expression to select your results, but if you are, it could take the form of something like the following (making some assumptions about your table structure which may not, of course, be completely representative):
CREATE FUNCTION dbo.GetPostTypes(#ParentTypeID int)
RETURNS TABLE
AS RETURN
(
WITH CurrentPostTypes(int_PostTypeId, vcr_PostType, int_PostTypeId_fk, vcr_Slug,
HLevel)
AS
(
-- Anchor member definition
SELECT pt.int_PostTypeId, pt.vcr_PostType, pt.int_PostTypeId_fk,
pt.vcr_Slug, 1 AS HLevel
FROM dbo.tblPostTypes AS pt
WHERE pt.int_PostTypeId = #ParentTypeID
UNION ALL
-- Recursive member definition
SELECT pt.int_PostTypeId, pt.vcr_PostType, pt.int_PostTypeId_fk,
cpt.vcr_Slug + '/' + pt.vcr_Slug AS vcr_Slug, cpt.HLevel + 1 AS HLevel
FROM dbo.tblPostTypes AS pt
INNER JOIN CurrentPostTypes AS cpt
ON pt.int_PostTypeId_fk = cpt.int_PostTypeId
)
SELECT *
FROM CurrentPostTypes
)
You'll notice in the recursive member definition where the previous value of vcr_Slug is suffixed with a slash and the current record's column value: cpt.vcr_Slug + '/' + pt.vcr_Slug AS vcr_Slug

Get a certain number of contiguous rows

I've got a school project, kind of huge, and only a few days left, so here is one problem I'm stuck on, hope you can help.
I've got this table:
places(id, otherInfo)
Simple enough, well I need to make a SQL query or PL/SQL function to retrieve a certain number of contiguous rows. For instance if I call the function using getContiguousPlaces(3); On the table which has the rows:
ID
1
4
5
6
18
19
I want to get rows with ID 4, 5 and 6.
How could I do that?
SELECT p.id, p.prev_id1, p.prev_id2
FROM (
SELECT id, LAG(id, 1) OVER(ORDER BY id) prev_id1, LAG(id, 2) OVER(ORDER BY id) prev_id2
FROM places
) p
WHERE p.prev_id1 = id-1
AND p.prev_id2 = id-2
here you go: http://sqlfiddle.com/#!4/a20e1/1
But I guess you could retrieve the datas differently if you wished.
edit: this case if for the parameter "3". But if you want to adapt it with a number "n", you either have to use it within a dynamic query, or maybe use the partition by clause. I'm going to have a look at this one now...
Try this:
WITH t AS
(SELECT p.*, LEVEL l,
CONNECT_BY_ROOT id AS cbr
FROM places p CONNECT BY
PRIOR id = id-1)
SELECT *
FROM t
WHERE cbr IN
(SELECT cbr
FROM t
WHERE l = 3 )
and l <= 3
ORDER BY cbr,
id
The constant 3 should be a parameter
Here is a fiddle

select in sql server 2005

I have a table follow:
ID | first | end
--------------------
a | 1 | 3
b | 3 | 8
c | 8 | 10
I want to select follow:
ID | first | end
---------------------
a-c | 1 | 10
But i can't do it. Please! help me. Thanks!
This works for me:
SELECT MIN(t.id)+'-'+MAX(t.id) AS ID,
MIN(t.[first]) AS first,
MAX(t.[end]) AS [end]
FROM dbo.YOUR_TABLE t
But please, do not use reserved words like "end" for column names.
I believe you can do this using a recursive Common Table Expression as follows, especially if you're not expecting very long chains of records:
WITH Ancestors AS
(
SELECT
InitRow.[ID] AS [Ancestor],
InitRow.[ID],
InitRow.[first],
InitRow.[end],
0 AS [level],
'00000' + InitRow.[ID] AS [hacky_level_plus_ID]
FROM
YOUR_TABLE AS InitRow
WHERE
NOT EXISTS
(
SELECT * FROM YOUR_TABLE AS PrevRow
WHERE PrevRow.[end] = InitRow.[first]
)
UNION ALL
SELECT
ParentRow.Ancestor,
ChildRow.[ID],
ChildRow.[first],
ChildRow.[end],
ParentRow.level + 1 AS [level],
-- Avoids having to build the recursive structure more than once.
-- We know we will not be over 5 digits since CTEs have a recursion
-- limit of 32767.
RIGHT('00000' + CAST(ParentRow.level + 1 AS varchar(4)), 5)
+ ChildRow.[ID] AS [hacky_level_plus_ID]
FROM
Ancestors AS ParentRow
INNER JOIN YOUR_TABLE AS ChildRow
ON ChildRow.[first] = ParentRow.[end]
)
SELECT
Ancestors.Ancestor + '-' + SUBSTRING(MAX([hacky_level_plus_ID]),6,10) AS [IDs],
-- Without the [hacky_level_plus_ID] column, you need to do it this way:
-- Ancestors.Ancestor + '-' +
-- (SELECT TOP 1 Children.ID FROM Ancestors AS Children
-- WHERE Children.[Ancestor] = Ancestors.[Ancestor]
-- ORDER BY Children.[level] DESC) AS [IDs],
MIN(Ancestors.[first]) AS [first],
MAX(Ancestors.[end]) AS [end]
FROM
Ancestors
GROUP BY
Ancestors.Ancestor
-- If needed, add OPTION (MAXRECURSION 32767)
A quick explanation of what each part does:
The WITH Ancestors AS (...) clause creates a Common Table Expression (basically a subquery) with the name Ancestors. The first SELECT in that expression establishes a baseline: all the rows that have no matching entry prior to it.
Then, the second SELECT is where the recursion kicks in. Since it references Ancestors as part of the query, it uses the rows it has already added to the table and then performs a join with new ones from YOUR_TABLE. This will recursively find more and more rows to add to the end of each chain.
The last clause is the SELECT that uses this recursive table we've built up. It does a simple GROUP BY since we've saved off the original ID in the Ancestor column, so the start and end are a simple MIN and MAX.
The tricky part is figuring out the ID of the last row in the chain. There are two ways to do it, both illustrated in the query. You can either join back with the recursive table, in which case it will build the recursive table all over again, or you can attempt to keep track of the last item as you go. (If building the recursive list of chained records is expensive, you definitely want to minimize the number of times you need to do that.)
The way it keeps track as it goes is to keep track of its position in the chain (the level column -- notice how we add 1 each time we recurse), zero-pad it, and then stick the ID at the end. Then, getting the item with the max level is simply a MAX followed by stripping the level data out.
If the CTE has to recurse too much, it will generate an error, but I believe you can tweak that using the MAXRECURSION option. The default is 100. If you have to set it higher than that, you may want to consider not using a recursive CTE to do this.
This also doesn't handle malformed data very well. If you have two records with the same first or a record where first == end, then this won't work right and you may have to tweak the join conditions inside the CTE or go with another approach.
This isn't the only way to do it. I believe it would be easier to follow if you built a custom procedure and did all the steps manually. But this has the advantage of operating in a single statement.