Calculate years from each date in Oracle SQL - sql

Noob question. I'm writing a script to execute a report showing:
Student last name and first name (comma separated), years enrolled, academic advisor last name and first name (comma separated)
Sort: years enrolled
Filter: only include currently active students
Struggling with calculating years enrolled and filtering by active students.
The tables I'm using:
SELECT CONCAT(CONCAT(Student.Last_Name, ', '), Student.First_Name) AS "Student",
Student_Enrollment_Status.Date_Status_Updated,
CONCAT(CONCAT(Faculty.Last_Name, ', '), Faculty.First_Name) AS "Advisor"
FROM Student
WHERE Faculty.Faculty_ID = Student.Advisor_ID
AND Student(Student_ID) = Student_Enrollment_Status(Student_ID)
AND Student_Enrollment_Status(Status_ID) = Enrollment_Status (Status_ID);

It would be something like this:
lines #1 - 22 represent sample data (you don't type that)
query you need begins at line #24
years enrolled is calculated by subtracting current year and year of enrollment; that's rather inaccurate, but - you didn't explain what exactly it means
I presumed that active statuses are IDs (2, 3, 5)
SQL> with
2 student (student_id, first_name, last_name, advisor_id) as
3 (select 1, 'ash', 'smith', 9 from dual union all
4 select 2, 'tash', 'paul', 8 from dual union all
5 select 3, 'carl', 'wall', 6 from dual union all
6 select 4, 'fred', 'john', 3 from dual),
7 student_enrollment_status (student_id, status_id, date_status_updated) as
8 (select 1, 2, date '2017-09-04' from dual union all
9 select 2, 3, date '2018-09-05' from dual union all
10 select 3, 3, date '2018-09-05' from dual union all
11 select 4, 2, date '2019-09-04' from dual),
12 enrollment_status (status_id, status) as
13 (select 2, 'enrolled' from dual union all
14 select 3, 'on leave' from dual union all
15 select 4, 'full time' from dual union all
16 select 5, 'part time' from dual union all
17 select 6, 'withdrawn' from dual),
18 faculty (faculty_id, first_name, last_name) as
19 (select 9, 'jane', 'gold' from dual union all
20 select 8, 'sam', 'greene' from dual union all
21 select 6, 'mark', 'west' from dual union all
22 select 3, 'jen', 'dash' from dual)
23 --
24 select s.last_name ||', '|| s.first_name student,
25 extract(year from sysdate) - extract(year from ses.date_status_updated)
26 years_enrolled,
27 f.last_name ||', '|| f.first_name advisor
28 from student s join student_enrollment_status ses on ses.student_id = s.student_id
29 join enrollment_status es on es.status_id = ses.status_id
30 join faculty f on f.faculty_id = s.advisor_id
31 where es.status_id in (2, 3, 5)
32 order by years_enrolled;
STUDENT YEARS_ENROLLED ADVISOR
----------- -------------- ------------
john, fred 1 dash, jen
paul, tash 2 greene, sam
wall, carl 2 west, mark
smith, ash 3 gold, jane
SQL>

Related

Returning number occurrences of a value in a column grouped by another column

I have a table that looks like below:
STUDENT_ID
CLASS_TYPE_ID
CLASS_TIME
CLASS_SUBJECT
1
1
08:30
Arithmetic
1
3
10:30
Science
1
3
11:30
Advanced Science
2
1
09:30
Arithmetic
2
7
11:00
Cooking
1
5
14:00
English
CREATE TABLE table1
(
student_id number,
class_type_id number,
class_time varchar,
class_subject varchar
);
---
INSERT INTO table1 (student_id, class_type_id, class_time, class_subject)
VALUES (1, 1, '08:30', 'Arithmetic'),
(1, 3, '10:30', 'Science'),
(1, 3, '11:30', 'Advanced Science'),
(2, 1, '09:30', 'Arithmetic'),
(2, 7, '11:00', 'Cooking'),
(1, 5, '14:00', 'English');
I want a query that checks for and returns any students that are enrolled in duplicate class types (shown below):
STUDENT_ID
CLASS_TYPE_ID
1
3
I've tried the following but was wondering if there was a 'cleaner' or different way to do this?
SELECT
student_id, count
FROM
(SELECT
COUNT(*) AS count,
student_id, class_type_id
FROM
(SELECT
student_id, class_type_id
FROM
table1)
GROUP BY
student_id)
WHERE
count > 1;
Also: is there any way I can cleanly verify whether STUDENT_ID <-> CLASS_TYPE_ID is a 1:1 relationship (I know that this example OBVIOUSLY is not a 1:1 relationship but was just curious...)
Thanks in advance!
Indeed, there is a simpler way.
Sample data:
SQL> with table1 (student_id, class_type_id, class_time, class_subject) as
2 (select 1, 1, '08:30', 'Arithmetic' from dual union all
3 select 1, 3, '10:30', 'Science' from dual union all
4 select 1, 3, '11:30', 'Advanced Science' from dual union all
5 select 2, 1, '09:30', 'Arithmetic' from dual union all
6 select 2, 7, '11:00', 'Cooking' from dual union all
7 select 1, 5, '14:00', 'English' from dual
8 )
Query begins here:
9 select student_id, class_type_id
10 from table1
11 group by student_id, class_type_id
12 having count(*) > 1;
STUDENT_ID CLASS_TYPE_ID
---------- -------------
1 3
SQL>

