Aggregate values by parents recursively in Oracle - sql

Consider the the following example structure:
DEPARTMENT
ID
PARENT_ID
NAME
DEPTH
PROJECT
ID
NAME
COST
DEPARTMENT_ID
Some data, just for the sake of the examples bellow:
| ID | PARENT_ID | NAME | DEPTH |
|----|-----------|-------|-------|
| 1 | NULL | DEPT1 | 1 |
| 2 | 1 | DEPT2 | 2 |
| 3 | 1 | DEPT3 | 2 |
| 4 | 2 | DEPT4 | 3 |
| 5 | 3 | DEPT5 | 3 |
| 6 | NULL | DEPT6 | 1 |
| 7 | 6 | DEPT7 | 2 |
| ID | NAME | COST | DEPARTMENT_ID |
|------|--------|-------|---------------|
| 1 | PRJ1 | 100 | 1 |
| 2 | PRJ2 | 200 | 2 |
| 3 | PRJ3 | 300 | 3 |
| 4 | PRJ4 | 400 | 4 |
| 5 | PRJ5 | 500 | 5 |
| 6 | PRJ6 | 600 | 6 |
| 7 | PRJ7 | 700 | 7 |
Now, I need to somehow aggregate the costs of the, projects by one department and then by its direct children.
If the choosen filter is DEPT1, the intented result is:
| LINE | DEPARTMENT_ID | PARENT_ID | NAME | AGGREGATE_COST |
|------|----------------|-----------|--------|----------------|
| 1 | 1 | NULL | DEPT1 | 1500 |
| 2 | 2 | 1 | DEPT2 | 600 |
| 3 | 3 | 1 | DEPT3 | 800 |
Where:
Line 3 aggregate is PRJ5 (of DEPT5, which is child of DEPT3) + PRJ3 (of DEPT3) cost
Line 2 aggregate is PRJ4 (of DEPT4, which is child of DEPT2) + PRJ2 (of DEPT2) cost
Line 1 aggregate is the sum of his childrens aggregates.
PRJ6 and PRJ7 costs are ignored because the are from DEPT6 and DEPT7, and those are not in the hierachy of DEPT1 (DEPT6 would be his sibling, not child)
EDIT:
| ID | NAME | COST | DEPARTMENT_ID |
|------|--------|-------|---------------|
| 1 | PRJ1 | 1 | 1 |
| 2 | PRJ2 | 1 | 1 |
| 3 | PRJ3 | 1 | 2 |
| 4 | PRJ4 | 1 | 2 |
| 5 | PRJ5 | 1 | 4 |
In this scenario, the solution ivanzg presented, doesn't seem to work.
I get doubled results for the projects in the highers ranks
If I get the aggregate for DEPT1, it returns something similar to this:
| LINE | DEPARTMENT_ID | PARENT_ID | NAME | AGGREGATE_COST |
|------|----------------|-----------|--------|----------------|
| 1 | 1 | NULL | DEPT1 | 8 |
| 2 | 2 | NULL | DEPT1 | 4 |

You can tag rows in a hierarchy query (to later create groups) by using CONNECT_BY_ROOT hierarchy operator. In the hierarchy query, by making all rows root rows you create every hierarchy combination, later only specified combinations are taken and aggregated. For your test data this returns what you specified.
SELECT ROOT_DEPT AS DEPARTMENT_ID
,ROOT_PARENT AS PARENT_ID
,ROOT_NAME AS NAME
,SUM(COST) AS AGGREGATE_COST
FROM (SELECT COST
,CONNECT_BY_ROOT DEPARTMENT_ID ROOT_DEPT
,CONNECT_BY_ROOT PARENT_ID ROOT_PARENT
,CONNECT_BY_ROOT NAME ROOT_NAME
FROM (SELECT B.DEPARTMENT_ID
,NVL(A.PARENT_ID,'0') PARENT_ID
,A.NAME
,SUM(B.COST) COST
FROM DEPARTMENT A
JOIN PROJECT B
ON A.ID = B.DEPARTMENT_ID
--> GROUP COST OF PROJECTS IN THE SAME DEPARTMENT IF THERE ARE ANY
GROUP BY B.DEPARTMENT_ID
,NVL(A.PARENT_ID,'0')
,A.NAME
)
--> MAKE ALL ROWS ROOT ROWS
CONNECT BY PRIOR DEPARTMENT_ID = PARENT_ID
)
WHERE ROOT_DEPT = 1 OR ROOT_PARENT = 1
GROUP BY ROOT_DEPT
,ROOT_PARENT
,ROOT_NAME

Related

