Laravel Nested Query - sql

i'm having hard time getting the right query for this in laravel.
I have table Exams.
id | exam_name
---|------------
1 | First Exam
2 | Second Exam
3 | Third Exam
And Students score table
id | exam_id | score
----|---------|-------
1 | 1 | 15
2 | 1 | 12
3 | 1 | 10
4 | 2 | 7
5 | 2 | 16
6 | 2 | 13
And i want to get the average scores for all exams that looks like
exam_name | average_score
------------|--------------
First Exam | 12.33
Second Exam | 12
Third Exam | 0 or NULL
I have tried left join but it returns multiple rows per exam.
I can also think of using nested Query but don't know how it works in Laravel.

DB::table('exams')
->leftJoin('student_score','exam_id','=','exams.id')
->select('exam_name', DB::raw('AVG(score) as average_score'))
->groupBy('exam_name')->get();
You can get the AVG score by using above query.
Hope this helps.

Do the left join with Students table :
select e.exam_name, avg(s.score) avg_score
from Exams e
left join Students s on s.exam_id = e.id
group by e.exam_name;
You can also use subquery with correlation approach
select *,
(select avg(s.score) from Students where e.exam_id = id) avg_score
from Exams e;

You don't need to join tables at all. You can solve this with plain laravel.
// in your controller
$exams = Exam::with('students')->get();
// in your view you should replace this with blade
foreach ($exams as $exam) {
$exam->students->avg('score'); // Average score for each exam
}
Your exam model should have:
public function students()
{
return $this->hasMany(student::class);
}
and your students class must have the inverse relation:
public function exam() {
return $this->belongsTo(exam::class);
}

Related

Condition expected - can't figure out why

I have the following query but it's not working. I want to see only one studentname because dated is unique.
SELECT r.classteacher,
r.studentname,
r.reggroup,
r.yeargroup,
rrl.cdated
FROM [dbo].[Reports] AS r
LEFT OUTER JOIN reportsreadlog AS rrl
ON r.rguid = rrl.creportguid
WHERE
(
SELECT TOP(1) r.studentname
FROM [dbo].[Reports] AS r
LEFT OUTER JOIN reportsreadlog AS rrl
ON r.rguid = rrl.creportguid
WHERE r.periodguid = '4390dc5f-eb21-4673-83f0-e7f973524916'
)
AND r.periodguid = '4390dc5f-eb21-4673-83f0-e7f973524916'
GROUP BY r.classteacher,
r.studentname,
r.reggroup,
r.yeargroup,
rrl.cdated
ORDER BY r.reggroup
Current result when I fix the error:
classteacher | studentname | reggroup | yeargroup | cdate
-------------+-------------+----------+-----------+------
Teacher 1 | Student 1 | class | year | dated
Teacher 1 | Student 1 | class | year | dated
Teacher 1 | Student 1 | class | year | dated
Desired result:
classteacher | studentname | reggroup | yeargroup | cdate
-------------+-------------+----------+-----------+------
Teacher 1 | Student 1 | class | year | dated
Teacher 1 | Student 2 | class | year | dated
Teacher 1 | Student 3 | class | year | dated
Without table definitions and sample/test data, its kind of hard to answer your question. So, please update your post to add them to assist further.
Meanwhile, please try below:
SELECT distinct r.classteacher,
r.studentname,
r.reggroup,
r.yeargroup,
rrl.cdated
FROM [dbo].[Reports] AS r
LEFT OUTER JOIN reportsreadlog AS rrl
ON r.rguid = rrl.creportguid
WHERE r.periodguid = '4390dc5f-eb21-4673-83f0-e7f973524916' --Based on my understanding you don't need other subquery condition here.
-- GROUP BY r.classteacher,
-- r.studentname,
-- r.reggroup,
-- r.yeargroup,
-- rrl.cdated
ORDER BY r.reggroup
Thank you all for helping out. In the end i gave up and did it the ugly way.
Ran the query twice and combined the results
Once and rrl.cdated is null and after that and rrl.cdated is not null

In a query (no editing of tables) how do I join data without any similarities?

