Selecting the latest row using groupby in Laravel - php

I have a table entries similar as follows:
+---------+---------+----------+
| Test_id | User_id | Attempts |
+---------+---------+----------+
| 12 | 5 | 1 |
| 13 | 5 | 1 |
| 12 | 5 | 2 |
+---------+---------+----------+
Now I want to select the elements group by test_id and should get the latest entry.
I tried this query:
$tests_took = Testresult::where('course_id', $courseId)
->where('user_id', Auth::id())
->groupby('test_id')
->orderBy('attempts', 'desc')
->get();
When I display the result, I'm getting the first two rows only (only one row for one test_id - which I what I want.) But instead of the last row for the test_id=12, it shows the first row. I always want the biggest attempt to be displayed.
My current output is like:
| 12 | 5 | 1 |
| 13 | 5 | 1 |
But I want this:
| 12 | 5 | 2 |
| 13 | 5 | 1 |
How can I achieve this? to get the latest row as the first array element when I use groupby or is there any other way to do this?

ORDER BY and GROUP BY don't work very well together...
If you simply want the highest attempt per test_id i suggest using the MAX() function:
$tests_took = Testresult::select('test_id', 'user_id', DB::raw('MAX(attempts) AS max_attempts'))
->where('course_id', $courseId)
->where('user_id', Auth::id())
->groupby('test_id')
->get();

You may use the following lines:
Testresult::select('*')
->join(DB::raw('(Select max(id) as last_id from Testresult group by test_id) LatestId'), function($join) {
$join->on('Testresult.id', '=', 'LatestId.last_id');
})
->orderBy('attempts', 'desc');
}

Related

Using Multidimensional array in Laravel 8 Blade

I am trying to get a status count of some processes and tasks just like a to-do list tasks count.
My current database look like this SQL Database Structure
+----+---------+------------+-------------+---------+------------+---------------------+---------------------+
| id | user_id | list_id | name | records | status | created_at | updated_at |
+----+---------+------------+-------------+---------+------------+---------------------+---------------------+
| 1 | 1 | 8Kwvf8ikcV | 10 mails | 19 | On-Hold | 2021-03-11 07:56:17 | 2021-03-11 07:56:17 |
| 2 | 1 | a0pJRv4Zfc | temp_emails | 884 | On-Hold | 2021-03-11 08:02:13 | 2021-03-11 08:02:13 |
| 3 | 1 | xrgZZkrLFA | 10 mails | 19 | Processing | 2021-03-11 08:06:37 | 2021-03-11 08:06:37 |
| 4 | 1 | lDOX8p2sgU | 10 mails | 19 | Completed | 2021-03-11 08:53:51 | 2021-03-11 08:53:51 |
+----+---------+------------+-------------+---------+------------+---------------------+---------------------+
I am using
return $listStats = ListName::select([
DB::raw("status"),
DB::raw("COUNT(status) as count")
])->where('user_id', Auth::id())
->groupBy('status')
->get();
Query to get the database and its count.
Current laravel out put is showing as
[{"status":"Completed","count":1},{"status":"On-Hold","count":2},{"status":"Processing","count":1}]
My question is how can I get the count of completed status without using foreach loop?
Do I need to change my query? or can I simply use $listStats['Completed']['count'] ?
A foreach will be run at some point unless you use a join in the query to give an order and force a result with all possible status.
One easy solution would be
$StatCount[ //make sure to list all possible statuses here
"Completed" => 0,
"Processing" => 0,
"On-Hold" => 0,
];
foreach ($listStats as $listStat) {
$StatCount[$listStat->status] = $listStat->count
}
Now you can access your status counts like this $statCount['Completed']
Counting is a pretty lightweight job and it can be done on the fly easily. Instead of creating an extra query to count the records, collect the records and count them in Blade. It's a better idea because you will fetch the tasks anyway and no need to query them twice.
// In controller
$user = User::where('id', Auth::id())->with(['tasks' => function($query) {
$query->groupBy('status')
}])->get();
return view('viewName')->with(['user', $user]);
// In Blade
$user->tasks->Completed->count();

Combine whereRaw() with leftjoin() using eloquent

