Get Lowest level in tree created with hierarchyId that respect some conditions - sql

I have created a hierarchy table in my SQL Server. I have a column hierarchyId. Each level of hierarchy represent a geographical/political level of a country:
Countries
Regions
Provinces
For each row I can fill some boundaries or not. To simplify I replace in the example the geometry column with a bit column.
I need to get lowest level that has boundaries filled. If at least one child has boundaries, surely also parent has boundaries.
I make an example:
For example, I have that tree. In my query I should get green and red areas. In this moment I get only green areas.. so, I should get:
Valle d'aosta because is the lowest level and it has boundaries (and that is OK);
Milano and Brescia because they are the lowest level with boundaries. I should not get Bergamo because it has no boundaries, but I should get Lombardia instead of Bergamo;
Italy because both Piemonte and Torino have no boundaries;
Lazio because Roma has no boundaries.
My query is partially correct.. I get all lowest levels. But I do not get the minimum high level that respect my condition..
I share a link with the example: http://sqlfiddle.com/#!18/878577/1
Here also the query you can see in sql fiddler:
select * from country_subdivisions cs
where IsoCode IN(
select cs.IsoCode
from parent p
where cs.Level.IsDescendantOf(p.Level) = 1
and p.CountryISOAlpha2Code = cs.CountryISOAlpha2Code
-- and (cs.Level.GetLevel() = p.Level.GetLevel() + 1 or cs.Level.GetLevel() = p.Level.GetLevel())
and cs.Level.GetLevel() = (SELECT MAX(leaf.Level.GetLevel()) FROM country_subdivisions leaf WHERE leaf.Level.IsDescendantOf(cs.Level) = 1 and leaf.HasBoundaries = 1)
)
As you can see I correctly get the green areas but not the red one.
Any Idea? Do I have been clear?
Thank you

I think the logic is summarized as follows:
Return a parent when:
That parent has boundaries, and
Either:
it has no children, or
it has has at least one child that has no boundaries.
This could be formulated as follows:
select parent.*
from country_subdivisions parent
where parent.HasBoundaries = 1
and 0 < (select case
when count(*) = 0 then 1
else count(case when child.HasBoundaries = 0 then 1 end)
end
from country_subdivisions child
where child.Level.GetAncestor(1) = parent.Level
);

Related

T-SQL: Single Table contains Permissions Tree