I Have a query that finds a table, here's an example one.
Name |Age |Hair |Happy | Sad |
Jon | 15 | Black |NULL | NULL|
Kyle | 18 |Blonde |YES |NULL |
Brad | 17 | Blue |NULL |YES |
Name and age come from one table in a database, hair color comes from a second which is joined, and happy and sad come from a third table.My goal would be to make the first line of the chart like this:
Name |Age |Hair |Happy |Sad |
Jon | 15 |Black |Yes |Yes |
Basically I want to get rid of the rows under the first and get the non NULL data joined to the right. The problem is that there is no column where the Yes values are on the Jon row, so I have no idea how to get them there. Any suggestions?
PS. With the data I am using I can't just put a 'YES' in the 'Jon' row and call it a day, I would need to find the specific value from the lower rows and somehow get that value in the boxes that are NULL.
Do you just want COALESCE()?
COALESCE(Happy, 'Yes') as happy
COALESCE() replaces a NULL value with another value.
If you want to join on a NULL value work with nested selects. The inner select gets an Id for NULLs, the outer select joins
select COALESCE(x.Happy, yn_table.description) as happy, ...
from
(select
t1.Happy,
CASE WHEN t1.Happy is null THEN 1 END as happy_id
from t1 ...) x
left join yn_table
on x.xhappy_id = yn_table.id
If you apply an ORDER BY to the query, you can then select the first row relative to this order with WHERE rownum = 1. If you don't apply an ORDER BY, then the order is random.
After reading your new comment...
the sense is that in my real data the yes under the other names will be a number of a piece of equipment. I want the numbers of the equipment in one row instead of having like 8 rows with only 4 ' yes' values and the rest null.
... I come to the conclusion that this a XY problem.
You are asking about a detail you think will solve your problem, instead of explaining the problem and asking how to solve it.
If you want to store several pieces of equipment per person, you need three tables.
You need a Person table, an Article table and a junction table relating articles to persons to equip them. Let's call this table Equipment.
Person
------
PersonId (Primary Key)
Name
optional attributes like age, hair color
Article
-------
ArticleId (Primary Key)
Description
optional attributes like weight, color etc.
Equipment
---------
PersonId (Primary Key, Foreign Key to table Person)
ArticleId (Primary Key, Foreign Key to table Article)
Quantity (optional, if each person can have only one of each article, we don't need this)
Let's say we have
Person: PersonId | Name
1 | Jon
2 | Kyle
3 | Brad
Article: ArticleId | Description
1 | Hat
2 | Bottle
3 | Bag
4 | Camera
5 | Shoes
Equipment: PersonId | ArticleId | Quantity
1 | 1 | 1
1 | 4 | 1
1 | 5 | 1
2 | 3 | 2
2 | 4 | 1
Now Jon has a hat, a camera and shoes. Kyle has 2 bags and one camera. Brad has nothing.
You can query the persons and their equipment like this
SELECT
p.PersonId, p.Name, a.ArticleId, a.Description AS Equipment, e.Quantity
FROM
Person p
LEFT JOIN Equipment e
ON p.PersonId = e.PersonId
LEFT JOIN Article a
ON e.ArticleId = a.ArticleId
ORDER BY p.Name, a.Description
The result will be
PersonId | Name | ArticleId | Equipment | Quantity
---------+------+-----------+-----------+---------
3 | Brad | NULL | NULL | NULL
1 | Jon | 4 | Camera | 1
1 | Jon | 1 | Hat | 1
1 | Jon | 5 | Shoes | 1
2 | Kyle | 3 | Bag | 2
2 | Kyle | 4 | Camera | 1
See example: http://sqlfiddle.com/#!4/7e05d/2/0
Since you tagged the question with the oracle tag, you could just use NVL(), which allows you to specify a value that would replace a NULL value in the column you select from.
Assuming that you want the 1st row because it contains the smallest age:
- wrap your query inside a CTE
- in another CTE get the 1st row of the query
- in another CTE get the max values of Happy and Sad of your query (for your sample data they both are 'YES')
- cross join the last 2 CTEs.
with
cte as (
<your query here>
),
firstrow as (
select name, age, hair from cte
order by age
fetch first row only
),
maxs as (
select max(happy) happy, max(sad) sad
from cte
)
select f.*, m.*
from firstrow f cross join maxs m
You can try this:
SELECT A.Name,
A.Age,
B.Hair,
C.Happy,
C.Sad
FROM A
INNER JOIN B
ON A.Name = B.Name
INNER JOIN C
ON A.Name = B.Name
(Assuming that Name is the key columns in the 3 tables)

SQL : select rows with value in some column and the value does is not contained in another table

There are two tables
Students:
ID | Name | Age
---+------+-----
1 | Alex | 19
2 | Matt | 23
3 | Ali | 19
Actions:
ACTIONID | Description
---------+---------------------------
1 | Alex hasn't paid yet
I want to select Student ID with age 19. But don't want to select student whose name is contained in any row of description column from the Actions table.
So result should be the following
ID: 3
How to do that? Could anybody help me?
Try this:
SELECT student.ID
FROM Students student
WHERE 0 = (SELECT COUNT(*)
FROM Actions action
WHERE action.Description LIKE CONCAT('%', student.Name, '%')
AND student.Age = 19;
If I understood your question.
select ID from Students where Age = 19

How to combine in one sql query in extra column the result of 2 group by queries?

Considering the following mdl_course_completions table that describes a course completion for a user:
id,bigint
userid,bigint
course,bigint
timeenrolled,bigint
timestarted,bigint
timecompleted,bigint
reaggregate,bigint
To determinate if a course has been finished by a student, I use a predicate on the timecompleted field.
When this field is null, the student has not finished the course, but when this field is not null, that means the student has finished the course.
Thus, the count of the number of students that finished course by course is given by:
SELECT mdl_course.fullname,count(*) as "number of students that didn't finish courses"
FROM mdl_course_completions
INNER JOIN mdl_course on mdl_course.id = mdl_course_completions.course
WHERE timecompleted IS NOT NULL
GROUP BY mdl_course.fullname
;
the result is:
| course name | number of students that finish courses |
|-------------|----------------------------------------|
| course 1 | 50 |
| course 2 | 200 |
| course 3 | 120 |
AND the count of the number of students that DIDN'T finished course by course is given by:
SELECT mdl_course.fullname,count(*) as "number student that didn't finish courses"
FROM mdl_course_completions
INNER JOIN mdl_course on mdl_course.id = mdl_course_completions.course
WHERE timecompleted IS NULL
GROUP BY mdl_course.fullname
;
the result is:
| course name | number of students that didn't finish courses |
|-------------|-----------------------------------------------|
| course 1 | 12 |
| course 2 | 12 |
| course 3 | 120 |
I wonder how can I combine this 2 queries to get in one query the results in an extra column such as:
| course name | number of students that finish courses | number of students that didn't finish courses |
|-------------|------------------------------------|-------------------------------------------|
| course 1 | 50 | 12 |
| course 2 | 200 | 12 |
| course 3 | 120 | 120 |
I am using postgresql.In my opinion, this kind of stuff is not related to database system. I just don't know how to proceed to combine these 2 queries in one in an extra column with the GROUP BY clause.
Use conditional aggregation.
SELECT mdl_course.fullname
,SUM((timecompleted IS NOT NULL)::int) as "number student that finish courses"
,SUM((timecompleted IS NULL)::int) as "number student that didn't finish courses"
FROM mdl_course_completions
INNER JOIN mdl_course on mdl_course.id = mdl_course_completions.course
GROUP BY mdl_course.fullname
From PostgreSQL 9.4 on, you can use the FILTER clause with aggregate functions:
count(*) FILTER (WHERE timecompleted IS NOT NULL)

Using multiple left joins to calculate averages and counts

I am trying to figure out how to use multiple left outer joins to calculate average scores and number of cards. I have the following schema and test data. Each deck has 0 or more scores and 0 or more cards. I need to calculate an average score and card count for each deck. I'm using mysql for convenience, I eventually want this to run on sqlite on an Android phone.
mysql> select * from deck;
+----+-------+
| id | name |
+----+-------+
| 1 | one |
| 2 | two |
| 3 | three |
+----+-------+
mysql> select * from score;
+---------+-------+---------------------+--------+
| scoreId | value | date | deckId |
+---------+-------+---------------------+--------+
| 1 | 6.58 | 2009-10-05 20:54:52 | 1 |
| 2 | 7 | 2009-10-05 20:54:58 | 1 |
| 3 | 4.67 | 2009-10-05 20:55:04 | 1 |
| 4 | 7 | 2009-10-05 20:57:38 | 2 |
| 5 | 7 | 2009-10-05 20:57:41 | 2 |
+---------+-------+---------------------+--------+
mysql> select * from card;
+--------+-------+------+--------+
| cardId | front | back | deckId |
+--------+-------+------+--------+
| 1 | fron | back | 2 |
| 2 | fron | back | 1 |
| 3 | f1 | b2 | 1 |
+--------+-------+------+--------+
I run the following query...
mysql> select deck.name, sum(score.value)/count(score.value) "Ave",
-> count(card.front) "Count"
-> from deck
-> left outer join score on deck.id=score.deckId
-> left outer join card on deck.id=card.deckId
-> group by deck.id;
+-------+-----------------+-------+
| name | Ave | Count |
+-------+-----------------+-------+
| one | 6.0833333333333 | 6 |
| two | 7 | 2 |
| three | NULL | 0 |
+-------+-----------------+-------+
... and I get the right answer for the average, but the wrong answer for the number of cards. Can someone tell me what I am doing wrong before I pull my hair out?
Thanks!
John
It's running what you're asking--it's joining card 2 and 3 to scores 1, 2, and 3--creating a count of 6 (2 * 3). In card 1's case, it joins to scores 4 and 5, creating a count of 2 (1 * 2).
If you just want a count of cards, like you're currently doing, COUNT(Distinct Card.CardId).
select deck.name, coalesce(x.ave,0) as ave, count(card.*) as count -- card.* makes the intent more clear, i.e. to counting card itself, not the field. but do not do count(*), will make the result wrong
from deck
left join -- flatten the average result rows first
(
select deckId,sum(value)/count(*) as ave -- count the number of rows, not count the column name value. intent is more clear
from score
group by deckId
) as x on x.deckId = deck.id
left outer join card on card.deckId = deck.id -- then join the flattened results to cards
group by deck.id, x.ave, deck.name
order by deck.id
[EDIT]
sql has built-in average function, just use this:
select deckId, avg(value) as ave
from score
group by deckId
What's going wrong is that you're creating a Cartesian product between score and card.
Here's how it works: when you join deck to score, you may have multiple rows match. Then each of these multiple rows is joined to all of the matching rows in card. There's no condition preventing that from happening, and the default join behavior when no condition restricts it is to join all rows in one table to all rows in another table.
To see it in action, try this query, without the group by:
select *
from deck
left outer join score on deck.id=score.deckId
left outer join card on deck.id=card.deckId;
You'll see a lot of repeated data in the columns that come from score and card. When you calculate the AVG() over data that has repeats in it, the redundant values magically disappear (as long as the values are repeated uniformly). But when you COUNT() or SUM() them, the totals are way off.
There may be remedies for inadvertent Cartesian products. In your case, you can use COUNT(DISTINCT) to compensate:
select deck.name, avg(score.value) "Ave", count(DISTINCT card.front) "Count"
from deck
left outer join score on deck.id=score.deckId
left outer join card on deck.id=card.deckId
group by deck.id;
This solution doesn't solve all cases of inadvertent Cartesian products. The more general-purpose solution is to break it up into two separate queries:
select deck.name, avg(score.value) "Ave"
from deck
left outer join score on deck.id=score.deckId
group by deck.id;
select deck.name, count(card.front) "Count"
from deck
left outer join card on deck.id=card.deckId
group by deck.id;
Not every task in database programming must be done in a single query. It can even be more efficient (as well as simpler, easier to modify, and less error-prone) to use individual queries when you need multiple statistics.
Using left joins isn't a good approach, in my opinion. Here's a standard SQL query for the result you want.
select
name,
(select avg(value) from score where score.deckId = deck.id) as Ave,
(select count(*) from card where card.deckId = deck.id) as "Count"
from deck;