How to handle multiple rows fulfilling criteria when joining Oracle tables

Given a table of roles, companies and a employee table where we store for each employee which role he/she has at each company.
I'm trying to create a view which indicates for each combination of role and company and employee by a ‘Y’ or ‘N’ in the “checked_yn” column, whether this employee has this role at this company.
company table
----------------
|ID | name |
-----------------
| 1 | A |
| 2 | B |
-----------------
roles table
-------------
|ID | role |
-------------
| 1 | X |
| 2 | Y |
| 3 | Z |
-------------
employee table
----------------------------------------------
|ID | company_id | role_id | employee_log_id |
---------------------------------------------|
| 1 | 1 | 1 | 1 |
| 2 | 1 | 2 | 1 |
| 3 | 2 | null | 1 |
----------------------------------------------
The desired outcome is this:
EMPLOYEE_ROLES_VW view
------------------------------------------------------------------------
|Id |company_id | role_id | Checked_yn | employee_id | employee_log_id |
|----------------------------------------------------------------------|
| 1 | 1 | 1 | Y | 1 | 1 |
| 2 | 1 | 2 | Y | 2 | 1 |
| 3 | 1 | 3 | N | null | 1 |
| 4 | 2 | 1 | N | null | 1 |
| 5 | 2 | 2 | N | null | 1 |
| 6 | 2 | 3 | N | null | 1 |
------------------------------------------------------------------------
This is my current query:
with ROLES_X_COMP as (SELECT ROL.ID AS X_ROLE_ID,
COM.ID AS X_COMPANY_ID,
FROM ROLES ROL
CROSS JOIN COMPANY COM)
SELECT ROWNUM AS ID,
EMP.ID AS SMCR_EMPLOYEE_ID,
EMP.EMPLOYEE_LOG_ID AS EMPLOYEE_LOG_ID,
ROLES_X_COMP.X_ROLE_ID ,
EMP.ROLE_ID AS ROLE_ID,
ROLES_X_COMP.X_COMPANY_ID,
EMP.COMPANY_ID AS COMPANY_ID,
CASE
WHEN ROLES_X_COMP.X_ROLE_ID = SE.ROLE_ID AND ROLES_X_COMP.X_COMPANY_ID =
SE.COMPANY_ID THEN 'Y'
ELSE 'N' END AS CHECKED_YN
FROM ROLES_X_COMP
LEFT OUTER JOIN EMPLOYEE EMP ON ROLES_X_COMP.X_COMPANY_ID = EMP.COMPANY_ID
Because of the join on EMPLOYEE “finds” the company with id=1 twice it joins twice with the cross join of role and company table. So I'm getting this result:
------------------------------------------------------------------------
|Id |company_id | role_id | Checked_yn | employee_id | employee_log_id |
|----------------------------------------------------------------------|
| 1 | 1 | 1 | Y | 1 | 1 |
| 2 | 1 | 2 | N | 1 | 1 |
| 3 | 1 | 3 | N | 1 | 1 |
| 4 | 1 | 1 | N | 2 | 1 |
| 5 | 1 | 2 | Y | 2 | 1 |
| 6 | 1 | 3 | N | 2 | 1 |
| 7 | 2 | 1 | N | 3 | 1 |
| 8 | 2 | 2 | N | 3 | 1 |
| 9 | 2 | 3 | N | 3 | 1 |
------------------------------------------------------------------------
I think a JOIN might be the wrong option here and a UNION more appropriate but I can't figure it out.
Use a partitioned outer join:
Query:
SELECT ROWNUM AS id,
e.company_id,
r.id AS role_id,
NVL2( e.role_id, 'Y', 'N' ) AS CheckedYN,
e.role_id AS employee_id,
e.employee_log_id
FROM roles r
LEFT OUTER JOIN
employee e
PARTITION BY ( e.company_id, e.employee_log_id )
ON ( r.id = e.role_id )
or (depending on how you want to partition and join the data):
SELECT ROWNUM AS id,
c.id AS company_id,
r.id AS role_id,
NVL2( e.role_id, 'Y', 'N' ) AS CheckedYN,
e.role_id AS employee_id,
e.employee_log_id
FROM roles r
CROSS JOIN
company c
LEFT OUTER JOIN
employee e
PARTITION BY ( e.employee_log_id )
ON ( c.id = e.company_id AND r.id = e.role_id )
Output:
Both output the same for the test data but may give differing results depending on your actual data.
ID | COMPANY_ID | ROLE_ID | CHECKEDYN | EMPLOYEE_ID | EMPLOYEE_LOG_ID
-: | ---------: | ------: | :-------- | ----------: | --------------:
1 | 1 | 1 | Y | 1 | 1
2 | 1 | 2 | Y | 2 | 1
3 | 1 | 3 | N | null | 1
4 | 2 | 1 | N | null | 1
5 | 2 | 2 | N | null | 1
6 | 2 | 3 | N | null | 1
db<>fiddle here
AND ROLES_X_COMP.X_ROLE_ID = EMP.ROLE_ID
Is missing at the end of your query
But the outcome will be
EMPLOYEE_ROLES_VW view
------------------------------------------------------------------------
|Id |company_id | role_id | Checked_yn | employee_id | employee_log_id |
|----------------------------------------------------------------------|
| 1 | 1 | 1 | Y | 1 | 1 |
| 2 | 1 | 2 | Y | 2 | 1 |
| 3 | 1 | 3 | N | null | null |
| 4 | 2 | 1 | N | null | null |
| 5 | 2 | 2 | N | null | null |
| 6 | 2 | 3 | N | null | null |
------------------------------------------------------------------------