I have a SQL Server Table that contains a 'Hierarchy/Tree' of User Permissions.
Each Individual Permission can have values: 1 [Allowed], Blank [Not Allowed] & 0 [specifically Cancelled].
Each Individual Permission can be in one or more 'Permission Groups' & a User can be assigned all the Individual Permissions in one or more Permission Groups.
Each of the 'Permission Groups', in turn, can be in one or more higher level permission groups ... and eventually, all Permissions Groups are under a Master Group named 'Main Menu'.
This SQL Code:
Select
'Main Menu' Base,
Description Level1,
ParentId,
SecurityNodesId,
ListOrder,
Category,
LastModified
From SecurityNodes
Where ParentId = 1
Order By Description
Produces the following Output:
'Main Menu' has a ParentId of NULL [Not Shown in screenshot].
The 'Level1' 'Folders' contain other folders or Individual Permissions which are 'Referenced' by the Values under SecurityNodesId.
For instance, a search for SecurityNodesId 102 [Level1 - Administration] in the ParentId column returns this list of Sub Folders under 'Level2':
So ... I can access each of these sub folders by writing separate queries.
But what I want is to have an end result that displays every Node of this Permissions Tree in Table form like this:
Main Menu Level1 Level2 Level3 Level4 PermissionName PermissionValue
I have never had to do something this complex before, though I have done plenty of self-joins.
I am currently thinking that I would need to do a self join to each self join ... to get to successive Levels of the Tree ... but I believe there may be a 'recursive' approach to this that might be more efficient?
I would appreciate any help I can get with this.
Thanks in advance!
The way to solve this is with a Recursive CTE.
These are definitely more advanced than your usual SQL, but once you have your head wrapped around them, they are pretty easy to put together and VERY useful for hierarchical data (any table that stores a parent/child relationship).
A recursive CTE has two parts, separated by a UNION ALL.
The recursive seed which is ran only once and determines the starting result set for the recursion. For you, this is likely any record with a parentId of 1.
The recursive term (or member) which joins the cte (itself) to the table that holds the parent/child relationship. It will run over and over and over and over again until the Join or a WHERE filter causes it to return no new records.
In your case, it will look something like below. Note that I don't know what your starting table looks like. Namely the Level1 column from your original SQL isn't clear if that is the column name or an alias you call Level1. Furthermore it's not at all clear how you derive a "Permission Group" or "Permission Value" from this data. But... at any rate this should get you in the ballpark:
WITH reccte as (
/*
* To start the recursion we need a "Seed"... or a set of data
* that defines the starting point on which we iterate after
* the UNION ALL below.
*
* The seed here is all records with a parentid of 1
*/
SELECT Base,
ParentID,
SecurityNodesID,
Level as Level1,
NULL as Level2,
NULL as Level3,
NULL as Level4,
'?' as PermissionName,
Category as PermissionValue,
1 as depth, --track how deep we recurse
Base + '>' + Level as path --keep track of where we've been and what has led us to this point in recurssion
FROM SecurityNodes
UNION ALL
/*
* This section is the part that iterates. It continues to join
* all rows that have been collected up that point with the Security
* Nodes table until that join fails.
*/
SELECT
reccte.Base,
SecurityNodes.ParentID,
SecurityNodes.SecurityNodesID,
reccte.Level1,
/*
* Depending on how deep we are in the security hierarchy
* capture the level string to the appropriate column
*/
CASE WHEN depth = 1 THEN SecurityNodes.Level ELSE reccte.Level2,
CASE WHEN depth = 2 THEN SecurityNodes.Level ELSE reccte.Level3,
CASE WHEN depth = 3 THEN SecurityNodes.Level ELSE reccte.Level4,
'?' as PermissionName,
SecurityNodes.Category as PermissionValue,
reccte.depth + 1, --increment depth
reccte.path + '>' + SecurityNodes.Level --add to the path so we know how we got here
FROM reccte
INNER JOIN SecurityNodes
/*Join parent to child*/
ON reccte.SecurityNodesId = SecurityNodes.parentId
WHERE depth < 5 --Stop looking up if we go deeper than 4 levels.
)
SELECT *
FROM reccte
While we track depth here and stop the recursion if we hit a depth of 4, you could stop the recursion with the MAXRECURSIVE option/hint. That would just go at the end of your query:
SELECT *
FROM reccte
OPTION (MAXRECURSION 4);
It's important to add either/or to your recursive CTE otherwise you risk causing an infinite loop should a security node have a child that is also one of its ancestors which would cause it to cycle endlessly.
OPTION (MAXRECURSION 2);
I followed through on an idea I mentioned in my original post and it looks like I have achieved what I was wanting.
I don't think it is the best possible solution because I know how many total levels there currently are. If we suddenly add another level or two, the SQL will not capture everything and I'll manually have to add one or more Left Joins.
Select
'Main Menu' Base,
sn.Description Level1,
sn2.Description Level2,
sn3.Description Level3,
sn4.Description Level4,
sn.ParentId,
sn.SecurityNodesId,
sn.ListOrder,
sn.Category,
sn.LastModified
From
SecurityNodes sn
Left Join SecurityNodes sn2 On sn2.ParentId = sn.SecurityNodesId
Left Join SecurityNodes sn3 On sn3.ParentId = sn2.SecurityNodesId
Left Join SecurityNodes sn4 On sn3.ParentId = sn3.SecurityNodesId
Order By sn.ParentId, sn.Description
I would still appreciate any suggestions for a more elegant/dynamic way of achieving what I need ... but for now, the above SQL is doing the job.

How to correctly order a recursive DB2 query

