How to count one to many - sql

i have two tables one Job Category with the structure | id | name | and the other one jobs with the structure | id | job_name | job_category |
How to count how many jobs are in each category?

select c.name, count(j.id)
from job_category c
left join jobs j on j.job_category = c.name
group by c.name

You can do it with a left join (see other answers) or with a subquery:
SELECT
c.Name
, (SELECT COUNT(*) FROM jobs j WHERE j.category_id=c.id) AS Count
FROM job_category c

Why do you need join at all? Is it not
select job_category, count(*)
from jobs
group by job_category
If not, do post your own query (and, possibly, some sample data which might help us help you).
[EDIT, after reading comments]
It appears that my (over)simplified "solution" lacks in some details. True, it shows categories with no jobs, while the OP asked that those should also be displayed having "0" as a result.
Outer join with appropriate COUNT function would fix that; here's an example.
SQL> with job_category(id, name) as
2 (select 1, 'categ 1' from dual union
3 select 2, 'categ 2' from dual
4 ),
5 job (id, job_name, job_category) as
6 (select 100, 'job 1', 1 from dual union
7 select 200, 'job 2', 1 from dual
8 )
9 select c.name, count(j.id)
10 from job_category c left join job j on j.job_category = c.id
11 group by c.name
12 order by c.name;
NAME COUNT(J.ID)
------- -----------
categ 1 2
categ 2 0
SQL>

Related

How to compare IDs with comma separated string in Oracle SQL?

I have two tables, for instance "Employees" and "Projects". I need a list of all projects with the name of all the employes involved.
Problem now is, that the employe_ids are saved with commas, like in the example below:
employe | ID project | employe_id
-------------- -----------------------
Person A | 1 Project X | ,2,
Person B | 2 Project Y |
Person C | 3 Project Z | ,1,3,
select
p.project, e.employe
from
projects p
left join employees e on e.id = p.employe_id ???
How do I have to write the join to get the desired output:
project | employe
--------------------
Project X | Person B,
Project Y |
Project Z | Person A, Person C
You can try group by and listagg as following:
select
p.project,
listagg(e.employe,',') within group (order by e.id) as employee
from
projects p
left join employees e on p.employe_id like '%,' || e.id || ',%'
Group by p.project
Cheers!!
Here's a kind of fun way to do it:
WITH cteId_counts_by_project AS (SELECT PROJECT,
NVL(REGEXP_COUNT(EMPLOYE_ID, '[^,]'), 0) AS ID_COUNT
FROM PROJECTS),
cteMax_id_count AS (SELECT MAX(ID_COUNT) AS MAX_ID_COUNT
FROM cteId_counts_by_project),
cteProject_employee_ids AS (SELECT PROJECT,
EMPLOYE_ID,
REGEXP_SUBSTR(EMPLOYE_ID, '[^,]',1, LEVEL) AS ID
FROM PROJECTS
CROSS JOIN cteMax_id_count m
CONNECT BY LEVEL <= m.MAX_ID_COUNT),
cteProject_emps AS (SELECT DISTINCT PROJECT, ID
FROM cteProject_employee_ids
WHERE ID IS NOT NULL),
cteProject_empnames AS (SELECT pe.PROJECT, pe.ID, e.EMPLOYE
FROM cteProject_emps pe
LEFT OUTER JOIN EMPLOYEES e
ON e.ID = pe.ID
ORDER BY pe.PROJECT, e.EMPLOYE)
SELECT p.PROJECT,
LISTAGG(pe.EMPLOYE, ',') WITHIN GROUP (ORDER BY pe.EMPLOYE) AS EMPLOYEE_LIST
FROM PROJECTS p
LEFT OUTER JOIN cteProject_empnames pe
ON pe.PROJECT = p.PROJECT
GROUP BY p.PROJECT
ORDER BY p.PROJECT
You can certainly compress some of the CTE's together to save space, but I kept them separate so you can see how each little bit adds to the solution.
dbfiddle here
Yet another option; code you need (as you already have those tables) begins at line #12:
SQL> -- Your sample data
SQL> with employees (employee, id) as
2 (select 'Person A', 1 from dual union all
3 select 'Person B', 2 from dual union all
4 select 'Person C', 3 from dual
5 ),
6 projects (project, employee_id) as
7 (select 'Project X', ',2,' from dual union all
8 select 'Project Y', null from dual union all
9 select 'Project Z', ',1,3,' from dual
10 ),
11 -- Employees per project
12 emperpro as
13 (select project, regexp_substr(employee_id, '[^,]+', 1, column_value) id
14 from projects cross join table(cast(multiset(select level from dual
15 connect by level <= regexp_count(employee_id, ',') + 1
16 ) as sys.odcinumberlist))
17 )
18 -- Final result
19 select p.project, listagg(e.employee, ', ') within group (order by null) employee
20 from emperpro p left join employees e on e.id = p.id
21 group by p.project
22 /
PROJECT EMPLOYEE
--------- ----------------------------------------
Project X Person B
Project Y
Project Z Person A, Person C
SQL>
You can extract the numeric IDs from the employee_id column of the projects table by using regexp_substr() and rtrim() ( trimming the last extra comma ) functions together, and then concatenate by listagg() function :
with p2 as
(
select distinct p.*, regexp_substr(rtrim(p.employee_id,','),'[^,]',1,level) as p_eid,
level as rn
from projects p
connect by level <= regexp_count(rtrim(p.employee_id,','),',')
)
select p2.project, listagg(e.employee,', ') within group (order by p2.rn) as employee
from p2
left join employees e on e.id = p2.p_eid
group by p2.project
Demo