How to filter records based on another grouped by column in a same table

I have a sample table as below:
Target output:
I have tried with below script but only number of count produced as output:
select ID, count(COUNTRY) from test
group by COUNTRY
having count(COUNTRY)=3;
Could anyone here please help how can I get the targeted output? Thanks.
Here's one option - a subquery:
Sample data:
SQL> with test (id, name, country) as
2 (select 1, 'Mike' , 'Australia' from dual union all
3 select 2, 'Jason', 'Australia' from dual union all
4 select 3, 'Lee' , 'China' from dual union all
5 select 4, 'Simon', 'India' from dual union all
6 select 5, 'Alex' , 'Malaysia' from dual union all
7 select 6, 'John' , 'Australia' from dual
8 )
Query:
9 select id, country
10 from test
11 where country in (select country
12 from test
13 group by country
14 having count(*) = 3
15 )
16 order by id;
ID COUNTRY
---------- ---------
1 Australia
2 Australia
6 Australia
SQL>
You can use analytic functions and only scan the table once:
SELECT id, name, country
FROM (
SELECT id, name, country,
COUNT(*) OVER (PARTITION BY country) AS people_per_country
FROM table_name
)
WHERE people_per_country = 3;
Which, for the sample data:
CREATE TABLE table_name (id, name, country) AS
SELECT 1, 'Mike' , 'Australia' FROM DUAL UNION ALL
SELECT 2, 'Jason', 'Australia' FROM DUAL UNION ALL
SELECT 3, 'Lee' , 'China' FROM DUAL UNION ALL
SELECT 4, 'Simon', 'India' FROM DUAL UNION ALL
SELECT 5, 'Alex' , 'Malaysia' FROM DUAL UNION ALL
SELECT 6, 'John' , 'Australia' FROM DUAL;
Outputs:
ID
NAME
COUNTRY
1
Mike
Australia
2
Jason
Australia
6
John
Australia
db<>fiddle here

How to get the max count of an attribute with 3 tables?

I need to query which author sold the most books and how many books the author sold.
select a.firstname ||''|| a.lastname as fullname,
max(count(datesold))
from author a,
transaction t,
book b
where a.authorid = b.authorid
and b.bookid = t.bookid
group by
a.firstname,
a.lastname;
It gave me an error of not a single-group group function.
Any idea what is the issue here?
With some sample data
SQL> with
2 author (authorid, firstname, lastname) as
3 (select 1, 'Stephen', 'King' from dual union all
4 select 2, 'Jo' , 'Nesbo' from dual),
5 book (bookid, authorid) as
6 (select 100, 1 from dual union all
7 select 200, 1 from dual union all
8 select 300, 2 from dual
9 ),
10 transaction (trans_id, bookid) as
11 (select 1, 100 from dual union all
12 select 2, 100 from dual union all
13 select 3, 100 from dual union all
14 select 4, 300 from dual
15 ),
query uses the RANK analytic function which ranks rows by number of rows in the transaction table (it says how many books were sold). Finally, fetch row(s) that rank as highest:
16 temp as
17 (select a.firstname || ' ' || a.lastname AS fullname,
18 count(t.bookid) cnt,
19 rank() over (order by count(t.bookid) desc) rnk
20 from author a join book b on a.authorid = b.authorid
21 join transaction t on t.bookid = b.bookid
22 group by a.firstname, a.lastname
23 )
24 select fullname, cnt
25 from temp
26 where rnk = 1;
FULLNAME CNT
------------- ----------
Stephen King 3
SQL>
You can use:
select MAX(a.firstname ||' '|| a.lastname) as fullname,
COUNT(datesold)
from author a
INNER JOIN book b
ON (a.authorid = b.authorid)
INNER JOIN transaction t
ON (b.bookid = t.bookid)
GROUP BY
a.authorid
ORDER BY
COUNT(datesold) DESC
FETCH FIRST ROW ONLY;
Do not aggregate by firstname and lastname as there are many people in the world with identical names and you do not want to count everyone with the same name as a single person.
Which, for the sample data:
CREATE TABLE author (authorid, firstname, lastname, dateofbirth) AS
SELECT 1, 'Alice', 'Adams', DATE '1900-01-01' FROM DUAL UNION ALL
SELECT 2, 'Alice', 'Adams', DATE '1910-01-01' FROM DUAL UNION ALL
SELECT 3, 'Betty', 'Baron', DATE '1920-01-01' FROM DUAL UNION ALL
SELECT 4, 'Carol', 'Corrs', DATE '1930-01-01' FROM DUAL UNION ALL
SELECT 5, 'Carol', 'Corrs', DATE '1940-01-01' FROM DUAL;
CREATE TABLE book (bookid, authorid) AS
SELECT 1, 1 FROM DUAL UNION ALL
SELECT 2, 2 FROM DUAL UNION ALL
SELECT 3, 3 FROM DUAL UNION ALL
SELECT 4, 4 FROM DUAL UNION ALL
SELECT 5, 5 FROM DUAL;
CREATE TABLE transaction (bookid, datesold) AS
SELECT 1, DATE '1970-01-01' FROM DUAL UNION ALL
SELECT 1, DATE '1970-01-02' FROM DUAL UNION ALL
SELECT 1, DATE '1970-01-03' FROM DUAL UNION ALL
SELECT 1, DATE '1970-01-04' FROM DUAL UNION ALL
SELECT 3, DATE '1970-01-01' FROM DUAL UNION ALL
SELECT 4, DATE '1970-01-01' FROM DUAL UNION ALL
SELECT 4, DATE '1970-01-02' FROM DUAL UNION ALL
SELECT 5, DATE '1970-01-01' FROM DUAL UNION ALL
SELECT 5, DATE '1970-01-02' FROM DUAL UNION ALL
SELECT 5, DATE '1970-01-03' FROM DUAL;
Outputs:
FULLNAME
COUNT(DATESOLD)
Alice Adams
4
db<>fiddle here