Hello friendly internet wizards.
I am attempting to extract a levelled bill of materials (BOM) from a dataset, running in DB2 on an AS400 server.
I have constructed most of the query (with a lot of help from online resources), and this is what I have so far;
#set item = '10984'
WITH BOM (origin, PMPRNO, PMMTNO, BOM_Level, BOM_Path, IsCycle, IsLeaf) AS
(SELECT CONNECT_BY_ROOT PMPRNO AS origin, PMPRNO, PMMTNO,
LEVEL AS BOM_Level,
SYS_CONNECT_BY_PATH(TRIM(PMMTNO), ' : ') BOM_Path,
CONNECT_BY_ISCYCLE IsCycle,
CONNECT_BY_ISLEAF IsLeaf
FROM MPDMAT
WHERE PMCONO = 405 AND PMFACI = 'M01' AND PMSTRT = 'STD'
START WITH PMPRNO = :item
CONNECT BY NOCYCLE PRIOR PMMTNO = PMPRNO)
SELECT 0 AS BOM_Level, '' AS BOM_Path, MMITNO AS Part_Number, MMITDS AS Part_Name,
IFSUNO AS Supplier_Number, IDSUNM AS Supplier_Name, IFSITE AS Supplier_Part_Number
FROM MITMAS
LEFT OUTER JOIN MITVEN ON MMCONO = IFCONO AND MMITNO = IFITNO AND IFSUNO <> 'ZGA'
LEFT OUTER JOIN CIDMAS ON MMCONO = IDCONO AND IDSUNO = IFSUNO
WHERE MMCONO = 405
AND MMITNO = :item
UNION ALL
SELECT BOM.BOM_Level, BOM_Path, BOM.PMMTNO AS Part_Number, MMITDS AS Part_Name,
IFSUNO AS Supplier_Number, IDSUNM AS Supplier_Name, IFSITE AS Supplier_Part_Number
FROM BOM
LEFT OUTER JOIN MITMAS ON MMCONO = 405 AND MMITNO = BOM.PMMTNO
LEFT OUTER JOIN MITVEN ON IFCONO = MMCONO AND IFITNO = MMITNO AND IFSUNO <> 'ZGA' AND MMMABU = '2'
LEFT OUTER JOIN CIDMAS ON MMCONO = IDCONO AND IDSUNO = IFSUNO
;
This is correctly extracting the components for a given item, as well as the sub-components (etc).
Current data looks like this (I have stripped out some columns that aren't relevant to the issue);
https://pastebin.com/LUnGKRqH
My issue is the order that the data is being presented in.
As you can see in the pastebin above, the first column is the 'level' of the component. This starts with the parent item at level 0, and can theoretically go down as far as 99 levels.
The path is also show there, so for example the second component 853021 tells us that it's a 2nd level component, the paths up to INST363 (shown later in the list as a 1st level), then up to the parent at level 0.
I would like for the output to show in path order (for lack of a better term).
Therefore, after level 0, it should be showing the first level 1 component, and then immediately be going into it's level 2 components and so on, until no further level is found. Then at that point, it returns back up the path to the next valid record.
I hope I have explained that adequately, but essentially the data should come out as;
Level
Path
Item
0
10984
1
: INST363
INST363
2
: INST363 : 853021
853021
1
: 21907
21907
Any help that can be provided would be very much appreciated!
Thanks,
This is an interesting query. Frankly I am surprised it works as well as it does since it is not structured the way I usually structure queries with a recursive CTE. The main issue is that while you have the Union in there, it does not appear to be within the CTE portion of the query.
When I write a recursive CTE, it is generally structured like this:
with cte as (
priming select
union all
secondary select)
select * from cte
So to get a BOM from an Item Master that looks something like:
CREATE TABLE item (
ItemNo Char(10) PRIMARY KEY,
Description Char(50));
INSERT INTO item
VALUES ('Item0', 'Root Item'),
('Item1a', 'Second Level Item'),
('Item1b', 'Another Second Level Item'),
('Item2a', 'Third Level Item');
and a linkage table like this:
CREATE TABLE linkage (
PItem Char(10),
CItem Char(10),
Quantity Dec(5,0),
PRIMARY KEY (PItem, CItem));
INSERT INTO linkage
VALUES ('Item0', 'Item1a', 2),
('Item0', 'Item1b', 3),
('Item1b', 'Item2a', 5)
The recursive CTE to list a BOM for 'Item0' looks like this:
WITH bom (Level, ItemNo, Description, Quantity)
AS (
-- Load BOM with root item
SELECT 0,
ItemNo,
Description,
1
FROM Item
WHERE ItemNo = 'Item0'
UNION ALL
-- Retrieve all child items
SELECT a.Level + 1,
b.CItem,
c.Description,
a.Quantity * b.Quantity
FROM bom a
join linkage b ON b.pitem = a.itemno
join item c ON c.itemno = b.citem)
-- Set the list order
SEARCH DEPTH FIRST BY itemno SET seq
-- List BOM
SELECT * FROM bom
ORDER BY seq
Here are my results:
LEVEL
ITEMNO
DESCRIPTION
QUANTITY
0
Item0
Root Item
1
1
Item1a
Second Level Item
2
1
Item1b
Another Second Level Item
3
2
Item2a
Third Level Item
15
Notice the search clause, that generates a column named seq which you can use to sort the output either depth first or breadth first. Depth first is what you want here.
NOTE: This isn't necessarily an optimum query since the description is in the CTE, and that increases the size of the CTE result set without really adding anything to it that couldn't be added in the final select. But it does make things a bit simpler since the 'priming query' retrieves the description.
Note also: the column list on the with clause following BOM. This is there to remove the confusion that DB2 had with the expected column list when the explicit column list was omitted. It is not always necessary, but if DB2 complains about an invalid column list, this will fix it.

Recursive CTE...dealing with nested parent/children records

I have the following records:
My goal is to check the SUM of the children for each parent and make sure it is 1 (or 100%).
In the example above, you have a first parent:
12043
It has 2 children:
12484 & 12485
Child (now parent) 12484 has child 12486. The child here (12486) has a percentage of 0.6 (which is NOT 100%). This is NOT OK.
Child (now parent) 12485 has child 12487. The child here (12487) has a percentage of 1 (or 100%). This is OK.
I need to sum the percentages of each nested children and get that value because it doesn't sum up to 100%, then I have to display a message. I'm having a hard time coming up with a query for this. Can someone give me a hand?
This is what I tried and I'm getting the "The statement terminated. The maximum recursion 100 has been exhausted before statement completion." error message.
with cte
as (select cp.parent_payee_id,
cp.payee_id,
cp.payee_pct,
0 as level
from dbo.tp_contract_payee cp
where cp.participant_id = 12067
and cp.payee_id = cp.parent_payee_id
union all
select cp.parent_payee_id,
cp.payee_id,
cp.payee_pct,
c.level + 1 as level
from dbo.tp_contract_payee cp
inner join cte c
on cp.parent_payee_id = c.payee_id
where cp.participant_id = 12067
)
select *
from cte
I believe something like the following should work:
WITH RECURSIVE recCTE AS
(
SELECT
parent_payee_id as parent,
payee_id as child,
payee_pct
1 as depth,
parent_payee_id + '>' + payee_id as path
FROM
table
WHERE
--top most node
parent_payee_id = 12043
AND payee_id <> parent_payee_id --prevent endless recursion
UNION ALL
SELECT
table.parent_payee_id as parent,
table.payee_id as child,
table.payee_pct,
recCTE.depth + 1 as Depth,
recCTE.path + '>' + table.payee_id as path
FROM
recCTE
INNER JOIN table ON
recCTE.child = table.parent_payee_id AND
recCTE.child <> table.payee_id --again prevent records where parent is child
Where depth < 15 --prevent endless cycles
)
SELECT DISTINCT parent
FROM recCTE
GROUP BY parent
HAVING sum(payee_pct) <> 1;
This differs from yours mostly in the WHERE statements on both the Recursive Seed (query before UNION) and the recursive term (query after UNION). I believe yours is too restrictive, especially in the recursive term since you want to allow records that are children of 12067 through, but then you only allow 12067 as the parent id to pull in.
Here, though, we pull every descendant of 12043 (from your example table) and it's payee_pct. Then we analyze each parent in the final SELECT and the sum of all it's payee_pcts, which are essentially that parent's first childrens sum(payee_pct). If any of them are not a total of 1, then we display the parent in the output.
At any rate, between your query and mine, I would imagine this is pretty close to the requirements, so it should be tweaks to get you exactly where you need to be if this doesn't do the trick.

Oracle Spatial Geometry covered by the most

I have a table which contains a number of geometries. I am attempting to extract the one which is most covered by another geometry.
This is best explained with pictures and code.
Currently I am doing this simple spatial query to get any rows that spatially interact with a passed in WKT Geometry
SELECT ID, NAME FROM MY_TABLE WHERE
sdo_anyinteract(geom,
sdo_geometry('POLYGON((400969 95600,402385 95957,402446 95579,400905 95353,400969 95600))',27700)) = 'TRUE';
Works great, returns a bunch of rows that interact in any way with my passed in geometry.
What I preferably want though is to find which one is covered most by my passed in geometry. Consider this image.
The coloured blocks represent 'MY_TABLE'. The black polygon over the top represents my passed in geometry I am searching with. The result I want returned from this is Polygon 2, as this is the one that is most covered by my polygon. Is this possible? Is there something I can use to pull the cover percentage in and order by that or a way of doing it that simply returns just that one result?
--EDIT--
Just to supplement the accepted answer (which you should go down and give an upvote as it is the entire basis for this) this is what I ended up with.
SELECT name, MI_PRINX,
SDO_GEOM.SDO_AREA(
SDO_GEOM.SDO_INTERSECTION(
GEOM,
sdo_geometry('POLYGON((400969.48717156524 95600.59583240788,402385.9445972018 95957.22742049221,402446.64806962677 95579.91508788493,400905.95874489535 95353.03765349534,400969.48717156524 95600.59583240788))',27700)
,0.005
)
,0.005) AS intersect_area
FROM LIFE_HEATHLAND WHERE sdo_anyinteract(geom, sdo_geometry('POLYGON((400969.48717156524 95600.59583240788,402385.9445972018 95957.22742049221,402446.64806962677 95579.91508788493,400905.95874489535 95353.03765349534,400969.48717156524 95600.59583240788))',27700)) = 'TRUE'
ORDER BY INTERSECT_AREA DESC;
This returns me all the results that intersect my query polygon with a new column called INTERSECT_AREA, which provides the area. I can then sort this and pick up the highest number.
Just compute the intersection between each of the returned geometries and your query window (using SDO_GEOM.SDO_INTERSECTION()), compute the area of each such intersection (using SDO_GEOM.SDO_AREA()) and return the row with the largest area (order the results in descending order of the computed area and only retain the first row).
For example, the following computes how much space Yellowstone National Park occupies in each state it covers. The results are ordered by area (descending).
SELECT s.state,
sdo_geom.sdo_area (
sdo_geom.sdo_intersection (
s.geom, p.geom, 0.5),
0.5, 'unit=sq_km') area
FROM us_states s, us_parks p
WHERE SDO_ANYINTERACT (s.geom, p.geom) = 'TRUE'
AND p.name = 'Yellowstone NP'
ORDER by area desc;
Which returns:
STATE AREA
------------------------------ ----------
Wyoming 8100.64988
Montana 640.277886
Idaho 154.657145
3 rows selected.
To only retain the row with the largest intersection do:
SELECT * FROM (
SELECT s.state,
sdo_geom.sdo_area (
sdo_geom.sdo_intersection (
s.geom, p.geom, 0.5),
0.5, 'unit=sq_km') area
FROM us_states s, us_parks p
WHERE SDO_ANYINTERACT (s.geom, p.geom) = 'TRUE'
AND p.name = 'Yellowstone NP'
ORDER by area desc
)
WHERE rownum = 1;
giving:
STATE AREA
------------------------------ ----------
Wyoming 8100.64988
1 row selected.
The following variant also returns the percentage of the park's surface in each intersecting state:
WITH p AS (
SELECT s.state,
sdo_geom.sdo_area (
sdo_geom.sdo_intersection (
s.geom, p.geom, 0.5),
0.5, 'unit=sq_km') area
FROM us_states s, us_parks p
WHERE SDO_ANYINTERACT (s.geom, p.geom) = 'TRUE'
AND p.name = 'Yellowstone NP'
)
SELECT state, area,
RATIO_TO_REPORT(area) OVER () * 100 AS pct
FROM p
ORDER BY pct DESC;
If you want to return the geometry of the intersections, just include that into your result set.

SELECT Query for a Bill of Materials

Our Bill of Materials table has 3 main columns: Part Number, Parent Part, and Level Code (0 being no parent, 3 being a child of a child of a child, etc).
I'd am trying to query based on a single part and get all of its children and their children, so on.
Its seems so simple but I have been struggling with this one. Anyone have a solution they can offer?
As always, I appreciate the help.
Per Randy's request, here is some more info:
PARPRT = Parent
COMPRT = Component
In the screen shot, all the component parts are part of the Bill of Materials for '101002'.
Later down the table, you will see each of those components in COMPRT listed in the PARPRT column with the components that make each of them up.
I want to query the BOM for '101002' and not only get the 4 parts in COMPRT where PARPRT = '101002', but also their COMPRTs and so on.
Thank you for those who suggested CTE's. That was exactly what I was looking for.
Here is the query I ended up with after some trial and error.
USE PartDatabase
GO
DECLARE #TheSinglePart CHAR(30) = '100001';
WITH BOM (PARPRT_02, COMPRT_02, QTYPER_02)
AS
(
-- Anchor member definition
SELECT e.PARPRT_02, e.COMPRT_02, e.QTYPER_02
FROM dbo.Product_Structure AS e
WHERE e.PARPRT_02 = #TheSinglePart
UNION ALL
-- Recursive member definition
SELECT e.PARPRT_02, e.COMPRT_02, e.QTYPER_02
FROM dbo.Product_Structure AS e
INNER JOIN BOM AS d
ON e.PARPRT_02 = d.COMPRT_02
)
SELECT *
FROM BOM;