Count for each record within table

I have a table (stu_grades) that stores student data and their grades at the centers they attended
I want to find out how many times for e.g. each student in that table got 'A' and then 'B' etc at any center
stu_grades
stu_ID|grade1|grade2|Grade3|center
1 A A C 1
2 B B B 2
3 C C A 1
1 C A C 2
the same student could occur more than once in the table with the same grades or even a different grade, same or different center
I especially want to check where the grade has appeared more than 3 or more times and how many centeres they exist in
So the final output should be like:
Stu_ID|Grade|Count|centercount
1 A 3 2 (As they accquired 'A' from 2 centres)
1 C 3 2
2 B 3 1 (As they only exist in 1 centre)
3 C 2 1
3 A 1 1
This is one approach using union all to unpivot the different grades into one column and then doing an aggregation.
select stu_id,grade,count(*) cnt
from (
select stu_id,grade_1 grade from stu_grades
union all
select stu_id,grade_2 grade from stu_grades
union all
select stu_id,grade_3 grade from stu_grades
) t
group by stu_id,grade
This should satisfy your requirement:
SELECT [Stud_ID], Grade, count(*) as GradeCount FROM
(SELECT [Stud_ID],Grade1 as Grade from [Stud_Details]
UNION ALL
SELECT [Stud_ID],Grade2 as Grade from [Stud_Details]
UNION ALL
SELECT [Stud_ID],Grade3 as Grade from [Stud_Details]) AS T
GROUP BY T.[Stud_ID],T.Grade
ORDER BY T.[Stud_ID]
UPDATE:
You can use HAVING after GROUP BY to get gradeCount which are greater than 3.
SELECT [Stud_ID], Grade, count(*) as GrdCountStud FROM
(SELECT [Stud_ID],Grade1 as Grade from [Stud_Details]
UNION ALL
SELECT [Stud_ID],Grade2 as Grade from [Stud_Details]
UNION ALL
SELECT [Stud_ID],Grade3 as Grade from [Stud_Details]) AS T
GROUP BY T.[Stud_ID],T.Grade
HAVING COUNT(*) > 3
ORDER BY T.[Stud_ID]

Oracle SQL Return at least one row