Distinct LISTAGG that is inside a subquery in the SELECT list

Here is a minimal working example of what I'm trying to do and what I'm getting:
I have a query as follows:
/*
with tran_party as -- ALL DUMMY DATA ARE IN THESE CTE FOR YOUR REFERENCE
(select 1 tran_party_id, 11 transaction_id, 101 team_id_redirect
from dual
union all
select 2, 11, 101 from dual
union all
select 3, 11, 102 from dual
union all
select 4, 12, 103 from dual
union all
select 5, 12, 103 from dual
union all
select 6, 12, 104 from dual
union all
select 7, 13, 104 from dual
union all
select 8, 13, 105 from dual),
tran as
(select 11 transaction_id, 1001 account_id, 1034.93 amount from dual
union all
select 12, 1001, 2321.89 from dual
union all
select 13, 1002, 3201.47 from dual),
account as
(select 1001 account_id, 111 team_id from dual
union all
select 1002, 112 from dual),
team as
(select 101 team_id, 'UUU' as team_code from dual
union all
select 102, 'VV' from dual
union all
select 103, 'WWW' from dual
union all
select 104, 'XXXXX' from dual
union all
select 105, 'Z' from dual)
-- */
-- The Actual Query
select a.account_id,
t.transaction_id,
(select listagg (tm_redir.team_code, ', ')
within group (order by tm_redir.team_code)
from tran_party tp_redir
inner join team tm_redir
on tp_redir.team_id_redirect = tm_redir.team_id
inner join tran t_redir
on tp_redir.transaction_id = t_redir.transaction_id
where t_redir.account_id = a.account_id
and t_redir.transaction_id != t.transaction_id)
as teams_redirected
from tran t inner join account a on t.account_id = a.account_id;
NOTE: tran_party.team_id_redirect is a foreign key that references team.team_id.
Current output:
ACCOUNT_ID TRANSACTION_ID TEAMS_REDIRECTED
---------- -------------- ----------------
1001 11 WWW, WWW, XXXXX
1001 12 UUU, UUU, VV
1002 13
Expected output:
I want the repeated items in TEAMS_REDIRECTED column to be selected only once, like this:
ACCOUNT_ID TRANSACTION_ID TEAMS_REDIRECTED
---------- -------------- ----------------
1001 11 WWW, XXXXX
1001 12 UUU, VV
1002 13
What I tried:
Instead of selecting from tran_party directly, I wrote an inline view that selects distinct values from tran_party like this:
select a.account_id,
t.transaction_id,
(select listagg (tm_redir.team_code, ', ')
within group (order by tm_redir.team_code)
from (select distinct transaction_id, team_id_redirect -- Note this inline view
from tran_party) tp_redir
inner join team tm_redir
on tp_redir.team_id_redirect = tm_redir.team_id
inner join tran t_redir
on tp_redir.transaction_id = t_redir.transaction_id
where t_redir.account_id = a.account_id
and t_redir.transaction_id != t.transaction_id)
as teams_redirected
from tran t inner join account a on t.account_id = a.account_id;
While this does give me the expected output, when I use this solution in my actual code, it takes about 13 seconds to retrieve just one row. Thus I cannot use what I already tried.
Any help will be appreciated.
The following method gets rid of the in-line view to fetch duplicates, it uses REGEXP_REPLACE and RTRIM on the LISTAGG function to get the distinct result set in the aggregated list. Thus, it won't do more than one scan.
Adding this piece to your code,
RTRIM(REGEXP_REPLACE(listagg (tm_redir.team_code, ',')
WITHIN GROUP (ORDER BY tm_redir.team_code),
'([^,]+)(,\1)+', '\1'),
',')
Modified query-
SQL> with tran_party as -- ALL DUMMY DATA ARE IN THESE CTE FOR YOUR REFERENCE
2 (select 1 tran_party_id, 11 transaction_id, 101 team_id_redirect
3 from dual
4 union all
5 select 2, 11, 101 from dual
6 union all
7 select 3, 11, 102 from dual
8 union all
9 select 4, 12, 103 from dual
10 union all
11 select 5, 12, 103 from dual
12 union all
13 select 6, 12, 104 from dual
14 union all
15 select 7, 13, 104 from dual
16 union all
17 select 8, 13, 105 from dual),
18 tran as
19 (select 11 transaction_id, 1001 account_id, 1034.93 amount from dual
20 union all
21 select 12, 1001, 2321.89 from dual
22 union all
23 select 13, 1002, 3201.47 from dual),
24 account as
25 (select 1001 account_id, 111 team_id from dual
26 union all
27 select 1002, 112 from dual),
28 team as
29 (select 101 team_id, 'UUU' as team_code from dual
30 union all
31 select 102, 'VV' from dual
32 union all
33 select 103, 'WWW' from dual
34 union all
35 select 104, 'XXXXX' from dual
36 union all
37 select 105, 'Z' from dual)
38 -- The Actual Query
39 select a.account_id,
40 t.transaction_id,
41 (SELECT RTRIM(
42 REGEXP_REPLACE(listagg (tm_redir.team_code, ',')
43 WITHIN GROUP (ORDER BY tm_redir.team_code),
44 '([^,]+)(,\1)+', '\1'),
45 ',')
46 from tran_party tp_redir
47 inner join team tm_redir
48 on tp_redir.team_id_redirect = tm_redir.team_id
49 inner join tran t_redir
50 on tp_redir.transaction_id = t_redir.transaction_id
51 where t_redir.account_id = a.account_id
52 and t_redir.transaction_id != t.transaction_id)
53 AS teams_redirected
54 from tran t inner join account a on t.account_id = a.account_id
55 /
ACCOUNT_ID TRANSACTION_ID TEAMS_REDIRECTED
---------- -------------- --------------------
1001 11 WWW,XXXXX
1001 12 UUU,VV
1002 13
SQL>