What is better way to create tables in SQL according to database normalization?

I have this table discount_user_product structure
discount_id | user_id | product_id
------------+---------+------------
1 | 1 | 1
1 | 1 | 2
1 | 2 | 3
My tables
Users
id | name |
----------+---------+-
1 | Bobby |
2 | Max |
3 | Joj |
Discounts
id | discount | expired_at
----------+----------+------------
1 | 100 |
2 | 50 |
3 | 30 |
Products
id | user_id | title
----------+---------+------------
1 | 1 | Milk
2 | 1 | Chees
3 | 2 | Jeam
I would like to use a better tables structure and decompose discount_user_product to this structure:
discount_user
discount_id | user_id |
------------+---------+-
1 | 1 |
1 | 2 |
user_product
user_id | product_id |
--------+------------+-
1 | 1 |
1 | 2 |
2 | 3 |
Is this will be the right way to make better performance and compliance with the rules of normalizations?

SQL compare multiple rows or partitions to find matches

The database I'm working on is DB2 and I have a problem similar to the following scenario:
Table Structure
-------------------------------
| Teacher Seating Arrangement |
-------------------------------
| PK | seat_argmt_id |
| | teacher_id |
-------------------------------
-----------------------------
| Seating Arrangement |
-----------------------------
|PK FK | seat_argmt_id |
|PK | Row_num |
|PK | seat_num |
|PK | child_name |
-----------------------------
Table Data
------------------------------
| Teacher Seating Arrangement|
------------------------------
| seat_argmt_id | teacher_id |
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 1 |
| 5 | 2 |
------------------------------
---------------------------------------------------
| Seating Arrangement |
---------------------------------------------------
| seat_argmt_id | row_num | seat_num | child_name |
| 1 | 1 | 1 | Abe |
| 1 | 1 | 2 | Bob |
| 1 | 1 | 3 | Cat |
| | | | |
| 2 | 1 | 1 | Abe |
| 2 | 1 | 2 | Bob |
| 2 | 1 | 3 | Cat |
| | | | |
| 3 | 1 | 1 | Abe |
| 3 | 1 | 2 | Cat |
| 3 | 1 | 3 | Bob |
| | | | |
| 4 | 1 | 1 | Abe |
| 4 | 1 | 2 | Bob |
| 4 | 1 | 3 | Cat |
| 4 | 2 | 2 | Dan |
---------------------------------------------------
I want to see where there are duplicate seating arrangements for a teacher. And by duplicates I mean where the row_num, seat_num, and child_name are the same among different seat_argmt_id for one teacher_id. So with the data provided above, only seat id 1 and 2 are what I would want to pull back, as they are duplicates on everything but the seat id. If all the children on the 2nd table are exact (sans the primary & foreign key, which is seat_argmt_id in this case), I want to see that.
My initial thought was to do a count(*) group by row#, seat#, and child. Everything with a count of > 1 would mean it's a dupe and = 1 would mean it's unique. That logic only works if you are comparing single rows though. I need to compare multiple rows. I cannot figure out a way to do it via SQL. The solution I have involves going outside of SQL and works (probably). I'm just wondering if there is a way to do it in DB2.
Does this do what you want?
select d.teacher_id, sa.row_num, sa.seat_num, sa.child_name
from seatingarrangement sa join
data d
on sa.seat_argmt_id = d.seat_argmt_id
group by d.teacher_id, sa.row_num, sa.seat_num, sa.child_name
having count(*) > 1;
EDIT:
If you want to find two arrangements that are the same:
select sa1.seat_argmt_id, sa2.seat_argmt_id
from seatingarrangement sa1 join
seatingarrangement sa2
on sa1.seat_argmt_id < sa2.seat_argmt_id and
sa1.row_num = sa2.row_num and
sa1.seat_num = sa2.seat_num and
sa1.child_name = sa2.child_name
group by sa1.seat_argmt_id, sa2.seat_argmt_id
having count(*) = (select count(*) from seatingarrangement sa where sa.seat_argmt_id = sa1.seat_argmt_id) and
count(*) = (select count(*) from seatingarrangement sa where sa.seat_argmt_id = sa2.seat_argmt_id);
This finds the matches between two arrangements and then verifies that the counts are correct.