I have an Oracle SQL query where I would like to return all rows where a certain column has a value, but if none of the rows have a value for that column then I would like to return one of the blank rows. How can this be accomplished?
My scenario is a learning path with courses that are to completed, but I want to show the percentage completion. Obviously if there are no courses completed I would like to show the learning path with a zero percentage completion.
The query is very complicated so I rather provide a dummy scenario below:
learning_paths
--------------
learning_path_id learning_path_name
1 Oracle Developer
2 Python Developer
courses
-------
course_id course_name
--------- -----------
1 Oracle SQL
2 Oracle PL/SQL
3 Python
4 Django
5 NLTK
learning_path_items
-------------------
learning_path_id course_id
---------------- ---------
1 1
1 2
2 3
2 4
2 5
learning_path_enrollments
-------------------------
employee_id learning_path_id
----------- ----------------
1 1
2 2
3 2
course_enrollments
------------------
employee_id course_id
----------- ---------
2 3
2 4
So my results should be:
employee_id learning_path_id course_id completion
----------- ---------------- --------- ----------
1 1 0 out of 2
2 2 3 2 out of 3
2 2 4 2 out of 3
3 2 0 out of 3
Below is the actual query, which is very complicated and still a work in progress.
select sub2.director,
sub2.cost_centre,
sub1.employee_number,
sub1.employee_name,
sub2.job_title,
sub1.course_name,
sub1.learning_path_name,
sub2.course_start_date,
sub2.course_end_date,
max(sub2.course_end_date) over (partition by sub1.employee_number, sub1.learning_path_name) max_course_end_date,
sub1.original_date_of_hire,
sub2.job_start_date promotion_date,
nvl(sub1.completion_target_days, 9999999999999999) completion_target_days,
sub1.completion_target_date,
sub1.no_of_completed_courses,
sub1.no_of_mandatory_courses,
round(least(sub1.no_of_completed_courses, sub1.no_of_mandatory_courses) / sub1.no_of_mandatory_courses * 100) completion_percentage
--round() time_percentage
from (
select papf.employee_number,
nvl(papf.known_as, papf.first_name) || ' ' || papf.last_name employee_name,
papf.original_date_of_hire,
oav.version_name course_name,
olpt.name learning_path_name,
ole.completion_target_date completion_target_date,
ole.no_of_completed_courses no_of_completed_courses,
ole.no_of_mandatory_courses no_of_mandatory_courses,
olp.duration completion_target_days,
papf.person_id,
oav.activity_version_id
from ota_lp_enrollments ole,
per_all_people_f papf,
ota_learning_paths olp,
ota_learning_paths_tl olpt,
ota_lp_sections ols,
ota_lp_sections_tl olst,
ota_learning_path_members olpm,
ota_activity_versions oav -- aka Courses
where papf.person_id = ole.person_id
and xxpay_bi_util.get_effective_date(ole.person_id, trunc(sysdate), 'all') between papf.effective_start_date
and papf.effective_end_date
and olp.learning_path_id = ole.learning_path_id
and olpt.learning_path_id = ole.learning_path_id
and olpt.language = userenv('LANG')
and ols.learning_path_id = ole.learning_path_id
and olst.learning_path_section_id = ols.learning_path_section_id
and olst.language = userenv('LANG')
and ols.learning_path_id = ole.learning_path_id
and olpm.learning_path_id = ole.learning_path_id
and olpm.learning_path_section_id = ols.learning_path_section_id
and oav.activity_version_id = olpm.activity_version_id
group by papf.employee_number,
nvl(papf.known_as, papf.first_name) || ' ' || papf.last_name,
papf.original_date_of_hire,
oav.version_name,
olpt.name,
olst.name,
ole.completion_target_date,
ole.no_of_completed_courses,
ole.no_of_mandatory_courses,
olp.duration,
papf.person_id,
oav.activity_version_id
) sub1,
(
select erm.*,
paaf.assignment_id,
paaf.person_id,
pj.name job_title,
haou.name cost_centre,
paafm.job_start_date,
xxpay_util.get_lookup_value(trunc(sysdate), 'TRUW_HR_DIRECTORS', hoi.org_information1) director -- 'Derek Kohler (Stores)'
from (
select odb.booking_id,
odb.delegate_person_id,
odb.date_booking_placed,
oe.course_start_date,
oe.course_end_date,
oe.course_start_time,
oe.course_end_time,
oe.enrolment_start_date,
oe.public_event_flag,
oe.title class_title,
oe.activity_version_id
from ota_delegate_bookings odb,
ota_events oe -- aka Classes
where oe.event_id = odb.event_id
) erm,
per_all_assignments_f paaf,
per_jobs pj,
hr_all_organization_units haou,
hr_organization_information hoi,
(
select paaf.person_id,
paaf.assignment_id,
paaf.job_id,
min(paaf.effective_start_date) job_start_date
from per_all_assignments_f paaf
where paaf.assignment_type in ('E', 'C')
and paaf.primary_flag = 'Y'
group by paaf.person_id,
paaf.assignment_id,
paaf.job_id
) paafm
where paaf.person_id (+) = erm.delegate_person_id
and erm.course_start_date between nvl(paaf.effective_start_date (+), to_date('01/01/1000', 'dd/mm/yyyy'))
and nvl(paaf.effective_end_date (+), to_date('31/12/4712', 'dd/mm/yyyy'))
and paafm.person_id (+) = paaf.person_id
and paafm.assignment_id (+) = paaf.assignment_id
and paafm.job_id (+) = paaf.job_id
and pj.job_id (+) = paaf.job_id
and haou.organization_id (+) = paaf.organization_id
and hoi.organization_id (+) = paaf.organization_id
and hoi.org_information_context (+) = 'TRU_ADD_ORG'
) sub2
where sub2.activity_version_id (+) = sub1.activity_version_id
and sub2.person_id (+) = sub1.person_id
and sub1.employee_number in ('2006591', '2005681', '2004118', '2004212')
order by 3, 1, 2, 4, 5, 7
If you have tables like this:
CREATE TABLE students (
student_id NUMBER PRIMARY KEY
);
CREATE TABLE courses (
course_id NUMBER PRIMARY KEY
);
CREATE TABLE student_courses (
student_id NUMBER REFERENCES Students( student_id ),
course_id NUMBER REFERENCES Courses( course_id ),
completed NUMBER(1,0),
score NUMBER(3,0),
CONSTRAINT student_courses_pk PRIMARY KEY ( student_id, course_id )
);
Then you can do:
SELECT s.student_id,
COUNT( CASE c.completed WHEN 1 THEN 1 END ) / COUNT( c.course_id ) * 100 AS percent_enrolled_completed
FROM student s
LEFT OUTER JOIN
student_courses c
ON ( s.student_id = c.student_id )
GROUP BY s.student_id;
Edited: to answer updated question
SQL Fiddle
Oracle 11g R2 Schema Setup:
create table learning_path_items AS
SELECT 1 AS learning_path_id, 1 AS course_id FROM DUAL
UNION ALL SELECT 1, 2 FROM DUAL
UNION ALL SELECT 2, 3 FROM DUAL
UNION ALL SELECT 2, 4 FROM DUAL
UNION ALL SELECT 2, 5 FROM DUAL;
create table learning_path_enrollments AS
SELECT 1 AS employee_id, 1 AS learning_path_id FROM DUAL
UNION ALL SELECT 2, 2 FROM DUAL
UNION ALL SELECT 3, 2 FROM DUAL;
create table course_enrollments AS
SELECT 2 AS employee_id, 3 AS course_id FROM DUAL
UNION ALL SELECT 2, 4 FROM DUAL;
Query 1:
WITH num_completed AS (
SELECT e.learning_path_id,
e.employee_id,
c.course_id,
COUNT( c.course_id ) OVER ( PARTITION BY e.learning_path_id, e.employee_id ) AS num_completed
FROM learning_path_items i
INNER JOIN
learning_path_enrollments e
ON (e.learning_path_id = i.learning_path_id)
INNER JOIN
course_enrollments c
ON (i.course_id = c.course_id
AND e.employee_id = c.employee_id)
),
num_courses AS (
SELECT e.learning_path_id,
e.employee_id,
COUNT( 1 ) AS num_courses
FROM learning_path_items i
INNER JOIN
learning_path_enrollments e
ON (e.learning_path_id = i.learning_path_id)
GROUP BY
e.learning_path_id,
e.employee_id
)
SELECT c.learning_path_id,
c.employee_id,
x.course_id,
COALESCE( x.num_completed, 0 ) || ' out of ' || c.num_courses AS completion
FROM num_courses c
LEFT OUTER JOIN
num_completed x
ON ( c.employee_id = x.employee_id
AND c.learning_path_id = x.learning_path_id )
ORDER BY 1, 2, 3
Results:
| LEARNING_PATH_ID | EMPLOYEE_ID | COURSE_ID | COMPLETION |
|------------------|-------------|-----------|------------|
| 1 | 1 | (null) | 0 out of 2 |
| 2 | 2 | 3 | 2 out of 3 |
| 2 | 2 | 4 | 2 out of 3 |
| 2 | 3 | (null) | 0 out of 3 |
SELECT whatever FROM wherever UNION ALL SELECT 0 FROM DUAL LIMIT 1;
If you get a result from your first query you would get the 1 row with the percentage you are trying to get
second select returns a row that has value 0 but is stored in a VARCHAR2(1) from the dummy table DUAL so you will either get the result from your first query (assuming you only get 1 result because otherwise it will only show the first row it returns)
if the first select returns no rows, the second select will return a single column single row result with a varchar2(1) '0'
ALTERNATIVELY
SELECT
something
FROM sometable
WHERE somefield = somevalue
UNION ALL
SELECT
0
WHERE NOT EXISTS (SELECT something FROM sometable WHERE somefield = somevalue)
This will do the same except only when your first select query returns no rows, I guess this would be the better answer for your question as the first query might be returning multiple rows