Oracle SQL dynamically select column name

A user gave me a table that looks like the following.
Name HH08 HH09 HH10 HH11 HH12 HH13
Bob 2 3 4 2 7 1
Steve 2 9 3 2 2 5
Mike 2 2 2 2 3 2
Pat 1 0 2 0 0 0
I need some sql that will select the row based on the name, and the column based on the current hour of sysdate when the query is run.
If it is 9:27 am and the user is Steve, the sql needs to select the 9 value.
Is there any simple sql that will do this, or do I need to restructure the table the user gives me, which will be occassionally.
Thanks in advance.
Try:
select case to_char(sysdate,'hh24')
when '08' then hh08
when '09' then hh09
when '10' then hh10
when '11' then hh11
when '12' then hh12
when '13' then hh13
end OutputValue
from TableName
WHERE Name = 'Steve'
SELECT 'HH'+convert(char(2),DATEPART(hour,getdate()))
FROM TableName
WHERE Name = 'Steve'
try this out
with t as (
select 'Bob' name, 2 hh08, 3 hh09, 4 hh10, 2 hh11, 7 hh12, 1 hh13 from dual union all
select 'Steve', 2, 9, 3, 2, 2, 5 from dual union all
select 'Mike', 2, 2, 2, 2, 3, 2 from dual union all
select 'Pat', 1, 0, 2, 0, 0, 0 from dual
)
--/\-- Data sample --/\--
select value from t
unpivot(value for hr in (hh08 as '08', hh09 as '09', hh10 as '10', hh11 as '11', hh12 as '12', hh13 as '13') )
where hr = to_char(sysdate, 'HH24')
and name = 'Pat';