Update MyTable with values from AnotherTable (with self join) - sql

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

Related

Oracle - Finding missing /non-joined records

I have an issue in Oracle 12 that is easiest explained with the traditional database design scenario of students, classes, and students taking classes called registrations. I understand this model well. I have a scenario where I need to get a COMPLETE list, of all students against ALL classes, and whether or not they are taking that class or not...
Lets use this table design here...
CREATE TABLE CLASSES
(CLASSID VARCHAR2(10) PRIMARY KEY,
CLASSNAME VARCHAR2(25),
INSTRUCTOR VARCHAR2(25) );
CREATE TABLE STUDENTS
(STUDENTID VARCHAR2(10) PRIMARY KEY,
STUDENTNAMENAME VARCHAR2(25)
STUDY_MAJOR VARCHAR2(25) );
CREATE TABLE REGISTRATION
(
CLASSID VARCHAR2(10 BYTE),
STUDENTID VARCHAR2(10 BYTE),
GRADE NUMBER(4,0),
CONSTRAINT "PK1" PRIMARY KEY ("CLASSID", "STUDENTID"),
CONSTRAINT "FK1" FOREIGN KEY ("CLASSID") REFERENCES "CLASSES" ("CLASSID") ENABLE,
CONSTRAINT "FK2" FOREIGN KEY ("STUDENTID") REFERENCES "EGR_MM"."STUDENTS" ("STUDENTID") ENABLE
) ;
So assume the following... 300 students, and 15 different classes... and the REGISTRATION table will show how many students taking how many classes... What I need is that info PLUS all the NON-TAKEN combinations... i.e. I need a report (SQL statement) that shows ALL possible combinations... i.e. 300 x 15, and then whether that row exists in the registration table...so for example, the output should look like this...
STUDENTID Class1_GRADE Class2_Grade Class3_Grade` Class4_Grade
101 A B Not Taking A
102 C Not Taking Not Taking Not Taking
****** THIS STUDENT NOT TAKING ANY CLASSES So NOT in the Registrations Table
103 Not Taking Not Taking Not Taking Not Taking
This would work as well, and I can probably do a PIVOT to get the above listing.
STUDENTID CLASSID GRADE
101 Class1 A
101 Class2 B
101 Class3 Not Taking
101 Class4 A
...
102 Class1 C
102 Class2 Not Taking
102 Class3 Not Taking
102 Class4 Not Taking
...
103 Class1 Not Taking // THIS STUDENT NOT TAKING ANY CLASSES
103 Class2 Not Taking
103 Class3 Not Taking
103 Class4 Not Taking
How do I fill in the missing data, i.e. the combination of students and classes NOT taken...?
CROSS JOIN the students and classes and then LEFT OUTER JOIN the registrations and then use COALESCE to get the Not taken value:
SELECT s.studentid,
c.classid,
COALESCE( TO_CHAR( r.grade ), 'Not taken' ) AS grade
FROM students s
CROSS JOIN classes c
LEFT OUTER JOIN registration r
ON ( s.studentid = r.studentid AND c.classid = r.classid )
Which, if you have the data:
INSERT INTO Classes
SELECT LEVEL,
'Class' || LEVEL,
'Instructor' || LEVEL
FROM DUAL
CONNECT BY LEVEL <= 3;
INSERT INTO Students
SELECT TO_CHAR( LEVEL, 'FM000' ),
'Student' || LEVEL,
'Major'
FROM DUAL
CONNECT BY LEVEL <= 5;
INSERT INTO Registration
SELECT 1, '001', 4 FROM DUAL UNION ALL
SELECT 1, '002', 2 FROM DUAL UNION ALL
SELECT 1, '003', 5 FROM DUAL UNION ALL
SELECT 2, '001', 3 FROM DUAL UNION ALL
SELECT 3, '001', 1 FROM DUAL;
Then it outputs:
STUDENTID | CLASSID | GRADE
:-------- | :------ | :--------
001 | 1 | 4
002 | 1 | 2
003 | 1 | 5
001 | 2 | 3
001 | 3 | 1
005 | 1 | Not taken
004 | 2 | Not taken
003 | 3 | Not taken
005 | 3 | Not taken
005 | 2 | Not taken
002 | 2 | Not taken
003 | 2 | Not taken
004 | 1 | Not taken
002 | 3 | Not taken
004 | 3 | Not taken
If you want to pivot it then:
SELECT *
FROM (
SELECT s.studentid,
c.classid,
COALESCE( TO_CHAR( r.grade ), 'Not taken' ) AS grade
FROM students s
CROSS JOIN classes c
LEFT OUTER JOIN registration r
ON ( s.studentid = r.studentid AND c.classid = r.classid )
)
PIVOT ( MAX( grade ) FOR classid IN (
1 AS Class1,
2 AS Class2,
3 AS Class3
) )
ORDER BY StudentID
Which outputs:
STUDENTID | CLASS1 | CLASS2 | CLASS3
:-------- | :-------- | :-------- | :--------
001 | 4 | 3 | 1
002 | 2 | Not taken | Not taken
003 | 5 | Not taken | Not taken
004 | Not taken | Not taken | Not taken
005 | Not taken | Not taken | Not taken
db<>fiddle here
This is just conditional aggregation:
select s.studentid,
max(case when r.classid = 1 then r.grade end) as class1_grade,
max(case when r.classid = 2 then r.grade end) as class2_grade,
. . .
from students s left join
registrations r
on r.studentid = s.studentid;
You do have to list the columns explicitly. To avoid that, you need dynamic SQL (execute immediate).
Getting the results with one grade per row is simpler. Use a cross join to generate the rows and a left join to bring in the values:
select s.studentid, c.classid, r.grade
from students s cross join
classes c left join
registrations r
on r.studentid = s.studentid and r.classid = c.classid;

How to get average with recursive query

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;

Combine two rows in a table to one with SQL

I'm trying to write an SQL query (in Oracle DB) which performs the following:
Table:
Id | Name | Father_id
1 | John | 2
2 | Peter |
3 | Ann | 2
Expected result:
Name | Father_Name
John Peter
Ann Peter
I would like to list all the people who have a father in one row with the father's name. The user can have (of course) max. one father, but has not neccessarily one.
Which would be the best way to write such a query?
self join, if you want fatherless children, do a left outer join
select *
from table t_child join
table t_father on t_child.father_id = t_father.id
try this sql it will definatlly work
select child.MenuName as ChildName,parent.MenuName as ParentName from table_name as child
left join table_name as parent on child.parentId = parent.MenuId
you need to use left because you mention that not necessarily one
You do not need to use a self-join (and two table/index scans) and can instead do it with a hierarchical query:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE people ( Id, Name, Father_id ) AS
SELECT 1, 'John', 2 FROM DUAL UNION ALL
SELECT 2, 'Peter', NULL FROM DUAL UNION ALL
SELECT 3, 'Ann', 2 FROM DUAL
Query 1:
SELECT name,
PRIOR name AS father_name
FROM people
WHERE LEVEL > 1
OR CONNECT_BY_ISLEAF = 0 -- Comment out if you do not want Peter,NULL
CONNECT BY PRIOR id = father_id
Results:
| NAME | FATHER_NAME |
|-------|-------------|
| Peter | (null) |
| John | Peter |
| Ann | Peter |

Keep null relations on WHERE IN() or with SELECT and LEFT JOIN

I have table like there:
table:
| id | fkey | label | amount |
|----|------|-------|--------|
| 1 | 1 | aaa | 10 |
| 2 | 1 | bbb | 15 |
| 3 | 1 | fff | 99 |
| 4 | 1 | jjj | 33 |
| 5 | 2 | fff | 10 |
fkey is a foreign key to other table.
Now I need to query for all amounts asociated with some labels ('bbb', 'eee', 'fff') and with specifed fkey, but i need to keep all unexisting labels with NULL.
For simple query with WHERE IN ('bbb', 'eee', 'fff') I got, of course, only two rows:
SELECT label, amount FROM table WHERE label IN ('bbb', 'eee', 'fff') AND fkey = 1;
| label | amount |
|-------|--------|
| bbb | 15 |
| fff | 99 |
but excepted result should be:
| label | amount |
|-------|--------|
| bbb | 15 |
| eee | NULL |
| fff | 99 |
I tried also SELECT label UNION ALL label (...) LEFT JOIN which should work on MySQL (Keep all records in "WHERE IN()" clause, even if they are not found):
SELECT T.label, T.amount FROM (
SELECT 'bbb' AS "lbl"
UNION ALL 'eee' AS "lbl"
UNION ALL 'fff' AS "lbl"
) LABELS
LEFT OUTER JOIN table T ON (LABELS."lbl" = T."label")
WHERE T.fkey = 1;
and also with WITH statement:
WITH LABELS AS (
SELECT 'bbb' AS "lbl"
UNION ALL 'eee' AS "lbl"
UNION ALL 'fff' AS "lbl"
)
SELECT T.label, T.amount FROM LABELS
LEFT OUTER JOIN table T ON (LABELS."lbl" = T."label")
WHERE T.fkey = 1;
but always this LEFT JOIN give me 2 rows istead 3.
Create temporary table doesn't work at all (I got 0 rows from it and I cannot use this in join):
CREATE TEMPORARY TABLE __ids (
id VARCHAR(9) PRIMARY KEY
) ON COMMIT DELETE ROWS;
INSERT INTO __ids (id) VALUES
('bbb'),
('eee'),
('fff');
SELECT
*
FROM __ids
Any idea how to enforce Postgres to keep empty relation? Or even any other idea to get label 'eee' with NULL amount if there is not row for this in table?
List of labels can be different on every request.
This case online: http://rextester.com/CRQY46630
------ EDIT -----
I extended this question with filter where, because answer from a_horse_with_no_name is great, but not cover my whole case (I supposed this where no matter there)
Your approach with the outer join does work. You just need to take the label value from the "outer" joined table, not from the "table":
with labels (lbl) as (
values ('bbb'), ('eee'), ('fff')
)
select l.lbl, --<< this is different to your query
t.amount
from labels l
left outer join "table" t on l.lbl = t.label;
Online example: http://rextester.com/LESK82163
Edit after the scope of the question was extended.
If you want to filter on the base table, you need to move the into the JOIN condition, not the where clause:
with labels (lbl) as (
values ('bbb'), ('eee'), ('fff')
)
select l.lbl, --<< this is different to your query
t.amount
from labels l
left outer join "table" t
on l.lbl = t.label
and t.fkey = 1; --<<
Online example: http://rextester.com/XDO76971

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