How to get average with recursive query - sql

I'm trying to write recursive query with postgres and its working fine and returning me all users which comes under user 5 with this codes :
WITH RECURSIVE subordinates AS (
SELECT
id,
supervisor_id,
name
FROM
employees
WHERE
id = 5
UNION
SELECT
e.id,
e.supervisor_id,
e.name
FROM
employees e
INNER JOIN subordinates s ON s.id = e.supervisor_id
) SELECT
*
FROM
subordinates;
But I've 1 more table in it which is called biscuits which have 3 columns type, weight and cooked_by_employee_id
now I want average weight of biscuits with id, name and supervisor_id but just small twist is if user id 1 comes under 3 and 3 comes under 4 and 4 comes under 5 then it should return average weight of all 1,3,4 and 5 cooked_by_employee_id records and 1 user can have multiple records in biscuits
I tried this but not working
WITH RECURSIVE subordinates AS (
SELECT
e.id,
e.supervisor_id,
e.name,
AVG(b.weight)
FROM
employees e
LEFT JOIN burrito b ON
e.id=b.cooked_by_employee_id
WHERE
e.id = 5
UNION
SELECT
e.id,
e.supervisor_id,
e.name,
b.weight
FROM
employees e
INNER JOIN subordinates s ON s.id = e.supervisor_id
LEFT JOIN burrito b ON b.cooked_by_employee_id=e.supervisor_id
) SELECT
*
FROM
subordinates;
Sample data :
employees :
+----+------+---------------+
| id | name | supervisor_id |
+----+------+---------------+
| 1 | a | 3 |
+----+------+---------------+
| 2 | b | 4 |
+----+------+---------------+
| 3 | c | 5 |
+----+------+---------------+
| 4 | d | 0 |
+----+------+---------------+
| 5 | e | 0 |
+----+------+---------------+
burrito :
+----+-------+--------+-----------------------+
| id | type | weight | cooked_by_employee_id |
+----+-------+--------+-----------------------+
| 1 | sweet | 1 | 1 |
+----+-------+--------+-----------------------+
| 2 | salty | 2 | 1 |
+----+-------+--------+-----------------------+
| 3 | sweet | 3 | 3 |
+----+-------+--------+-----------------------+
So if it runs for employee_id 5 then it should return
+-------------+------+-----------------------------------------------------+
| employee_id | name | weight |
+-------------+------+-----------------------------------------------------+
| 5 | e | 2 (Average 2 because both 1 and 3 id comes under 5) |
+-------------+------+-----------------------------------------------------+

You have the recursive CTE figured out and that could be half (or more) of the battle. The following however builds a slightly different one. The recursive cte (path_to_chef) gets employee ids and builds a path from the top level to the employee. The second cte (cook_chef) then strips the path string back to the top level employee id. The result then contains 2 columns, the employee id and the employee id for the root level employee. This then sets the stage to join with the other tables for the final result.
The query can work as stand alone and for a single root level by supplying id where indicated. But I tend to create generic reusable routines so I've wrapped it into a SQL function.
create or replace function chef_avg_burrito_weight()
returns table ( employee_id integer
, name text
, weight numeric
)
language sql
as $$
with recursive path_to_chef as
( select id, trim(to_char(id, '9999'))||'>' path
from employees
where supervisor_id = 0
union all
select e.id, path || trim(to_char(e.id, '9999'))||'>'
from employees e
join path_to_chef c on e.supervisor_id = c.id
) -- select * from path_to_chef
, cook_chef as
(select id cook
, substring(path from '^([[:digit:]]+)>')::integer chef
from path_to_chef
) -- select * from cook_chef
select e.id, e.name, round(avg(b.weight),2) average_weight
from burrito b
join cook_chef c on (b.cooked_by_employee_id = c.cook)
join employees e on (e.id = c.chef)
-- where chef = 5
group by e.id, e.name;
$$;
-- test
select *
from chef_avg_burrito_weight()
where employee_id = 5;