I am trying to join 2 tables and get the latest unique results using whereRaw() and leftJoin() with Laravel eloquent.
I have 2 tables:-
skills table (has timestamps):-
| id| name | icon |
| 1 | skill 1 | skill1.png |
| 2 | skill 2 | skill2.png |
| 3 | skill 3 | skill3.png |
scores table (has timestamps):-
| id| player_id | skill_id | score |
| 1 | 1 | 1 | 1 |
| 2 | 1 | 2 | 1 |
| 3 | 1 | 3 | 1 |
| 4 | 1 | 2 | 2 |
I would like to return all skills but only the latest entry(by id), so for the above snippet I should get:-
| id| player_id | name | skill_id | score |
| 1 | 1 | skill 1 | 1 | 1 |
| 3 | 1 | skill 3 | 1 | 1 |
| 4 | 1 | skill 2 | 2 | 2 |
I can get the latest unique records by using:
return SkillScores::where('player_id', $this->id)
->whereRaw('id in (select max(id) from skills group by (name))')
->get();
I can get the skill names by using:-
return SkillScores::where('player_id', $this->id)
->leftJoin('skills', 'skill_scores.skill_id', '=', 'skills.id')
->get();
but when I combine them I get an SQLSTATE[23000] error
return SkillScores::where('player_id', $this->id)
->whereRaw('id in (select max(id) from skills group by (name))')
->leftJoin('skills', 'skill_scores.skill_id', '=', 'skills.id')
->get();
Can anyone help me figure out what is going wrong?
EDIT:-
It turns out that the SQLSTATE[23000] error is occuring because I have an id column in both tables and I havent told it which one I am referencing, the below fixes the issue and gives me the correct result.
return SkillScores::where('player_id', $this->id)
->whereRaw('skill_scores.id in (select max(skill_scores.id) from skill_scores group by (skill_id))')
->leftJoin('skills', 'skill_scores.skill_id', '=', 'skills.id')
->get();
I think there is a minor problem on your expected result(id and name is not matching) but i made it work as following;
The query;
SELECT scores.*, skills.*
FROM scores
INNER JOIN (SELECT skill_id, max(id) AS maxId
FROM scores
WHERE player_id = 1
GROUP BY skill_id) AS sub ON sub.maxId = scores.id
INNER JOIN skills ON skills.id = scores.skill_id;
The eloquent version (You may replace it with DB::table() if you want)
$subQuery = Score::where('player_id', DB::raw($this->id))
->groupBy('skill_id')
->select('skill_id', DB::raw('MAX(id) as maxId'));
return Score::join(DB::raw('(' . $subQuery->toSql() . ') as subQuery'), 'subQuery.maxId', '=', 'scores.id')
->join('skills', 'skills.id', '=', 'scores.skill_id')
->get(['scores.*', 'skills.*']);

Getting result from array of Ids in Laravel

I've a following attendance table:
id | grade_id | subject_id | date | students
1 | 2 | 6 | 2020-05-05 | [3,6,8,11,17,21,20,19]
I want to fetch all rows with name of all students from array of Ids.
What I've tried is:
$result[] = Attendance::where('date', '=', $day)
->with('grades', 'subjects')
->join('students', function($join) {
$join->on('students.id', '=', 'attendances.id')
->where('students.id', '=', 'attendances.students');
})->get();
But couldn't get result. Please help me!
It is not a proper table structure.Will be better if you make another table like
id | attendance_id | student_id
1 | 1 | 3
1 | 1 | 6
And make its relation in model with attendance and student table.This way table will be normalized and will be easy to handle relationship.

Laravel Implement greatest-n-per-group