Getting record with the only document by priority issue

I have two tables:
contacts:
id, name
1 Alex
2 John
documents:
id, contactID, type
1 1 1
2 1 2
...
30 1 3
31 2 1
32 2 3
I want to get contact name, and document type. the only record for each contact. Contact may have several document types (the only document per type), and I have the following priority for document types: 2, 3, 1.
For example, Alex has documents of all types, but I should get only:
Alex, 2
John has document types = 1, 3, as a result I should get:
John, 3
Oracle database. But if you'll be able to give sql standard solution, it would be great
Oracle 9i+, use:
WITH example AS (
SELECT c.name,
d.type,
ROW_NUMBER() OVER (PARTITION BY c.id
ORDER BY CASE d.type
WHEN 2 THEN 1
WHEN 3 THEN 2
WHEN 1 THEN 3
ELSE 4
END) AS rnk
FROM CONTACTS c
JOIN DOCUMENTS d ON d.contactid = c.id)
SELECT e.name, e.type
FROM example e
WHERE e.rnk = 1
...or the non-Subquery Factoring (AKA CTE) version (still 9i+):
SELECT e.name, e.type
FROM (SELECT c.name,
d.type,
ROW_NUMBER() OVER (PARTITION BY c.id
ORDER BY CASE d.type
WHEN 2 THEN 1
WHEN 3 THEN 2
WHEN 1 THEN 3
ELSE 4
END) AS rnk
FROM CONTACTS c
JOIN DOCUMENTS d ON d.contactid = c.id) e
WHERE e.rnk = 1
Use an inline view where you map types to priorities, then group by contact, then map priorities back to types.
SELECT C.name
DECODE( V.priority, 'A', 2, 'B', 3, 'C', 1 ) AS type
FROM contacts C
, ( SELECT D.contactid
, MIN( DECODE( D.type, 2, 'A', 3, 'B', 1, 'C' ) ) AS priority
FROM documents D
GROUP BY D.contact_id
) V
WHERE V.contactid = C.id

