SQL Server: querying hierarchical and referenced data - sql

I'm working on an asset database that has a hierarchy. Also, there is a "ReferenceAsset" table, that effectively points back to an asset. The Reference Asset basically functions as an override, but it is selected as if it were a unique, new asset. One of the overrides that gets set, is the parent_id.
Columns that are relevant to selecting the heirarchy:
Asset: id (primary), parent_id
Asset Reference: id (primary), asset_id (foreignkey->Asset), parent_id (always an Asset)
---EDITED 5/27----
Sample Relevent Table Data (after joins):
id | asset_id | name | parent_id | milestone | type
3 3 suit null march shape
4 4 suit_banker 3 april texture
5 5 tie null march shape
6 6 tie_red 5 march texture
7 7 tie_diamond 5 june texture
-5 6 tie_red 4 march texture
the id < 0 (like the last row) signify assets that are referenced. Referenced assets have a few columns that are overidden (in this case, only parent_id is important).
The expectation is that if I select all assets from april, I should do a secondary select to get the entire tree branches of the matching query:
so initially the query match would result in:
4 4 suit_banker 3 april texture
Then after the CTE, we get the complete hierarchy and our result should be this (so far this is working)
3 3 suit null march shape
4 4 suit_banker 3 april texture
-5 6 tie_red 4 march texture
and you see, the parent of id:-5 is there, but what is missing, that is needed, is the referenced asset, and the parent of the referenced asset:
5 5 tie null march shape
6 6 tie_red 5 march texture
Currently my solution works for this, but it is limited to only a single depth of references (and I feel the implementation is quite ugly).
---Edited----
Here is my primary Selection Function. This should better demonstrate where the real complication lies: the AssetReference.
Select A.id as id, A.id as asset_id, A.name,A.parent_id as parent_id, A.subPath, T.name as typeName, A2.name as parent_name, B.name as batchName,
L.name as locationName,AO.owner_name as ownerName, T.id as typeID,
M.name as milestoneName, A.deleted as bDeleted, 0 as reference, W.phase_name, W.status_name
FROM Asset as A Inner Join Type as T on A.type_id = T.id
Inner Join Batch as B on A.batch_id = B.id
Left Join Location L on A.location_id = L.id
Left Join Asset A2 on A.parent_id = A2.id
Left Join AssetOwner AO on A.owner_id = AO.owner_id
Left Join Milestone M on A.milestone_id = M.milestone_id
Left Join Workflow as W on W.asset_id = A.id
where A.deleted <= #showDeleted
UNION
Select -1*AR.id as id, AR.asset_id as asset_id, A.name, AR.parent_id as parent_id, A.subPath, T.name as typeName, A2.name as parent_name, B.name as batchName,
L.name as locationName,AO.owner_name as ownerName, T.id as typeID,
M.name as milestoneName, A.deleted as bDeleted, 1 as reference, NULL as phase_name, NULL as status_name
FROM Asset as A Inner Join Type as T on A.type_id = T.id
Inner Join Batch as B on A.batch_id = B.id
Left Join Location L on A.location_id = L.id
Left Join Asset A2 on AR.parent_id = A2.id
Left Join AssetOwner AO on A.owner_id = AO.owner_id
Left Join Milestone M on A.milestone_id = M.milestone_id
Inner Join AssetReference AR on AR.asset_id = A.id
where A.deleted <= #showDeleted
I have a stored procedure that takes a temp table (#temp) and finds all the elements of the hierarchy. The strategy I employed was this:
Select the entire system heirarchy into a temp table (#treeIDs) represented by a comma separated list of each entire tree branch
Get entire heirarchy of assets matching query (from #temp)
Get all reference assets pointed to by Assets from heirarchy
Parse the heirarchy of all reference assets
This works for now because reference assets are always the last item on a branch, but if they weren't, i think i would be in trouble. I feel like i need some better form of recursion.
Here is my current code, which is working, but i am not proud of it, and I know it is not robust (because it only works if the references are at the bottom):
Step 1. build the entire hierarchy
;WITH Recursive_CTE AS (
SELECT Cast(id as varchar(100)) as Hierarchy, parent_id, id
FROM #assetIDs
Where parent_id is Null
UNION ALL
SELECT
CAST(parent.Hierarchy + ',' + CAST(t.id as varchar(100)) as varchar(100)) as Hierarchy, t.parent_id, t.id
FROM Recursive_CTE parent
INNER JOIN #assetIDs t ON t.parent_id = parent.id
)
Select Distinct h.id, Hierarchy as idList into #treeIDs
FROM ( Select Hierarchy, id FROM Recursive_CTE ) parent
CROSS APPLY dbo.SplitIDs(Hierarchy) as h
Step 2. Select the branches of all assets that match the query
Select DISTINCT L.id into #RelativeIDs FROM #treeIDs
CROSS APPLY dbo.SplitIDs(idList) as L
WHERE #treeIDs.id in (Select id FROM #temp)
Step 3. Get all Reference Assets in the branches
(Reference assets have negative id values, hence the id < 0 part)
Select asset_id INTO #REFLinks FROM #AllAssets WHERE id in
(Select #AllAssets.asset_id FROM #AllAssets Inner Join #RelativeIDs
on #AllAssets.id = #RelativeIDs.id Where #RelativeIDs.id < 0)
Step 4. Get the branches of anything found in step 3
Select DISTINCT L.id into #extraRelativeIDs FROM #treeIDs
CROSS APPLY dbo.SplitIDs(idList) as L
WHERE
exists (Select #REFLinks.asset_id FROM #REFLinks WHERE #REFLinks.asset_id = #treeIDs.id)
and Not Exists (select id FROM #RelativeIDs Where id = #treeIDs.id)
I've tried to just show the relevant code. I am super grateful to anyone who can help me find a better solution!

--getting all of the children of a root node ( could be > 1 ) and it would require revising the query a bit
DECLARE #AssetID int = (select AssetId from Asset where AssetID is null);
--algorithm is relational recursion
--gets the top level in hierarchy we want. The hierarchy column
--will show the row's place in the hierarchy from this query only
--not in the overall reality of the row's place in the table
WITH Hierarchy(Asset_ID, AssetID, Levelcode, Asset_hierarchy)
AS
(
SELECT AssetID, Asset_ID,
1 as levelcode, CAST(Assetid as varchar(max)) as Asset_hierarchy
FROM Asset
WHERE AssetID=#AssetID
UNION ALL
--joins back to the CTE to recursively retrieve the rows
--note that treelevel is incremented on each iteration
SELECT A.Parent_ID, B.AssetID,
Levelcode + 1 as LevelCode,
A.assetID + '\' + cast(A.Asset_id as varchar(20)) as Asset_Hierarchy
FROM Asset AS a
INNER JOIN dbo.Batch AS Hierarchy
--use to get children, since the parentId of the child will be set the value
--of the current row
on a.assetId= b.assetID
--use to get parents, since the parent of the Asset_Hierarchy row will be the asset,
--not the parent.
on Asset.AssetId= Asset_Hierarchy.parentID
SELECT a.Assetid,a.name,
Asset_Hierarchy.LevelCode, Asset_Hierarchy.hierarchy
FROM Asset AS a
INNER JOIN Asset_Hierarchy
ON A.AssetID= Asset_Hierarchy.AssetID
ORDER BY Hierarchy ;
--return results from the CTE, joining to the Asset data to get the asset name
---that is the structure you will want. I would need a little more clarification of your table structure

It would help to know your underlying table structure. There are two approaches which should work depending on your environment: SQL understands XML so you could have your SQL as an xml structure or simply have a single table with each row item having a unique primary key id and a parentid. id is the fk for the parentid. The data for the node are just standard columns. You can use a cte or a function powering a calculated column to determin the degree of nesting for each node. The limit is that a node can only have one parent.

Related

How to find an objects that doesn't have some required properties from dictionary

I have three tables:
object
id
Name
1
ball
2
pencil
object_properties
object_id
property_id
image
1
4
path
1
5
path
1
6
path
2
5
path
property
id
name
4
left
5
right
6
top
All rows in the table property are required for object_properties.
In this case query should find the second object pencil, because it doesn't have the all properties.
I tried a query:
select b.*
from objects b
left join object_properties p ON b.id = p.object_id
where property_id not in (select id from property)
But it's not working.
SQL Fiddle
Expected result:
id
Name
2
pencil
An alternative to Gordon's query is to not only compare the number of records, but the records themselves:
WITH j AS (
SELECT b.id,array_agg(p.property_id) prop
FROM objects b
LEFT JOIN object_properties p ON b.id = p.object_id
GROUP BY b.id
)
SELECT * FROM j
WHERE prop <> (SELECT array_agg(id) FROM property);
If you just count the records, the query will filter out objects that e.g. contain left three times, which is most certainly invalid.
Demo: db<>fiddle
You can use aggregation to count the properties on each property and then having to filter down to the ones missing properties:
select o.id, o.name
from objects o left join
object_properties op
on o.id = op.object_id left join
properties p
on op.property_id = p.id
group by o.id, o.name
having count(p.id) <> (select count(*) from properties);
Here is a db<>fiddle. Note that this returns both objets because the properties table has four rows not three.

Combine rows from Mulitple tables into single table

I have one parent table Products with multiple child tables -Hoses,Steeltubes,ElectricCables,FiberOptics.
ProductId -Primary key field in Product table
ProductId- ForeignKey field in Hoses,Steeltubes,ElectricCables,FiberOptics.
Product table has 1 to many relationship with Child tables
I want to combine result of all tables .
For eg - Product P1 has PK field ProductId which is used in all child tables as FK.
If Hoses table has 4 record with ProductId 50 and Steeltubes table has 2 records with ProductId 50 when I perform left join then left join is doing cartesian product of records showing 8 record as result But it should be 4 records .
;with HOSESTEELCTE
as
(
select '' as ModeType, '' as FiberOpticQty , '' as NumberFibers, '' as FiberLength, '' as CableType , '' as Conductorsize , '' as Voltage,'' as ElecticCableLength , s.TubeMaterial , s.TubeQty, s.TubeID , s.WallThickness , s.DWP ,s.Length as SteelLength , h.HoseSeries, h.HoseLength ,h.ProductId
from Hoses h
left join
(
--'' as HoseSeries,'' as HoseLength ,
select TubeMaterial , TubeQty, TubeID , WallThickness , DWP , Length,ProductId from SteelTubes
) s on (s.ProductId = h.ProductId)
) select * from HOSESTEELCTE
Assuming there are no relationships between child tables and you simply want a list of all child entities which make up a product you could generate a cte which has a number of rows which are equal to the largest number of entries across all the child tables for a product. In the example below I have used a dates table to simplify the example.
so for this data
create table products(pid int);
insert into products values
(1),(2);
create table hoses (pid int,descr varchar(2));
insert into hoses values (1,'h1'),(1,'h2'),(1,'h3'),(1,'h4');
create table steeltubes (pid int,descr varchar(2));
insert into steeltubes values (1,'t1'),(1,'t2');
create table electriccables(pid int,descr varchar(2));
truncate table electriccables
insert into electriccables values (1,'e1'),(1,'e2'),(1,'e3'),(2,'e1');
this cte
;with cte as
(select row_number() over(partition by p.pid order by datekey) rn, p.pid
from dimdate, products p
where datekey < 20050105)
select * from cte
create a cartesian join (one of the rare ocassions where an implicit join helps) pid to rn
result
rn pid
-------------------- -----------
1 1
2 1
3 1
4 1
1 2
2 2
3 2
4 2
And if we add the child tables
;with cte as
(select row_number() over(partition by p.pid order by datekey) rn, p.pid
from dimdate, products p
where datekey < 20050106)
select c.pid,h.descr hoses,s.descr steeltubes,e.descr electriccables from cte c
left join (select h.*, row_number() over(order by h.pid) rn from hoses h) h on h.rn = c.rn and h.pid = c.pid
left join (select s.*, row_number() over(order by s.pid) rn from steeltubes s) s on s.rn = c.rn and s.pid = c.pid
left join (select e.*, row_number() over(order by e.pid) rn from electriccables e) e on e.rn = c.rn and e.pid = c.pid
where h.rn is not null or s.rn is not null or e.rn is not null
order by c.pid,c.rn
we get this
pid hoses steeltubes electriccables
----------- ----- ---------- --------------
1 h1 t1 e1
1 h2 t2 e2
1 h3 NULL e3
1 h4 NULL NULL
2 NULL NULL e1
In fact, the result having 8 rows can be expected to be the result, since your four records are joined with the first record in the other table and then your four records are joined with the second record of the other table, making it 4 + 4 = 8.
The very fact that you expect 4 records to be in the result instead of 8 shows that you want to use some kind of grouping. You can group your inner query issued for SteelTubes by ProductId, but then you will need to use aggregate functions for the other columns. Since you have only explained the structure of the desired output, but not the semantics, I am not able with my current knowledge about your problem to determine what aggregations you need.
Once you find out the answer for the first table, you will be able to easily add the other tables into the selection as well, but in case of large data you might get some scaling problems, so you might want to have a table where you store these groups, maintain it when something changes and use it for these selections.

pad database out with NULL criteria

If I have the following sample table (order by ID)
ID Date Type
-- ---- ----
1 01/01/2000 A
2 22/04/1995 A
2 14/02/2001 B
Where you can immediate see that ID=1 does not have a Type=B, but ID=2 does. What I want to do, if fill in a line to show this:
ID Date Type
-- ---- ----
1 01/01/2000 A
1 NULL B
2 22/04/1995 A
2 14/02/2001 B
where there could potentially be 100's of different types, (so may need to end up inserting 100's rows per person if they lack 100's Types!)
Is there a general solution to do this?
Could I possibly outer join the table on itself and do it that way?
You can do this with a cross join to generate all the rows and a left join to get the actual data values:
select i.id, s.date, t.type
from (select distinct id from sample) i cross join
(select distinct type from sample) t left join
sample s
on s.id = i.id and
s.type = t.type;

To arrange the hierarchy in a table

I have a table dbo.Hierarchy that contains the following data:
Level1 Level2 Level3 Level4 Level5 Level6 Level7 Level8 Level9 Level10
-----------------------------------------------------------------------
a b c d e f g h i j
k l m n o
There are a total of 10 levels and any item can have hierarchy upto any level. In the above data a is the parent of b, b is the parent of c and so on. j and o are the last levels in their respective hierarchies. How can I get the output in the below format:
Name ParentName LevelID
-------------------------------
a NULL 1
b a 2
j i 10
k NULL 1
l k 2
o n 5
Something like (untested)
with t(L1,L2,L3,L4,L5,L6,L7,L8,L9,L10) as (
values ('a','b','c','d','e','f','g','h','i','j')
, ('k','l','m','n','o',null,null,null,null,null)
)
select x.*
from t
cross apply (
values (L1,null,1),(L2,L1,2),(L3,L2,3),(L4,L3,4),(L5,L4,5)
, (L6,L5,6),(L7,L6,7),(L8,L7,8),(L9,L8,9),(L10,L9,10)) x (name, parentname, levelid)
where name is not null
Try this:
;with base as
(select *, row_number() over (order by level1) rn from tbl),
src as
(
select
valname as name,
cast(substring(colname,6,len(colname)) as int) as level,
rn from
(select * from base) s
unpivot
(
valname
for colname in ([level1],[level2],[level3],[level4],[level5],[level6],[level7], [level8],[level9],[level10])
) u
),
cte as
(select * from src
where level = 1
union all
select s.* from src s
inner join cte c on s.level = c.level + 1 and s.rn = c.rn)
select distinct s.name, t.name parentname, s.level levelid from
cte s
left join cte t on s.rn = t.rn and s.level = t.level + 1
Breakdown:
CTE base is used to generate row number as a derived column. We will use this column to keep track of which values belong to which row. This will help in uniquely mapping children to their parents.
CTE src is where we transform the table from this denormalised structure to a normalised one. Using UNPIVOT, we reduce the data set to 3 columns - name, level and the row number rn.
CTE cte is a recursive CTE that we use to get all possible combinations of parents and children (including immediate parents as well as ancestors).
Finally, we LEFT JOIN cte to itself on the condition that row number is same on both sides of the join i.e. the values belong to same record from the base table, and also that the value from the right side is the immediate ancestor (parent) of the value on the left side.
Demo
The huge mass of code above can be avoided, if you were to choose a normalised structure for your table. I would suggest something like this:
CREATE TABLE tbl
(ID int, --Keep track of values that are related to each other
Name varchar(100), --Name
Level int --The level for a particular value
)
With this proposed structure, all you would need is the recursive CTE (cte from the above code) and the left join to get the parent-child data. The beauty of this approach is that you can extend it to any number of levels that you like, without having to hard-code the level numbers.
Demo

Joining 3 Tables Using Newest Rows

I have 3 tables in my database: children, families, statuslog
Every time a child is checked in or out of the database, it is updated in the statuslog. I've done this a long time ago, but I can't seem to figure out how to do it anymore. I want to create a new view that joins all 3 tables, but I only want the newest entry from statuslog (by using the highest id).
For example, statuslog looks like this:
childID researcher status id
1 Dr. A out 1
1 Dr. A in 2
1 Dr. B out 3
1 Dr. B in 4
2 Dr. C out 5
2 Dr. C in 6
3 Dr. B out 7
3 Dr. B in 8
This is what I want to do:
SELECT *
FROM children, families, statuslog
WHERE children.familyID = families.familyID AND children.childID = statuslog.childID
Obviously, this will return the children+families tuples coupled with every single log entry, but I can't remember how to only combine it with the newest log entry.
Any help would be appreciated!
Aggregate query with max(id) retrieves last ID given a childID. This is then joined to statuslog to retrieve other columns.
SELECT *
FROM children
INNER JOIN families
ON children.familyID = families.familyID
INNER JOIN
(
SELECT childID, researcher, status
FROM statuslog
INNER JOIN
(
SELECT childID, max(ID) ID
FROM statuslog
GROUP BY childID
) lastSL
ON statuslog.childID = lastSL.childid
AND statuslog.ID = lastSL.ID
) sl
ON children.childID = sl.childID
This seems to be the typical greatest-n-per-group in which the higher id is interpreted as the newest. This query should do the trick:
select * from (
select s1.* from statusLog s1
left join statusLog s2
on s1.childId = s2.childId and s1.id < s2.id
where s2.id is null
) final
join children c on c.childId = final.childId
join families f on f.familyId = c.familyId
Correct any syntactical errors.