User table:
| id | name | age |
|----|------------|-----|
| 1 | Apple | 22 |
| 2 | Strawberry | 23 |
| 3 | Orange | 50 |
| 4 | Mango | 30 |
Memberships table:
| id | user_id | expire_at |
|----|---------|----------------------|
| 1 | 1 | 2019-08-17T11:19:30Z |
| 2 | 1 | 2019-08-10T11:20:10Z |
| 3 | 2 | 2019-08-29T11:20:19Z |
| 4 | 3 | 2019-08-02T11:20:30Z |
| 5 | 3 | 2019-08-28T11:20:40Z |
Problom
I want select users with the latest 'expire_at'.
After reference: https://stackoverflow.com/a/2111420/5588637,
I tried the following:
SELECT
u.*,
m1.*
FROM
users u
INNER JOIN memberships m1 ON u.id = m1.user_id
LEFT JOIN memberships m2 ON u.id = m2.user_id
AND (
m1.expire_at < m2.expire_at
OR m1.expire_at = m2.expire_at
AND m1.id < m2.id
)
WHERE
m2.id IS NULL;
Result
The id will appear twice because I used to join.
| id | name | age | id | user_id | expire_at |
|----|------------|-----|----|---------|----------------------|
| 1 | Apple | 22 | 1 | 1 | 2019-08-17T11:19:30Z |
| 2 | Strawberry | 23 | 3 | 2 | 2019-08-29T11:20:19Z |
| 3 | Orange | 50 | 5 | 3 | 2019-08-28T11:20:40Z |
After change m1.* to m1.expire_at. I got the result I want.
| id | name | age | expire_at |
|----|------------|-----|----------------------|
| 1 | Apple | 22 | 2019-08-17T11:19:30Z|
| 2 | Strawberry | 23 | 2019-08-29T11:20:19Z |
| 3 | Orange | 50 | 2019-08-28T11:20:40Z |
online try: http://sqlfiddle.com/#!9/27fa22/4
Implement in Lavavel
Laravel Framework version: 5.6.39
I am trying to convert the above SQL into Laravel using Database: Query Builder.
$users = DB::table('users as u')
->select('u.*', 'm1.*')
->join('memberships as m1','u.id','=','m1.user_id')
->leftJoin('memberships as m2', function($join){
$join->on('u.id', '=', 'm2.user_id')
->where(function ($query) {
$query->where('m1.expire_at','<','m2.expire_at')
->orWhere('m1.expire_at','=','m2.expire_at')
->where('m1.id','<','m2.id');
});
})
->whereNull('m2.id')
->toSQL();
I'm using toSql(). This will convert it to SQL first to make sure it's same of above SQL.
SELECT
`u`.*,
`m1`.*
FROM
`users` AS `u`
INNER JOIN `memberships` AS `m1` ON `u`.`id` = `m1`.`user_id`
LEFT JOIN `memberships` AS `m2` ON `u`.`id` = `m2`.`user_id`
AND (
`m1`.`expire_at` < ?
OR `m1`.`expire_at` = ?
AND `m1`.`id` < ?
)
WHERE
`m2`.`id` IS NULL
? seems to be the characteristic of laravel, I believe it is same of above SQL.
when i change toSQL() to get(), the result following:
Collection { ▼
#items: []
}
The above result is wrong, so i tried remove
whereNull('m2.id') in Laravel code (WHERE m2.id IS NULL in SQL), let’s see what happened.
Laravel result
Collection { ▼
#items: array:5 [▼
0 => { ▼
+"id": 1
+"name": "Apple"
+"age": "Eric Yiu SL"
+"user_id": 1
+"expire_at": "2019-08-10T11:20:10Z"
}
...
]
Ideal result
| id | name | age | id | user_id | expire_at |
|----|------------|-----|----|---------|----------------------|
| 1 | Apple | 22 | 2 | 1 | 2019-08-10T11:20:10Z |
| 3 | Orange | 50 | 4 | 3 | 2019-08-02T11:20:30Z |
| 1 | Apple | 22 | 1 | 1 | 2019-08-17T11:19:30Z |
| 2 | Strawberry | 23 | 3 | 2 | 2019-08-29T11:20:19Z |
| 3 | Orange | 50 | 5 | 3 | 2019-08-28T11:20:40Z |
Comparing results, Laravel result missing second id which is memberships table id, i guess this is the reason of incorrect results.
I have searched the Internet, seems is this problem.
https://github.com/laravel/framework/issues/4962
But I failed after various attempts...
You cannot select two rows with the same name in Laravel. The second one will override the first one. Use an alias instead.
$users = DB::table('users as u')
->select('u.*', 'm1.id as membership_id')
->join('memberships as m1','u.id','=','m1.user_id')
->leftJoin('memberships as m2', function($join){
$join->on('u.id', '=', 'm2.user_id')
->where(function ($query) {
$query->whereColumn('m1.expire_at','<','m2.expire_at')
->orWhere(function ($query) {
$query->whereColumn('m1.expire_at','=','m2.expire_at')
->whereColumn('m1.id','<','m2.id');
});
});
})
->whereNull('m2.id')
->get();
Note: I also encapsulated the orWhere() in the join to avoid confusion about the order of AND/OR.
What also works is using a different order in the select. You can for example use the following:
$query->select([
'm1.*',
'm1.id as membership_id',
'u.*'
])
It will return all columns of both tables plus the new membership_id column. But if there is a column on the users table which is named similarly to a column on the memberships table, only the users table column is returned (e.g. created_at). What comes last in your list is returned.
EDIT:
As #Namoshek mentioned, you should not select everything because you have a duplicate key problem in your SQL query. I modified my answer so that it would match #RaymondNijland answer. And by the way, even for the table user, you should select exactly what you need. And not only for a duplicate key problem but also for the speed of your SQL query. We don't think about it enough but it can quickly make the difference on a big set of results.
Less data to send from the database to your PHP server = faster
You should try this one :
DB::table('users as u')
->select('u.*', 'm1.id as membership_id')
->join('memberships as m1','u.id','=','m1.user_id')
->leftJoin('memberships as m2', function ($join) {
$join->on('u.id', '=', 'm2.user_id')
->on(function($join) {
$join->on('m1.id', '<', 'm2.id')
->on(function($join) {
$join->on('m1.expire_at', '<', 'm2.expire_at')
->orOn('m1.expire_at', '=', 'm2.expire_at');
});
});
})
->whereNull('m2.id')
->toSQL()
As mentioned in Laravel's documentation on this page: https://laravel.com/api/5.8/Illuminate/Database/Query/JoinClause.html#method_on
You can pass a closure to the on() method and there is the orOn() method that you can use in this closure.
I tested it and it gives the same result as your SQL query.

