Order oracle query result in sequence as of OR clauses - sql

I have an oracle table with structure something like:
School {
Student_Id,
Student_Name,
Class,
Sport,
Club
}
I want to write a query to fetch all students who either belong to X class, or Y Sport, or Z club.
But I want to order the result based on the sequence of my OR conditions.
That is all students belonging to X class will come first, before the students of Y sport. Then students of Z club will come.
Also, no duplicate results. That is if John is from class X & also belongs to sport Y, then he should only be only appear once and on top of all results of sports Y.

This is how I understood the question:
SQL> with school (student_name, class, sport, club) as
2 (select 'Scott', 'x', 'a', 'c' from dual union all
3 select 'Mike' , 'b', 'c', 'z' from dual union all
4 select 'Kate' , 'x', 'y', 'z' from dual union all
5 select 'John' , 'x', 'b', 'd' from dual union all
6 select 'Vito' , 'd', 'e', 'g' from dual
7 )
8 select * from school
9 where class = 'x' or sport = 'y' or club = 'z'
10 order by case when class = 'x' then 1 end,
11 case when sport = 'y' then 2 end,
12 case when club = 'z' then 3 end;
STUDENT CLASS SPORT CLUB
------- ----- ----- -----
Kate x y z
Scott x a c
John x b d
Mike b c z
SQL>
If that's not it, please, post sample data and expected result.

I would write this as:
select s.*
from school s
where class = 'X' or sport = 'Y' or club = 'Z'
order by (case when class = 'X' then 1
when sport = 'Y' then 2
when club = 'Z' then 3
end)
If you don't want to repeat the conditions, you can use a subquery, CTE, or -- in Oracle 12C -- a lateral join:
select s.*
from school s cross join lateral
(select (case when class = 'X' then 1
when sport = 'Y' then 2
when club = 'Z' then 3
end) as ord
from dual
) x
where x.ord is not null
order by x.ord

select student_id
, case when class='X' then 1
when sport = 'Y' then 2
when club='Z' then 3
else 4
end as Ordr
from School
WHERE class='X' or sport = 'Y' or club='Z'
Order by ordr
Explanation:
The WHERE class='X' or sport = 'Y' or club='Z' simply implements your desired filtering.
The custom ordering is implemented by creating an additional column called ordr and using it in the ORDER BY clause. This column is created using a case statement. The order of the how this is written is important because a CASE expression evaluates to the first true condition and, if there is no true condition, it evaluates to the ELSE part.
So all students taking class X will get an ordr of 1, regardless of their sport and club.
If a student does not take class X, the expression will try to evaluate if the student takes sport Y and if that is the case, that student will get assigned an ordr of 2 regardless other column values.
Then if the student does not take either class X nor sport Y, the case expression will check if the student is in club Z. If that is true he will be asigned an ordr of 3.
Lastly, if a student is neither in class X, does sport Y or is in club Z, he wil get assigned an ordr of 4.
ORDER BY is ASCENDING by degfault meaning 1 will show up before 2 and so on.
What this algorithm does not do, which you can tell by the above explanation, is prioritize a student that takes class X, sport Y, club Z over someone that only takes class X.

Related

query to select columns from a row in which another column has certain value only SQL

Consider the following table
id attribute
1 a
1 a
1 b
2 a
2 a
3 c
4 a
I want to select the ids that have attribute of 'a' only, ie 2 and 4.
Cant select 1 because 1 has 'a' and 'b', cant select 3 because it has 'c' only. We select 2 and 4 because it has 'a' value only.
You can use
SELECT id
FROM YourTable
GROUP BY id
HAVING MAX(attribute) = 'a' AND MIN(attribute) = 'a'
AND COUNT(*) = COUNT(attribute)
the
COUNT(*) = COUNT(attribute)
is to discard any id that have NULL attribute as well as a. Remove this if that is not the semantics you want or the column is not nullable anyway.
Please test this:
SELECT id
FROM attribute
GROUP BY id
HAVING
COUNT(DISTINCT attribute) = 1 AND MIN(attribute)= 'a';

