So I have a crosstab that measures the amount of hours spent at a single facility. Our rows seperate the type of task for the hours worked, and the columns seperate the facility where the hours were worked.
The problem I have is that in our database we have 2 levels of facilities. Each Level 1 Facility has ten Level 2 "child" systems below it. What we want is to be able to roll all of the Level 2 facility columns' data into their respective Level 1 facility parent.
I've included an example below. The first t̶a̶b̶l̶e̶ crosstab is the one I have, and I want to get to the second t̶a̶b̶l̶e̶ crosstab.
So in our system the "parent" and "child" facilities are connected through a field called OBJ_PARENT. Each of the "children" have the name of its "parent" inside OBJ_PARENT.
I will give the SQL I have written up upon request.
EDIT: I've provided my current SQL below
select EVT_WORKORDER, EVT_DEPARTMENT, EVT_WOSTATUS, EVT_WOTYPE, EVT_FACILITY, OBJ_FACILITY, OBJ_PMD, OBJ_PARENT, BOO_HOURS, BOO_DATE, BOO_ENTERED, BOO_ACTIVITY, BOO_PERSON, ACT_ACTIVITY, ACT_TASK, ACT_WORKORDER, PRV_CODE, PRV_PROPERTY, PRV_VALUE,
sum(case when boo_person <> 'UNPAID' then boo_hours else 0 end ) "Paid Hours",
sum( boo_hours ) "All Hours"
FROM R5EVENTS
JOIN R5OBJECTS ON EVT_FACILITY = OBJ_FACILITY
JOIN R5BOOKEDHOURS ON BOO_EVENT = EVT_WORKORDER
join R5ACTIVITIES on EVT_WORKORDER = ACT_WORKORDER
JOIN r5propertyvalues ON ( ACT_TASK || '#0' = PRV_CODE)
where prv_property = 'CORESRV'
and ACT_ACTIVITY= BOO_ACTIVITY
AND EVT_DEPARTMENT = 'PK-MAINT'
AND EVT_WOTYPE IN ('JOB', 'PPM')
AND EVT_WOSTATUS IN ('R', 'FC', 'C', 'H', 'FI', 'CI', 'AP', 'IP', 'DF')
and OBJ_PMD in (#PROMPTMANY ('PMD')#)
and (OBJ_FACILITY in (#PROMPTMANY ('Park')#)
OR
OBJ_PARENT in ( SELECT OBJ_FACILITY from R5OBJECTS where OBJ_FACILITY in (#PROMPTMANY ('Park')#)))
group by EVT_WORKORDER, EVT_DEPARTMENT, EVT_WOSTATUS, EVT_WOTYPE, EVT_FACILITY, OBJ_FACILITY, OBJ_PMD, OBJ_PARENT, BOO_HOURS, BOO_DATE, BOO_ENTERED, BOO_ACTIVITY, BOO_PERSON, ACT_ACTIVITY, ACT_TASK, ACT_WORKORDER, PRV_CODE, PRV_PROPERTY, PRV_VALUE
BOO stands for "Booked"
The proper way would be:
Create a true hierarchy table in the database
Have all of the children (which, in this case with the way your data is stored, also includes what you refer to as Level 1) relate to a parent
Model in FM so that the fact table joins to the dimension table on the child
Expose the parent object from the hierarchy table
Bring the parent object into the top edge of the crosstab
If you want to do it quick and dirty, use some sort of string function to chop off the "child" part of the facility.
Related
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.
I'm trying to write a 2 step process to determine material types for different components.
Step 1: this includes results from a different query containing RecordID and Component Name :
Sample records :
RecordID
Component
Material
BR39590
00000000000000564792
000000000002073757
BR39590
00000000000000567649
000000000002073757
BR39591
00000000000000567650
000000000002073758
Above RecordId's will contain several component numbers and corresponding Materials
Loop through the Material derived from above query result and join to a different table called 'Material' to determine Material type, if Material type belongs to 'A' or 'B' the process should exit out and insert the records into a new table. If Material type does not belong to 'A' or B then Query should go back to step 1 and fetch the next component to look for 'A' or 'B' or blank resultset.
Material
Category
000000000002073757
A
000000000002073758
B
Above 2 steps are repeated for all RecordID's
Final Result:
RecordId
Material
Component
Category
BR39590
000000000002073757
00000000000000564792
A
BR39590
000000000002073757
00000000000000567649
A
BR39591
000000000002073758
00000000000000567650
B
I did not understand what you mean, With dynamic SQL or coding in SQL environment, your wishes can be met, but there is a much, much easier way.
with a left join you can find the end result.
select
c.RecordID,
c.Material,
c.Component,
m.Category
from components c left join material m on c.Material = m.Material
I'm improving a report that currently uses a static table using the lookup function to fill its data from a few different datasets. We're pretty sure this is causing the report to take a lot longer to run, so I'm trying to use a table that uses column groups to achieve the same effect from a single dataset.
Here's what my query currently looks like. This functions exactly as I want it to as long as there's data.
Select CatName, CatCount, Category = 'Category 1', Sorting = 1
FROM
(Select CatName, Count(CatName) as CatCount FROM DataSet WHERE Parameters)
UNION
Select CatName, CatCount, Category = 'Category 2', Sorting = 2
FROM
(Select CatName, Count(CatName) as CatCount FROM DataSet WHERE Parameters)
When there are CatNames and CatCounts to pull from the select statement, the Category works and is pulled by the table as a column group. I need all of the groups to exist at all times.
However, sometimes we don't have data that fits the parameters for a category. The result when that happens is that there isn't a row for the Category field to use and that group doesn't exist in the table. Is there any way I can force the Category field to exist regardless of the data?
If I understand the question correctly, then you may be able to use ISNULL. ISNULL returns either the value you were for which you were looking (check_expression) or the alternative (replacement_value) if check_expression is NULL.
ISNULL ( check_expression , replacement_value )
Select CatName, CatCount, Category = 'Category 2', Sorting = 2
FROM
(Select isnull(CatName,""), Count(CatName) as CatCount FROM DataSet WHERE Parameters)
EDIT
How about a left outer join?
Select b.CatName, b.CatCount, Category = 'Category 2', Sorting = 2
FROM
(select '' as CatName, 0 as Catcount) a left outer join (Select CatName, Count(CatName) as CatCount FROM DataSet WHERE Parameters) b on a.CatName = b.CatName
Found a solution. Took a few tries, not the prettiest, and we'll have to see if it actually improves performance, but it works the way we wanted. Generalized code:
Select C.CatName, C.CatCount, Category = 'Category 1', Sorting = 1
FROM
(Select Top 5 B.CatName, Count(B.CatName) as CatCount
FROM
(Select CatName = case when CatOnlyParam in (Category1Filter) then A.CatName else NULL end
FROM
(Select CatName FROM DataSet WHERE GeneralParameters) as A
) as B
order by CatCount
) as C
UNION
etc
Separating the parameters into different steps guarantees that there will be values for each category, even if those values are NULL. I'm sure there's a cleaner way to get the same effect, but this functions.
Working from the inside out:
Stage 1 (Select statement A): Selects the value from the dataset with very general parameters (between a start and end date, resolved or not, etc).
Stage 2 (Select statement B): Uses the case statement to only pull the data that is relevant for this department while leaving behind NULLs for the data that isn't.
Stage 3 (Select statement C): Takes the data from the list of names and NULLs and gets a count from it. Sorts by that count and takes the top 5. If a category has no data, then the nulls will get "counted" to 0 and passed on to the final step.
Stage 4 (Final select statement): Adds the static fields to the information from the previous step. A category without data will get passed to this as:
CatName: NULL
CatCount: 0
Category: "Category 1"
Sorting: 1
Then this is repeated for the other categories and UNION'd together. Any suggestions to improve this are more than welcome.
I'm at a point within one of my Oracle APEX projects where I need to implement different levels of security for specific individuals for specific applications.
To start, I created a cartesian containing the information from the user table, the app table, and the role table.
It looks like this:
SELECT
A.user_id, B.app_id, C.role_id
FROM user A, app B, role C
ORDER BY A.user_id ASC, B.app_id ASC, C.role_id ASC
This allows me to return EVERY combination of user, app, and role. w/o using a where clause it returns over 303k rows. currently almost 500 users, 6 roles, and over 100 apps.
when I select from this view for a specific user its returning in approximately 10 ms which is acceptable.
Now, I also have a vw that stores each user's app/role assignment. I've joined this table to the cartesian in the following fashion.
SELECT
A.*,
DECODE(B.app_right_id, null, 0, 1) AS user_access
FROM
vw_user_app_role A -- My cartesian view
LEFT JOIN vw_tbl_user_app_role B
ON A.user_id = B.user_id
AND A.app_id = B.app_id
AND A.role_id = B.role_id
This returns a very usable set of data that resembles
user_id app_id role_id user_access
50 5 1 0
50 10 2 1
50 15 3 1
75 5 1 1
75 10 2 0
75 15 3 0
I'm considering what my next step should be, If I should create a pivot of the data where the app_id would be the row, the role_id would be the columns, and the user_access would be the "data". The "data" would ultimately be rendered as a check box on a website with the appropriate row/column headings.
I'm also considering using a pure ajax/json solution where I will build the json string using pl sql and return the entire string to the client to be processed via jquery.
I'm concerned with the difficulty of the first option (i'm very new to pl sql, and I'm unsure of how to generate a pivot table to be used in this version of oracle (v 10) ) and I'm concerned with the expense of creating an entire json string that will contain so much data.
Any suggestions would be greatly appreciated.
EDIT
I've achieved the pivot table that I desired via the following sql:
SELECT
B.application_nm,
A.user_id,
MAX(DECODE(b.role_name, 'role 1', A.USER_ACCESS, NULL)) "role 1",
MAX(DECODE(b.role_name, 'role 2', A.USER_ACCESS, NULL)) "role 2",
MAX(DECODE(b.role_name, 'role 3', A.USER_ACCESS, NULL)) "role 3",
MAX(DECODE(b.role_name, 'role 4', A.USER_ACCESS, NULL)) "role 4",
MAX(DECODE(b.role_name, 'role 5', A.USER_ACCESS, NULL)) "role 5",
MAX(DECODE(b.role_name, 'role 6', A.USER_ACCESS, NULL)) "role 6"
FROM
vw_user_app_access A LEFT JOIN vw_tbl_app B ON A.app_id = B.app_id
LEFT JOIN vw_tbl_roles C ON A.role_id = C.role_id
GROUP BY B.application_name, A.user_id
ORDER BY A.user_id DESC
Only problem is when in the future we have to add 'role 7'. I have to then go back into this query and add the line MAX(DECODE(b.role_name, 'role 7', A.USER_ACCESS, NULL)) "role 7"
Thinking ahead, this may be an inconvenience, but considering APEX's framework, I would have to go into the report any way to update the number of columns manually i believe.
I'm thinking this may be the "best" solution for now, unless anyone has any other suggestions...
It is possible for an Apex report region based on a dynamic SQL query to return a different number of columns as the query changes. I have set up a simple demo on apex.oracle.com. Type a new column name into the Columns tabular form and press "Add Row", and the Matrix report is re-drawn with an extra column of that name.
You have to:
Base the report on a function that returns the SQL to be run as a string
Select the region attribute "Use Generic Column Names (parse query at runtime only)"
Set the report Headings Type to PL/SQL and then use a function to dynamically return the required column headings as a colon-separated list. Note that this can be different from the column name, although my example uses the same text for both.
If my example isn't clear enough I'll add more info later - I'm out of time now.
I hope any sense can be made from my explanation. I was able to create the query, however my query only works for Items related to Containers, AND only if no more than one Items are related. I really hope anybody can be of any assistance!
Consider the following objects:
Container
Person
Item
I have one table where instances of all objects are stored. The table uses a self-referencing parent-child construction so it is db-technically possible to put a Container 'inside' a Person (just mentioning, this is not happening).
Object_Instances
objectid
parentid
typeid
typespecification (same as containerid, personid or itemid => one of three is filled)
containerid
personid
itemid
I have two tables which can be used to link persons/items to containers:
Container_Person
containerid
personid
amount
required (boolean)
Container_Item
containerid
itemid
amount
required (boolean)
(there are also a person/*item* table)
Now for an instance of a container I would like to calculate a number between 0 and 1 which is based on the related Container_Person and Container_Item specification in the following way:
if NO Container_Person/Container_Item are related to the Container => result = 1
if there are related records they should be taken into account in the following manner:
if the Container DOES NOT contain (has a child record of) ALL of the related persons/items which are required => result 0
otherwise:
result = average based on:
(# child records vs amount in Container_xxx relation)
if there are more than 1 person or item related to the container then the 'weight' for the (#records vs amount) value should be the ratio between the related Container_xxx.amount values for that Container.
Here is my current 'solution':
This query only works for one related item to a container. It doesn't take persons into account in any way... If more than one item are related to the container then the query returns multiple records.
So my actual question is: How can I group/sum(calculate to 0 - 1 decimal) the results of the following query based on the _Person / _Item related amount/required specification?
SELECT
Container.name,
Item.name,
(ifnull(Sum(Object_Instance.amount),0) / Container_Item.amount) as value
FROM
Container
Inner Join Object_Instances as Containers
ON Containers.typeid = 'container'
AND Container.containerid = Containers.typespecification
Left Outer Join Container_Item ON Container_Item.containerid = Container.containerid
Left Outer Join Item ON Item.itemid = Container_Item.itemid
Left Outer Join Object_Instance as ContainerItems
ON Item.itemid = ContainerItems.typespecification
AND ContainerItems.typeid = 'item'
AND ContainerItems.parentid = Containers.objectid
WHERE Containers.objectid = 1
GROUP BY
Container.name,
Container_Item.amount,
Item.name,
Container.containerid
When doing complex joins and grouping I will often have to resort to this type of query:
Create grouped (or unique) list
In a new query take the grouped list and then join in a ungrouped list that provides additional details that are needed.