Related

Update MyTable with values from AnotherTable (with self join)

I'm relatively new to SQL and currently making some practical tasks to gain experience and got struggled with an update of my custom overview table with values from another table that contains join.
I have an overview table MyTable with column EmployeeID. AnotherTable contains data of employees with EmployeeID and their ManagerID.
I am able to retrieve ManagerName using different join methods, including:
SELECT m.first_name
FROM AnotherTable.employees e LEFT JOIN
AnotherTable.employees m
on m.EmployeeID = e.ManagerID
But I am getting stuck updating MyTable, as I usually receive errors such as "single row query returns more than one row" or "SQL command not properly ended". I've read that Oracle doesnt support joins for updating tables. How can I overcome this issue? A sample data would be:
MyTable
------------------------------
EmployeeID | SomeOtherColumns| ..
1 | SomeData |
2 | SomeData |
3 | SomeData |
4 | SomeData |
5 | SomeData |
------------------------------
OtherTable
-------------------------------------
EmployeeID | Name | ManagerID |
1 | Steve | - |
2 | John | 1 |
3 | Peter | 1 |
4 | Bob | 2 |
5 | Patrick | 3 |
6 | Connor | 1 |
-------------------------------------
And the result would be then:
MyTable
-------------------------------------------
EmployeeID | SomeOtherColumns |ManagerName|
1 | SomeData | - |
2 | SomeData | Steve |
3 | SomeData | Steve |
4 | SomeData | John |
5 | SomeData | Peter |
6 | SomeData | Steve |
-------------------------------------------
As one of the options I tried to use is:
update MyTable
set MyTable.ManagerName = (
SELECT
(m.name) ManagerName
FROM
OtherTable.employees e
LEFT JOIN OtherTable.employees m ON
m.EmployeeID = e.ManagerID
)
But there I get "single row query returns more than one row" error. How is it possible to solve this?
You can use a hierarchical query:
UPDATE mytable m
SET managername = (SELECT name
FROM othertable
WHERE LEVEL = 2
START WITH employeeid = m.employeeid
CONNECT BY PRIOR managerid = employeeid);
or a self-join:
UPDATE mytable m
SET managername = (SELECT om.name
FROM othertable o
INNER JOIN othertable om
ON (o.managerid = om.employeeid)
WHERE o.employeeid = m.employeeid);
Which, for the sample data:
CREATE TABLE MyTable (EmployeeID, SomeOtherColumns, ManagerName) AS
SELECT LEVEL, 'SomeData', CAST(NULL AS VARCHAR2(20))
FROM DUAL
CONNECT BY LEVEL <= 5;
CREATE TABLE OtherTable(EmployeeID, Name, ManagerID) AS
SELECT 1, 'Alice', NULL FROM DUAL UNION ALL
SELECT 2, 'Beryl', 1 FROM DUAL UNION ALL
SELECT 3, 'Carol', 1 FROM DUAL UNION ALL
SELECT 4, 'Debra', 2 FROM DUAL UNION ALL
SELECT 5, 'Emily', 3 FROM DUAL UNION ALL
SELECT 6, 'Fiona', 1 FROM DUAL;
Then after either update, MyTable contains:
EMPLOYEEID
SOMEOTHERCOLUMNS
MANAGERNAME
1
SomeData
null
2
SomeData
Alice
3
SomeData
Alice
4
SomeData
Beryl
5
SomeData
Carol
Note: Keeping this data violates third-normal form; instead, you should keep the employee name in the table with the other employee data and then when you want to display the manager's name use SELECT ... FROM ... LEFT OUTER JOIN with a hierarchical query to include the result. What you do not want to do is duplicate the data as then it has the potential to become out-of-sync when something changes.
db<>fiddle here

Multiple select from CTE with different number of rows in a StoredProcedure