Laravel 5.7 - multiply two columns and add to final sum

I have two tables - a students table and a products table.
When i make a list of the students in a table, i need to see the total amount (sum) of payments that has been made, unfortunately it seems like the result is the correct sum but multiplied by the amount of rows.
Students table:
+----+----------+
| id | name |
+----+----------+
| 1 | Jonathan |
| 2 | Bob |
+----+----------+
Products table:
+----+------------+-------+----------+
| id | student_id | money | quantity |
+----+------------+-------+----------+
| 1 | 1 | 1000 | 2 |
| 2 | 1 | 2000 | 1 |
| 3 | 2 | 500 | 5 |
| 4 | 2 | 3000 | 1 |
+----+------------+-------+----------+
Payments table:
+----+-------+------------+
| id | money | student_id |
+----+-------+------------+
| 1 | 5000 | 1 |
| 2 | 2000 | 1 |
| 3 | 2500 | 2 |
| 4 | 2500 | 2 |
+----+-------+------------+
In theory, the output of my query should be:
+-------------+----------+----------------+----------------+
| id | name | payments_total | products_total |
+-------------+----------+----------------+----------------+
| 1 | Jonathan | 4000 | 7000 |
| 2 | Bob | 5500 | 10000 |
+-------------+----------+----------------+----------------+
What i have tried:
$teamStudents = DB::table('students')->where('students.team', $team)->orderBy('first_name', 'ASC')
->join('products', 'students.id', '=', 'products.student_id')
->join('payments', 'students.id', '=', 'payments.student_id')
->select('students.first_name AS first_name', 'students.last_name AS last_name', 'students.created_at AS created_at', DB::raw('SUM(products.money * products.amount) AS products_total'), DB::raw('SUM(payments.money) AS payments_total'), 'students.id AS id')
->groupBy('students.id')
->get();
It returns no error except for the fact that the returned "payments_total" is inaccurate and multiplied by the amount of rows for some reason.
So my question is:
How do i get around this and what have i done wrong? I've been googling for an hour with no result.
Is my query an issue or the way i've set it up, if so, what would the correct solution be?
With your edit I was able to solve the problem that you have, but in your edit you use couple of things for which I don't have data, such as the $team, first_name and last_name of the students. But anyway, here is a solution for your problem, you have to use subqueries in order to solve this:
$teamStudents = DB::table('students')->orderBy('name', 'ASC')
->leftJoin(DB::raw('(select SUM(products.money * products.quantity) AS products_total, student_id from products group by student_id) products'), 'students.id', '=', 'products.student_id')
->leftJoin(DB::raw('(select sum(payments.money) as payments_total, student_id from payments group by student_id) payments'), 'students.id', '=',
'payments.student_id')
->select('students.name', 'payments.payments_total', 'products.products_total', 'students.id AS id')
->groupBy('students.id')
->get();
I am not sure if technically I will be correct, but the problem is because you use multiple joins, so that's why the results are doubled, if you don't use subqueries.
There's no need to join in this case, you don't use it anyways.
$teamStudents = DB::table('students')
->select('id, name')
->selectRaw('select sum(money) from payments where student_id = students.id as payments_total')
->selectRaw('select sum(money) from products where student_id = students.id as products_total')
->orderBy('name')
->get();

Categories