Get column values separated by semi colon - sql

I have two tables in plsql
tblStudent:-
StudentId Name .........
1 A
2 B
tblDept:-
DeptId DeptName StudentId
1 Dep Aero 1
2 IT 1
3 Dep Maths 1
4 Dep Chemistry 2
I want to get studentId, with all its departments which starts with 'Dep' separated by semi colon, If i pass where StudentId = 1 in SELECT result should look like
StudentId DeptName
1 Dep Aero;Dep Maths
any help please?

You may use LISTAGG to concat and LIKE to filter the records.
SELECT studentid,
LISTAGG(deptname,';') WITHIN GROUP(
ORDER BY deptid
) as deptname
FROM t
WHERE deptname LIKE 'Dep%'
GROUP BY studentid;

If you want an empty list of departments when a student has no entries then you can use an outer join between the two tables, e.g.:
select s.studentid,
listagg(d.deptname, ';') within group (order by d.deptname) as deptnames
from tblstudent s
left join tbldept d on d.studentid = s.studentid
and deptname like 'Dep%'
group by s.studentid;
Demo using CTEs for your sample data, including a third student ID with no matching departments:
-- CTEs for sample data
with tblstudent (studentid, name) as (
select 1, 'A' from dual
union all select 2, 'B' from dual
union all select 3, 'C' from dual
),
tbldept (deptid, deptname, studentid) as (
select 1, 'Dep Aero', 1 from dual
union all select 2, 'IT', 1 from dual
union all select 3, 'Dep Maths', 1 from dual
union all select 4, 'Dep Chemistry', 2 from dual
)
-- actual query
select s.studentid,
listagg(d.deptname, ';') within group (order by d.deptname) as deptnames
from tblstudent s
left join tbldept d on d.studentid = s.studentid
and deptname like 'Dep%'
group by s.studentid;
STUDENTID DEPTNAMES
---------- ------------------------------
1 Dep Aero;Dep Maths
2 Dep Chemistry
3
Your data model looks odd though; you should probably have a department table which only has the department IDs and names, and then another tables that links each student to all of their departments - something like (in CTE form again):
-- CTEs for sample data
with tblstudent (studentid, name) as (
select 1, 'A' from dual
union all select 2, 'B' from dual
union all select 3, 'C' from dual
),
tbldept (deptid, deptname) as (
select 1, 'Dep Aero' from dual
union all select 2, 'IT' from dual
union all select 3, 'Dep Maths' from dual
union all select 4, 'Dep Chemistry' from dual
),
tblstudentdept (studentid, deptid) as (
select 1, 1 from dual
union all select 1, 2 from dual
union all select 1, 3 from dual
union all select 2, 4 from dual
)
-- actual query
select s.studentid,
listagg(d.deptname, ';') within group (order by d.deptname) as deptnames
from tblstudent s
left join tblstudentdept sd on sd.studentid = s.studentid
left join tbldept d on d.deptid = sd.deptid
and deptname like 'Dep%'
group by s.studentid;
STUDENTID DEPTNAMES
---------- ------------------------------
1 Dep Aero;Dep Maths
2 Dep Chemistry
3
Either way, if you only want to see a single student's results when add that as a where clause, right before the group by:
...
where s.studentid = 1
group by s.studentid;