How to combine values in table

The database has the schema students(name TEXT, score INTEGER), and there is a table called grades:
Grade MIN_score MAX_score
A 4 5
B 3 4
C 2 3
I want to select the names of all students and their grades according to the table, and turn A and B to 'pass' in the resulting table.
Below is my partial solution without turning A and B to 'pass' in the resulting table, and I wonder how to achieve that additional function.
SELECT name, grade
FROM students
LEFT JOIN grades
ON grade BETWEEN MIN_score and MAX_score;
Don't use between. You'll get duplicates.
select s.name, s.score,
(case when g.grade in ('A', 'B') then 'Pass' end) as status
from students s join
grades g
on s.score > g.MIN_score and s.score <= MAX_score;
You need to be very careful about the join condition so a score of "4" is not treated as both an "A" and a "B" (as between would do).
You need to use case when expression, e.g.:
select case when grade in ('A', 'B') then 'Pass' else '' end
I believe you query should be something like this:
select name, score, case when grade in ('A', 'B') then 'Pass' else '-' end
from students
join grades on score between MIN_score and MAX_score

How can I select data from multiple SQL tables?

I have 2 tables, as described below:
Grades
student id,
exem1,
exam2,
exam3
Names
student id,
names
I want to display the names of students, their average on the 3 exams, and a letter grade. The letter grade is computed as follows:
90+ is an “A”, 80 - <90 will be a “B”, and so on. How should i do it.?
> SELECT n.student_names, ((g.exam1+g.exam2+g.exam3)/3) AS 'AVERAGE_RESULT', (CASE
WHEN ((g.exam1+g.exam2+g.exam3)/3) =90
THEN 'A'
WHEN (((g.exam1+g.exam2+g.exam3)/3) BETWEEN 80 AND 89)
THEN 'B'
WHEN (((g.exam1+g.exam2+g.exam3)/3) BETWEEN 70 AND 79)
THEN 'C'
ELSE 'D') END AS 'Student_grades',
FROM names n, grades g
WHERE n.students_id = g.students_id;
Using BETWEEN to include the ranges
SELECT n.NAME AS "Name", CASE WHEN ((g.exam1+g.exam2+g.exam3)/3) = 90 then 'A'
WHEN ((g.exam1+g.exam2+g.exam3)/3) < 90 then 'B'
ELSE 'C'
END AS "Grade"
FROM NAMES AS n INNER JOIN GRADES AS g
ON g.student_id = n.student_id
Modify the ranges as you want.

SQL Counting the number of occurence based on a subject

I find it hard to word what I am trying to achieve. I have a table that looks like this:
user char
---------
a | x
a | y
a | z
b | x
b | x
b | y
c | y
c | y
c | z
How do I write a query that would return me the following result?
user x y z
-------
a |1|1|1|
b |2|1|0|
c |0|2|1|
the numbers represent the no of occurences of chars in the original table
EDIT:
The chars values are unknown hence the solution cannot be restricted to these values. Sorry for not mentioning it sooner. I am using Oracle DB but planning to use JPQL to construct the query.
select user,
sum(case when char='x' then 1 else 0 end) as x,
sum(case when char='y' then 1 else 0 end) as y,
sum(case when char='z' then 1 else 0 end) as z
from thetable
group by user
Or, if you don't mind stacking vertically, this solution will give you a solution that works even with unknown sets of characters:
select user, char, count(*) as count
from thetable
group by user, char
This will give you:
user char count
a x 1
a y 1
a z 1
b x 2
If you want to string an unknown set of values out horizontally (as in your demo output), you're going to need to get into dynamic queries... the SQL standard is not designed to generate output with an unknown number of columns... Hope this is helpful!
Another option, using T-SQL PIVOT (SQL SERVER 2005+)
select *
from userchar as t
pivot
(
count([char]) for [char] in ([x],[y],[z])
) as p
Result:
user x y z
----------- ----------- ----------- -----------
a 1 1 1
b 2 1 0
c 0 2 1
(3 row(s) affected)
Edit ORACLE:
You can build a similar PIVOT table using ORACLE.
The tricky part is that you need the right column names in the IN ([x],[y],[z],...) statement. It shouldn't be too hard to construct the SQL query in code, getting a (SELECT DISTINCT [char] from table) and appending it to your base query.
Pivoting rows into columns dynamically in Oracle
If you don't know the exact values on which to PIVOT, you'll either need to do something procedural or mess with dynamic sql (inside an anonymous block), or use XML (in 11g).
If you want the XML approach, it would be something like:
with x as (
select 'a' as usr, 'x' as val from dual
union all
select 'a' as usr, 'y' as val from dual
union all
select 'b' as usr, 'x' as val from dual
union all
select 'b' as usr, 'x' as val from dual
union all
select 'c' as usr, 'z' as val from dual
)
select * from x
pivot XML (count(val) as val_cnt for val in (ANY))
;
Output:
USR VAL_XML
a <PivotSet><item><column name = "VAL">x</column><column name = "VAL_CNT">1</column></item><item><column name = "VAL">y</column><column name = "VAL_CNT">1</column></item></PivotSet>
b <PivotSet><item><column name = "VAL">x</column><column name = "VAL_CNT">2</column></item></PivotSet>
c <PivotSet><item><column name = "VAL">z</column><column name = "VAL_CNT">1</column></item></PivotSet>
Hope that helps