SQL Query in MANY- MANY RELATIONSHIP exactly one record with matching criteria

I have 3 like with many - many relationship
As:
TABLE 1 : select * from student;
| id | name |
| 1 | sone |
| 2 | stwo |
| 3 | sthree |
| 4 | sfour |
| 6 | ssix |
TABLE 2 : select * from course;
| id | name |
| 100 | CSE |
| 101 | ECE |
| 102 | ITI |
RELATION_SHIP TABLE : select * from student_course
| id | stu_id | cou_id |
| 1 | 1 | 101 |
| 2 | 2 | 102 |
| 3 | 2 | 100 |
| 4 | 3 | 100 |
| 5 | 3 | 101 |
| 6 | 1 | 101 |
| 1 | 6 | 101 |
I need to write a query to select a student with exactly one course 'CSE' and he should not have any other courses.
Thanks in advance
Use query:
SELECT
sc.`stu_id`,
COUNT(sc.`cou_id`) AS cnt
FROM
student_course sc
GROUP BY sc.`stu_id`
HAVING cnt = 1
AND GROUP_CONCAT(cou_id) LIKE
(SELECT
id
FROM
course
WHERE NAME = 'CSE')

Oracle SQL Count of a field Grouped by another field

I have a small query below which i want to have a Count of each HOUSE_ID and Grouped By LOCATION_ID however it is not grouping by LOCATION_ID because the HOUSE_ID's are different. I want it to count HOUSE_ID's by LOCATION_ID's regardless of the HOUSE_ID.
QUERY
SELECT
COUNT(HOUSE_ID) AS Count,
LOCATION_ID,
ZONE,
AREA
FROM TABLE
WHERE SITE_ID = 'ABC'
AND LOCATION_ID NOT LIKE ('%LAND%')
GROUP BY LOCATION_ID, HOUSE_ID, ZONE, AREA
Expected Result
_____________________________
|Count|LOCATION_ID|ZONE|AREA|
|¯¯¯¯¯|¯¯¯¯¯¯¯¯¯¯¯|¯¯¯¯|¯¯¯¯|
| 4 | LOCA | 2 | 1 |
| 7 | LOCB | 6 | 2 |
| 3 | LOCC | 3 | 1 |
| 9 | LOCD | 5 | 7 |
| 6 | LOCE | 7 | 4 |
| 2 | LOCF | 2 | 1 |
| 8 | LOCG | 7 | 5 |
| 7 | LOCH | 9 | 1 |
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Actual Result
_____________________________
|Count|LOCATION_ID|ZONE|AREA|
|¯¯¯¯¯|¯¯¯¯¯¯¯¯¯¯¯|¯¯¯¯|¯¯¯¯|
| 1 | LOCA | 2 | 1 |
| 1 | LOCA | 6 | 2 |
| 1 | LOCA | 3 | 1 |
| 1 | LOCA | 5 | 7 |
| 1 | LOCA | 7 | 4 |
| 1 | LOCA | 2 | 1 |
| 1 | LOCA | 7 | 5 |
| 1 | LOCA | 9 | 1 |
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
You have to remove HOUSE_ID from group by clause.
Without source data I can only guess that you also need agregate functions for ZONE and AREA column, MAX fro example. Try below solution:
SELECT
COUNT(HOUSE_ID) AS Count,
LOCATION_ID,
MAX(ZONE),
MAX(AREA)
FROM TABLE
WHERE SITE_ID = 'ABC'
AND LOCATION_ID NOT LIKE ('%LAND%')
GROUP BY LOCATION_ID
Got it, Needed to Count(*)!!
SELECT
COUNT(*) AS Count,
SUM(AREA) AS AREA
LOCATION_ID,
ZONE,
FROM TABLE
WHERE SITE_ID = 'ABC'
AND LOCATION_ID NOT LIKE ('%LAND%')
GROUP BY LOCATION_ID, ZONE