SQL query to group based on sum

I have a simple table with values that I want to chunk/partition into distinct groups based on the sum of those values (up to a certain limit group sum total).
e.g.,. imagine a table like the following:
Key Value
-----------
A 1
B 4
C 2
D 2
E 5
F 1
And I would like to group into sets such that no one grouping's sum will exceed some given value (say, 5).
The result would be something like:
Group Key Value
-------------------
1 A 1
B 4
--------
Total: 5
2 C 2
D 2
--------
Total: 4
3 E 5
--------
Total: 5
4 F 1
--------
Total: 1
Is such a query possible?
While I am inclined to agree with the comments that this is best done outside of SQL, here is some SQL which would seem to do roughly what you're asking:
with mytable AS (
select 'A' AS [Key], 1 AS [Value] UNION ALL
select 'B', 4 UNION ALL
select 'C', 2 UNION ALL
select 'D', 2 UNION ALL
select 'E', 5 UNION ALL
select 'F', 1
)
, Sums AS (
select T1.[Key] AS T1K
, T2.[Key] AS T2K
, (SELECT SUM([Value])
FROM mytable T3
WHERE T3.[Key] <= T2.[Key]
AND T3.[Key] >= T1.[Key]) AS TheSum
from mytable T1
inner join mytable T2
on T2.[Key] >= T1.[Key]
)
select S1.T1K AS StartKey
, S1.T2K AS EndKey
, S1.TheSum
from Sums S1
left join Sums S2
on (S1.T1K >= S2.T1K and S1.T2K <= S2.T2K)
and S2.TheSum > S1.TheSum
and S2.TheSum <= 5
where S1.TheSum <= 5
AND S2.T1K IS NULL
When I ran this code on SQL Server 2008 I got the following results:
StartKey EndKey Sum
A B 5
C D 4
E E 5
F F 1
It should be straightforward to construct the required groups from these results.
If you want to have only two members or less in each set, you can use the following query:
Select
A.[Key] as K1 ,
B.[Key] as K2 ,
isnull(A.value,0) as V1 ,
isnull(B.value,0) as V2 ,
(A.value+B.value)as Total
from Table_1 as A left join Table_1 as B
on A.value+B.value<=5 and A.[Key]<>B.[Key]
For finding sets having more members, you can continue to use joins.