How to create this sql statment to search in each record at the many table side?

Now I have to create a query for search page depends on database that have 30 tables:
I will describe my requirement with an example:
there is a relation between table A and table B -> one to many relation
If we consider that table A is for Students info and table B to save the courses for that student each year . it is only allowed to take 3 subjects each year so the structure will be as the following:
Table A: ID ,Student-Name ,Tel#,Birthdate ......
Table B: :ID,StudentID,Date,Subject-one,Subject-two,Subject-three
so the student can take any 3 subjects per year
I want to make a query for subject called X to get all students who take it at any year but only once
example for valid student:
StudentID Sub1 Sub2 Sub3 Year
60 Z (X) Y 1
60 L W V 2
60 M P Y 3
example for invalid student:
StudentID Sub1 Sub2 Sub3 Year
10 Z (X) O 1
10 L W V 2
10 O P (x) 3
i hope that to be clear enough for my problem
I agree with comments that you would be better with one row per subject in table 2 (a normalised structure is virtually always better and more SQL friendly).
That said, you can solve it in your structure.
SELECT
studentID
FROM
table2
WHERE
sub1 = 'x'
OR sub2 = 'x'
OR sub3 = 'x'
GROUP BY
studentID
HAVING
COUNT(*) = 1
(This assumes the student can't take the same subject twice in the same year.)
Some varieties of SQL may allow the WHERE clause to be shortened...
WHERE
'x' IN (sub1, sub2, sub3)
You can then pick up all the student details by putting this in a sub-query...
SELECT
*
FROM
table1
INNER JOIN
(
<put the above query here>
)
AS lookup
ON lookup.studentID = table1.studentID
EDIT
I don't understand your comment. But an alternative (and potentially more generalisable) HAVING clause could be..
HAVING
SUM(CASE WHEN sub1 = 'x' THEN 1 ELSE 0 END +
CASE WHEN sub2 = 'x' THEN 1 ELSE 0 END +
CASE WHEN sub3 = 'x' THEN 1 ELSE 0 END)
= 1
One way of doing this..
For a given subject X, you can run through a loop for every student to check if he is a valid student for that subject like this. Inside the loop:
IF(
(SELECT count(1) from TableB where Sub1 = 'X' and StudentID = 60 ) +
(SELECT count(1) from TableB where Sub2 = 'X' and StudentID = 60 ) +
(SELECT count(1) from TableB where Sub3 = 'X'and StudentID = 60 ) ) = 1
BEGIN
INSERT INTO #ValidStudent(...)
END
You will have to put StudentID variable in place of 60 and at the end..
SELECT * FROM #ValidStudent