Try this GROUP_CONCAT -
SELECT stud2.studentId,
CAST((SELECT GROUP_CONCAT(CONCAT(dep.depName,'; ') FROM tblDept dep
INNER JOIN tblStudent stud ON (stud.DeptId = dep.DeptId)))
FROM tblStudent stud2

No join is necessary for your query. If you want to do this for a particular student id:
select listagg(d.DeptName, ';') within group (order by d.DeptName)
from tblDept d
where d.studentid = :studentid and
d.DeptName like 'Dep%';

you can alo use this:
select
st.studentid,
listagg(d.DeptName,';') within group( order by d.DeptName )
From tblStudent st
join tblDept d on d.studentid = st.studentid
where DeptName like 'Dep%'
group by st.studentid
sqlFiddle

Related

How to substitute with Comma Separate values in Oracle SQL

I have a scenario like below (Oracle's SQL).
Table:Employee
S.No
Name
Role
1
a
ELE,PLU,OTH
2
b
MAN,DIR
3
c
DIR,FND
4
d
Table: Role_master
Role
Role name
ELE
Electrician
PLU
Plumber
MAN
Manager
DIR
Director
FND
Founder
OTH
Other
With the above tables, i would like to join both and expecting the output like below. Please help on the better way.
S.No
Name
Role
1
a
Electrician,Plumber,Other
2
b
Manager,Director
3
c
Director,Founder
4
d
Here's one option:
sample data in lines #1 - 12; query begins at line #14
split employee.role into rows (i.e. separate values) so that you could join them to role_master.role
aggregate them back (using listagg)
SQL> with
2 employee (sno, name, role) as
3 (select 1, 'a', 'ELE,PLU,OTH' from dual union all
4 select 2, 'b', 'MAN,DIR' from dual
5 ),
6 role_master (role, role_name) as
7 (select 'ELE', 'Electrician' from dual union all
8 select 'PLU', 'Plumber' from dual union all
9 select 'OTH', 'Other' from dual union all
10 select 'MAN', 'Manager' from dual union all
11 select 'DIR', 'Director' from dual
12 )
13 --
14 select e.sno,
15 e.name,
16 listagg(m.role_name, ',') within group (order by column_value) role
17 from employee e cross join
18 table(cast(multiset(select level from dual
19 connect by level <= regexp_count(e.role, ',') + 1
20 ) as sys.odcinumberlist))
21 join role_master m on m.role = regexp_substr(e.role, '[^,]+', 1, column_value)
22 group by e.sno, e.name;
SNO NAME ROLE
---------- ---- ----------------------------------------
1 a Electrician,Plumber,Other
2 b Manager,Director
SQL>
Another solution could use this logic :
First, generate the maximum number of rows needed for the split step (required_rows_v)
Then, make left join between the three data sources like below
Then, use listagg function to re-aggregate rows
With required_rows_v (lvl) as (
select level lvl
from dual
connect by level <= ( select max( regexp_count( e.Role, '[^,]+' ) ) from employee e )
)
select e.SNO,
e.NAME,
listagg(rm.Role_name, ',')within group (order by e.SNo, v.lvl) Role
from employee e
left join required_rows_v v on v.lvl <= regexp_count( e.Role, '[^,]+' )
left join Role_master rm on rm.Role = regexp_substr( e.Role, '[^,]+', 1, v.lvl )
group by e.SNO, e.NAME
demo

Writing Nested SQL queries

I have 3 tables as StudentData which includes data of students, Subjects table which has data of all subjects offered and Marks which has the marks obtained by students for each subject.Marks table maps to StudentData table by StudentId and Subjects table by SubjectId
What I want to do is select the maximum marks for each subject and the name of the student like follows
So I wrote a Oracle PL/SQL Query as follows,
select MAX(marks)
from
(select Marks ,subjects.name as SJN ,studentdata.name
from (studentdata inner Join marks On studentdata.studentid = marks.studentid)
Inner Join subjects On subjects.subjectid = marks.subjectid)
where SJN in (select name from subjects);
But it gives only one result.Please help me to develop a query to get my expected result set.
Something like this? Lines #1 - 26 represent sample data (you don't type that); query you need begins at line #28.
SQL> with
2 -- sample data
3 studentdata (studentid, name, course) as
4 (select 1, 'olivier', 'it' from dual union all
5 select 2, 'noah', 'business' from dual union all
6 select 3, 'jack', 'business' from dual union all
7 select 4, 'mason', 'it' from dual union all
8 select 5, 'julion', 'it' from dual),
9 subjects (subjectid, name) as
10 (select 1, 'java' from dual union all
11 select 2, 'business stg' from dual union all
12 select 3, 'python' from dual union all
13 select 4, 'statistics' from dual union all
14 select 5, 'mgt accounting' from dual union all
15 select 7, 'social studies' from dual union all
16 select 8, 'ess english' from dual),
17 marks (id, studentid, subjectid, marks) as
18 (select 1, 1, 1, 56 from dual union all
19 select 2, 1, 2, 78 from dual union all
20 select 3, 1, 7, 83 from dual union all
21 select 4, 1, 3, 45 from dual union all
22 select 5, 1, 5, 63 from dual union all
23 --
24 select 6, 2, 1, 99 from dual union all
25 select 7, 3, 2, 10 from dual union all
26 select 8, 4, 7, 83 from dual)
27 --
28 select b.name subject, s.name student, m.marks
29 from marks m join subjects b on b.subjectid = m.subjectid
30 join studentdata s on s.studentid = m.studentid
31 where m.marks = (select max(m1.marks)
32 from marks m1
33 where m1.subjectid = m.subjectid
34 )
35 order by b.name, s.name;
SUBJECT STUDENT MARKS
-------------- ------- ----------
business stg olivier 78
java noah 99
mgt accounting olivier 63
python olivier 45
social studies mason 83
social studies olivier 83
6 rows selected.
SQL>
You can use the analytical function ROW_NUMBER as follows:
SELECT SJN, MARKS, STUNAME FROM
(SELECT
MARKS.MARKS,
SUBJECTS.NAME AS SJN,
STUDENTDATA.NAME AS STUNAME,
ROW_NUMBER() OVER (PARTITION BY SUBJECTS.SUBJECTID
ORDER BY MARKS.MARKS DESC NULLS LAST) AS RN
FROM
STUDENTDATA
INNER JOIN MARKS ON STUDENTDATA.STUDENTID = MARKS.STUDENTID
INNER JOIN SUBJECTS ON SUBJECTS.SUBJECTID = MARKS.SUBJECTID)
WHERE RN = 1;
The first thing that comes to mind: select the best mark per subject, then select the student(s) with that mark in that subject:
select s.name as subject, m.marks, sd.name as studentname
from marks m
join studentdata sd on sd.studentid = m.studentid
join subjects s on s.subjectid = m.subjectid
where (m.subjectid, m.marks) in
(
select subjectid, max(marks)
from marks
group by subjetid
);
As you see, we select twice from marks. This can be avoided with a window function:
select s.name as subject, m.marks, sd.name as studentname
from
(
select
subjectid,
marks,
max(marks) over (partition by subjectid) as max_marks
from marks
) m
join studentdata sd on sd.studentid = m.studentid
join subjects s on s.subjectid = m.subjectid
where m.marks = m.max_marks;
Another option is to join and to check that no better marks exist for the subject:
select s.name as subject, m.marks, sd.name as studentname
from marks m
join studentdata sd on sd.studentid = m.studentid
join subjects s on s.subjectid = m.subjectid
where not exists
(
select null
from marks m2
where m2.subjectid = m.subjectid
and m2.marks > m.marks
);
Which of these options is best, I cannot tell. Decide for the one that you find most readable. Regardless of which query you choose, this index should help the DBMS finding the highest marks quickly:
create index idx on marks(subjectid, marks);

Query to pick departments

I have a table like below:
Dept:
DeptName State Country
D1 Paris France
D2 Lyon France
D3 Lille France
Employee:
EmployeeId EmployeeName DeptName
1 John D1
2 Diesel D2
3 Rock D2
4 John D1
5 Diesel D2
6 Karn D2
7 Triple D1
8 Nancy D3
9 Greg D3
Output:
DeptName CountOfDifferentEmployeeName
D3 2
Get the department name and count of unique employee names for department using following conditions:
Pick only those department which have unique employee names in their country.There should not be same employee name in department in same country
While counting, avoid counting repeated employee names for the same department
But I am confused with whether I should do group by country and zoo name and how do I pick zoo name satisfying condition 1.
select * from Dept d
inner join Employee e on d.DeptName = e.DeptName
group by
If I get your requirement correct, this following script will help you to achieve your desired output-
DEMO HERE
SELECT D.DeptName,D.Country,
COUNT(E.EmployeeName) CountOfDifferentEmployeeName
FROM Employee E
iNNER JOIN Dept D ON E.DeptName = D.DeptName
GROUP BY D.DeptName,D.Country
HAVING COUNT(E.EmployeeName) = COUNT(DISTINCT E.EmployeeName)
I gather you want the names of department that don't have repeated employee name. If that is the case the query below is one way you can do it:
--1 Get per country all department that have repeated employee names
--2 Get the count per country and department of unique employee excluding the county department list in the cte
with Dept as
(
select 'D1' as DeptName, 'Paris' as State , 'France' as Country
union select 'D2' , 'Lyon' , 'France'
union select 'D3' , 'Lille' , 'France'
),
Employee as
(
select 1 as EmployeeId , 'John' as EmployeeName , 'D1' as DeptName
union select 2 , 'Diesel' , 'D2'
union select 3 , 'Rock' , 'D2'
union select 4 , 'John' , 'D1'
union select 5 , 'Diesel' , 'D2'
union select 6 , 'Karn' , 'D2'
union select 7 , 'Triple' , 'D1'
union select 8 , 'Nancy' , 'D3'
union select 9 , 'Greg' , 'D3'
)
select d.DeptName, d.Country, count(*) as unique_employee_names
from Dept d
inner join Employee e on d.DeptName = e.DeptName
where not exists
(
select 1
from (
select d.DeptName, d.Country
from Dept d
inner join Employee e on d.DeptName = e.DeptName
group by d.DeptName, d.Country, e.EmployeeName
having count(*) > 1
) c where d.DeptName = c.DeptName and d.Country = c.Country
)
group by d.DeptName, d.Country
Ouput is:
DeptName Country unique_employee_names
D3 France 2

Interaction of where clause with connect by And Creating query to fetch next level in a hierarchy

Table:
create table temp_hierarchy_define (dept varchar2(25), parent_dept varchar2(25))
create table temp_employee (empid number(1), empname varchar2(50), dept varchar2(25), salary number(10))
Data
Select 'COMPANY' dept , 'COMPANY' parent_dept From Dual Union All
Select 'IT' , 'COMPANY' From Dual Union All
Select 'MARKET' , 'COMPANY' From Dual Union All
Select 'ITSEC' , 'IT' From Dual Union All
Select 'ITDBA' , 'IT' From Dual Union All
Select 'ITDBAORC' , 'ITDBA' From Dual Union All
Select 'ITDBASQL' , 'ITDBA' From Dual
select 1 empid, 'Rohan-ITDBASQL' empname ,'ITDBASQL' dept ,10 salary from dual union all
select 2, 'Raj-ITDBAORC' ,'ITDBAORC' ,20 from dual union all
select 3, 'Roy-ITDBA' ,'ITDBA' ,30 from dual union all
select 4, 'Ray-MARKET' ,'MARKET' ,40 from dual union all
select 5, 'Roopal-IT' ,'IT' ,50 from dual union all
select 6, 'Ramesh-ITSEC' ,'ITSEC' ,60 from dual
Requirement
Summarize salary of all IT dept:
CATEGORY SALARY
5,50
ITSEC,60
ITDBA,60
Summarize salary of all COMPANY dept:
CATEGORY SALARY
IT,170
MARKET,40
Summarize salary of all ITDBA dept:
CATEGORY SALARY
3,30
ITDBASQL,10
ITDBAORC,20
You will notice that we are trying to summarize based on the next level in the hierarchy. If any emp is already part of that level then we need to show the employee.
Trial Query:
Select Category,sum(salary) from (
Select
NVL((Select dept.dept from temp_hierarchy_define dept
Where dept.parent_dept = 'IT'
And dept.dept != 'IT'
Start With dept.dept = emp.dept
Connect by NOCYCLE dept.dept = Prior dept.parent_dept
and prior dept.dept is not null),emp.empid) category,
emp.*
From temp_employee emp
Where emp.DEPT in
(Select dept.dept from temp_hierarchy_define dept
Start With dept.dept = 'IT'
connect by nocycle prior dept.dept = dept.parent_dept) ) Group by Category
Concerns & queries:
Whether this query will work well in all scenarios. Or there any better way of doing it ??
How does where condition interact with connect by. For eg in the sub query we are filtering with parent_dept = 'IT', however while starting connect by some emp might have parent_dept = 'ITDBASQL' which is also part of IT. I am having a hard time in understanding the workflow.
Thank you for your time and assistance.
Or there any better way of doing it ?
This is an equivalent query that only requires one table scan for each table. You will need to determine whether your query or this one is more performant for your data/indexes/etc.
SQL Fiddle
Oracle 11g R2 Schema Setup:
create table temp_hierarchy_define (
dept varchar2(25), parent_dept varchar2(25));
create table temp_employee (
empid number(1), empname varchar2(50), dept varchar2(25), salary number(10));
INSERT INTO temp_hierarchy_define( dept, parent_dept )
Select 'COMPANY' , 'COMPANY' From Dual Union All
Select 'IT' , 'COMPANY' From Dual Union All
Select 'MARKET' , 'COMPANY' From Dual Union All
Select 'ITSEC' , 'IT' From Dual Union All
Select 'ITDBA' , 'IT' From Dual Union All
Select 'ITDBAORC' , 'ITDBA' From Dual Union All
Select 'ITDBASQL' , 'ITDBA' From Dual;
INSERT INTO temp_employee( empid, empname, dept, salary )
select 1, 'Rohan-ITDBASQL' ,'ITDBASQL' ,10 from dual union all
select 2, 'Raj-ITDBAORC' ,'ITDBAORC' ,20 from dual union all
select 3, 'Roy-ITDBA' ,'ITDBA' ,30 from dual union all
select 4, 'Ray-MARKET' ,'MARKET' ,40 from dual union all
select 5, 'Roopal-IT' ,'IT' ,50 from dual union all
select 6, 'Ramesh-ITSEC' ,'ITSEC' ,60 from dual;
Query 1:
SELECT dept,
SUM( salary )
FROM (
SELECT CASE
WHEN lvl = 1 AND h.parent_dept = e.dept
THEN CAST( e.empid AS VARCHAR2(25) )
ELSE root_dept
END AS dept,
e.empid,
e.salary
FROM ( SELECT CONNECT_BY_ROOT( dept ) AS root_dept,
h.*,
LEVEL AS lvl,
ROW_NUMBER() OVER ( PARTITION BY parent_dept ORDER BY ROWNUM ) AS rn
FROM temp_hierarchy_define h
WHERE parent_dept != dept
START WITH h.parent_dept = 'IT'
CONNECT BY NOCYCLE PRIOR h.dept = h.parent_dept
) h
LEFT OUTER JOIN
temp_employee e
ON ( h.dept = e.dept
OR ( h.parent_dept = e.dept AND h.lvl = 1 AND h.rn = 1)
)
)
GROUP BY dept
Results:
| DEPT | SUM(SALARY) |
|-------|-------------|
| ITDBA | 60 |
| 5 | 50 |
| ITSEC | 60 |
You can run the queries individually to find out what they are doing:
SELECT CONNECT_BY_ROOT( dept ) AS root_dept,
h.*,
LEVEL AS lvl,
ROW_NUMBER() OVER ( PARTITION BY parent_dept ORDER BY ROWNUM ) AS rn
FROM temp_hierarchy_define h
WHERE parent_dept != dept
START WITH h.parent_dept = 'IT'
CONNECT BY NOCYCLE PRIOR h.dept = h.parent_dept
Just lists all the rows in the hierarchy and uses CONNECT_BY_ROOT to get the department at the root of the branch of the hierarchy. LEVEL and ROW_NUMBER() are used to find the first row at the top of the hierarchy.

Binary "OR" operation on a SQL column

I have a query that returns weekdays
Select PERSON_NAME, PERSON_DAY from PERSON_DAYS WHERE PERSON_ID = #myId
say I obtain
John 1 (mo)
John 3 (mo tu)
John 8 (th)
I need to obtain for John all the days when is busy. How do I a logical OR on the PERSON_DAY column in this query?
the result should be 11 (mo tu th)
well here my best so far
;with PowersOf2
as
(
select 1 as Number
union all
select A.Number * 2 from PowersOf2 as A where A.Number < 64
)
select P.PERSON_NAME, sum(distinct P.PERSON_DAY & PowersOf2.Number)
from PERSON_DAYS as P
left outer join PowersOf2 on PowersOf2.Number <= P.PERSON_DAY
where P.PERSON_ID = #myId
group by P.PERSON_NAME
SQL FIDDLE EXAMPLE
If I understand you correctly, you can use a combination of bitwise operator and and aggregate function sum to do what you want.
Example:
with person_days as (
select 'John' as person_name, 1 as weekday --mo
union select 'John', 3 -- mo, tu
union select 'John', 8 -- th
union select 'Jane', 1 -- mo
union select 'Jane', 9 -- mo, th
union select 'Jane', 40 -- th, sa
),
Bits AS (
SELECT 1 AS BitMask --mo
UNION ALL SELECT 2 --tu
UNION ALL SELECT 4 --we
UNION ALL SELECT 8 --th
UNION ALL SELECT 16 --fr
UNION ALL SELECT 32 --sa
UNION ALL SELECT 64 --su
UNION ALL SELECT 128
)
, person_single_days as (
select distinct person_name, weekday & bits.BitMask single_weekday
from person_days
inner join bits on person_days.weekday & bits.BitMask > 0
)
select person_name, sum(single_weekday) weekdays
from person_single_days
group by person_name;
result:
person_name weekdays
----------- -----------
Jane 41
John 11
"inspired" by Roman's CTE: (note that the first CTE just generates demo data)
with p as
(
select 'John' as PERSON_NAME, 1 as PERSON_DAY
union
select 'John', 3
union
select 'John', 8
union
select 'Jane', 2
union
select 'Jane', 4
),
cte as
(
select PERSON_NAME, PERSON_DAY from p
union all
select cte2.PERSON_NAME, p.PERSON_DAY | cte2.PERSON_DAY
from p
inner join cte as cte2 on p.PERSON_NAME = cte2.PERSON_NAME
where p.PERSON_DAY & cte2.PERSON_DAY = 0
)
select PERSON_NAME, MAX(PERSON_DAY) from cte
group by PERSON_NAME
I think what you are looking for is a custom aggregate that does an OR. You can write that using SQL CLR in .NET. This is probably the cleanest solution. It will be reusable, too.
Alternatively, you could use cursor-based loops to calculate the result.
You could also (mis)use CTE's for this purpose.