How to do two select with joins from the cte's which returns total number of columns in the two selects?
I tried doing union but that appends to the same list and there is no way to differentiate for further use.
WITH campus AS
(SELECT DISTINCT CampusName, DistrictName
FROM dbo.file
),creditAcceptance AS
(SELECT CampusName, EligibilityStatusFinal, CollegeCreditAcceptedFinal, COUNT(id) AS N
FROM dbo.file
WHERE (EligibilityStatusFinal LIKE 'Eligible%') AND (CollegeCreditEarnedFinal = 'Yes') AND (CollegeCreditAcceptedFinal = 'Yes')
GROUP BY CampusName, EligibilityStatusFinal, CollegeCreditAcceptedFinal
),eligibility AS
(SELECT CampusName, EligibilityStatusFinal, COUNT(id) AS N, CollegeCreditAcceptedFinal
FROM dbo.file
WHERE (EligibilityStatusFinal LIKE 'Eligible%')
GROUP BY CampusName, EligibilityStatusFinal, CollegeCreditAcceptedFinal
)
SELECT a.CampusName, c.[EligibilityStatusFinal], SUM(c.N) AS creditacceptCount
FROM campus as a FULL OUTER JOIN creditAcceptance as c ON a.CampusName=c.CampusName
WHERE (a.DistrictName = 'xy')
group by a.CampusName ,c.EligibilityStatusFinal
Union ALL
SELECT a.CampusName , b.[EligibilityStatusFinal], SUM(b.N) AS eligible
From Campus as a FULL OUTER JOIN eligibility as b ON a.CampusName = b.CampusName
WHERE (a.DistrictName = 'xy')
group by a.CampusName,b.EligibilityStatusFinal
Expected output:
+------------+------------------------+--------------------+
| CampusName | EligibilityStatusFinal | creditacceptCount |
+------------+------------------------+--------------------+
| M | G | 1 |
| E | NULL | NULL |
| A | G | 4 |
| B | G | 8 |
+------------+------------------------+--------------------+
+------------+------------------------+----------+
| CampusName | EligibilityStatusFinal | eligible |
+------------+------------------------+----------+
| A | G | 8 |
| C | G | 9 |
| A | T | 9 |
+------------+------------------------+----------+
As you can see here CTEs can be used in a single statement only, so you can't get the expected output with CTEs.
Here is an excerpt from Microsoft docs:
A CTE must be followed by a single SELECT, INSERT, UPDATE, or DELETE
statement that references some or all the CTE columns. A CTE can also
be specified in a CREATE VIEW statement as part of the defining SELECT
statement of the view.
You can use table variables (declare #campus table(...)) or temp tables (create table #campus (...)) instead.

Count how many times a value appears in tables SQL

Here's the situation:
So, in my database, a person is "responsible" for job X and "linked" to job Y. What I want is a query that returns: name of person, his ID and he number of jobs it's linked/responsible. So far I got this:
select id_job, count(id_job) number_jobs
from
(
select responsible.id
from responsible
union all
select linked.id
from linked
GROUP BY id
) id_job
GROUP BY id_job
And it returns a table with id in the first column and number of occurrences in the second. Now, what I can't do is associate the name of person to the table. When i put that in the "select" from beginning it gives me all the possible combinations... How can I solve this? Thanks in advance!
Example data and desirable output:
| Person |
id | name
1 | John
2 | Francis
3 | Chuck
4 | Anthony
| Responsible |
process_no | id
100 | 2
200 | 2
300 | 1
400 | 4
| Linked |
process_no | id
101 | 4
201 | 1
301 | 1
401 | 2
OUTPUT:
| OUTPUT |
id | name | number_jobs
1 | John | 3
2 | Francis | 3
3 | Chuck | 0
4 | Anthony | 2
Try this way
select prs.id, prs.name, count(*) from Person prs
join(select process_no, id
from Responsible res
Union all
select process_no, id
from Linked lin ) a on a.id=prs.id
group by prs.id, prs.name
I would recommend aggregating each of the tables by the person and then joining the results back to the person table:
select p.*, coalesce(r.cnt, 0) + coalesce(l.cnt, 0) as numjobs
from person p left join
(select id, count(*) as cnt
from responsible
group by id
) r
on r.id = p.id left join
(select id, count(*) as cnt
from linked
group by id
) l
on l.id = p.id;
select id, name, count(process_no) FROM (
select pr.id, pr.name, res.process_no from Person pr
LEFT JOIN Responsible res on pr.id = res.id
UNION
select pr.id, pr.name, lin.process_no from Person pr
LEFT JOIN Linked lin on pr.id = lin.id) src
group by id, name
order by id
Query ain't tested, give it a shot, but this is the way you want to go

SQL Query- N Level hierarchy - Return Parent and Last Child Only

I have a multilevel hierarchy data.
I want to combine everything into two level using sql query.
I want to return First parent and and Last Child of the Data
PID CId Name
Null 1 Electronics
1 2 Laptop
2 3 Toshiba
1 4 Mobile
4 5 Samsung
I need the result like this
PID CId Name
Null 1 Electronics
1 3 Toshiba
1 5 Samsung
Using this tutorial I create my first recursive CTE just for you.
The trick here is mark the row with null as the BigBoss and pass that value on the recursive secttion.
The last where have two filter
PID IS NULL to show the father item.
OR NOT EXISTS to show the items without childrens
SQL Fiddle Demo
Fiddle show your desire output, but if you unncoment the code you could see the full result to better understand.
WITH DirectReports (PID, CId, Name, Level, BigBoss)
AS
(
-- Anchor member definition
SELECT e.PID, e.CId, e.Name, 0 AS Level, e.CId BigBoss
FROM dbo.Table1 AS e
WHERE e.PID IS NULL
UNION ALL
-- Recursive member definition
SELECT e.PID, e.CId, e.Name, Level + 1, d.BigBoss
FROM dbo.Table1 AS e
INNER JOIN DirectReports AS d
ON e.PID = d.CId
)
-- Statement that executes the CTE
-- SELECT d.PID, d.CId, d.Name, d.Level, d.BigBoss
-- FROM DirectReports d
SELECT CASE
WHEN PID IS NULL THEN NULL
ELSE BigBoss
END PID,
Cid, Name
FROM DirectReports
WHERE
PID IS NULL
OR NOT EXISTS ( SELECT *
FROM Table1 t
WHERE d.CId = t.PID)
Result:
| PID | Cid | Name |
|--------|-----|-------------|
| (null) | 1 | Electronics |
| 1 | 5 | Samsung |
| 1 | 3 | Toshiba |

Return name of the employee who having top salary with join

here is the situation. I have two tables, one is table Tbl_employ, second is tbl_details.
Tbl_employ
----------
id | name
1 | Ravi
2 | ram
3 | sham
4 | john
Tbl_details
-----------
id | salary | emp_id
1 | 500 | 1
2 | 200 | 2
3 | 400 | 3
4 | 501 | 4
I want to return the name of the employee who has top salary in tbl_detail.
What will be the join query for this condition?
Please suggest. Thanks in advance.
Perhaps:
SELECT TOP(1) name
FROM Tbl_employ e INNER JOIN Tbl_details d ON e.id = d.emp_id
ORDER BY d.salary DESC;
Essentially, this joins the two tables on the key fields (id and emp_id), returning only a single result (TOP(1)) that is the maximum salary row (ORDER BY d.salary DESC).
I appreciate the answer of #Max Vernon.
You can also do it by another way. Please try this
select t.name from (
select Distinct top 1 salary ,name
from Tbl_employ as E
left outer join Tbl_details as D on D.empid=E.id
order by salary desc
) as t
you can check it here SQL Fiddle