Count rows of each status - Laravel eloquent - php

There is a table named requests with a column named status and I have access to this table through a HasManyThrough relation.
The table looks like this:
id | status | request
--------------------------
1 | 1 | lorem ipsum
2 | 2 | lorem ipsum
3 | 1 | lorem ipsum
4 | 3 | lorem ipsum
5 | 1 | lorem ipsum
I need to count all different status rows in an efficient way, the result I'm looking for is something like this:
status | status_count
---------------------
1 | 3
2 | 1
3 | 1
Also I need to be able to do something like this:
$status[1] Or $status->id[1] // which prints out the count of that status
I Know this two option which both of them are ugly:
1. Using filters:
// Through a table named group which contains year column
$requests = Auth::user()->requests()->where('year', 2016)->get();
$status[1] = $requests->filter(
function($request){
return $request->status == 1;
})->count();
Now I have 10 status id, and I should repeat this code 10 times, what if i had 100 status... so it's not a good option.
2. Trying to create a right SQL:
As I know the correct SQL statement for doing this looks like this:
SELECT 'status', COUNT(*) FROM requests GROUP BY status
For creating this statement I'm doing this:
$groups = Group::where(['owner_id' => Auth::user()->id, 'year' => 2016])
->lists('id');
$requests = DB::table('requests')
->select( DB::raw('count(status) as status_count, status') )
->whereIn('group_id', $groups)
->groupBy('status')
->get();
And here is my dd output:
array:2 [▼
0 => {
+"status_count": "3"
+"status": "1"
}
1 => {
+"status_count": "1"
+"status": "2"
}
2 => {
+"status_count": "1"
+"status": "3"
}
]
Which I have no idea how can I use them, for example how can I know what is the count of status 2? I can use a foreach to create an array with status as a key and status_counts as a value but I'm looking fro a right way to do this.
Another thing is how can I create this SQL statement through eloquent and not DB?
Is there any other way that I'm missing?

Okay, here is what I've did:
First I managed to create a correct SQL only with eloquent and getting
the statuses and their count only by 1 request.
Here is the hasManyThrough relation:
public function requests()
{
return $this->hasManyThrough('App\Models\Request',
'App\Models\Group', 'owner_id', 'group_id');
}
Then to get the count of desired requests statuses:
$requests = Auth::user()->requests()->
select( DB::raw('count(requests.status) as count, requests.status') )->
groupBy('requests.status')->where('year', '2016')->get();
Again our result is exactly as we wanted:
status | status_count
---------------------
1 | 3
2 | 1
3 | 1
This time we have two option to work with counts, I go with the new one first:
1. Using where:
// returns null if there is no row with status of 1
$status = $projects->where('status_id', "1")->first();
// if $status is null there is no row with status of 1 so return 0
echo ( empty($status) ) ? 0 : $status->count;
Easy right? but it's not as clean as we want.
2. The foreach trick:
As I mentioned it in my question we can use a foreach to create an array of statuses and their count. but whats happens when we try to get the count of an status which does not exist in the result of our query? we are going to get an error: Undefined offset.
And here is my Idea:
First we will create an array to keep all status codes and their count then we
fill this array with zero.
My system only has 10 status code so:
$requests_counts = array_fill(1, 10, 0); // status 1 to 10 - default count: 0
Then with a foreach statement we only override the indexs that have a count in our query result:
foreach($requests as $request)
$requests_counts[$request->status] = $request->count;
So all status codes going to have a value even the ones that are not exist in
our query result.
*Count of all status codes
But what if we want to print out the number of all requests no matter what status code they have? should we create an other query and count all the results rows? nope we are going use a function named array_sum:
array_sum($requests_counts); // prints out number of all requests

$groups = Group::where(['owner_id' => Auth::user()->id, 'year' => 2016])
->groupBy('status')
->select( DB::raw('status , COUNT(*) as status_count') )
->get();

I would stick to your second option with little tweak.
$groups = Group::where(['owner_id' => Auth::user()->id, 'year' => 2016])
->lists('id');
$requests = DB::table('requests')
->select( DB::raw('count(status) as status_count, status') )
->whereIn('group_id', $groups)
->groupBy('status')
->get();
$output = [];
foreach ($requests as $a)
{
$output[$a->status] = $a->status_count;
}
Then in your ouput you have $output[1] count for status 1 etc...

Related

SQL Filter by List of ids

I have three tables
1-rests
2-amenity_rest
3-amenities
rests
id name
1 rest1
2 rest2
3 rest3
amenities
id name
1 amenity1
2 amenity2
3 amenity3
amenity_rest
rest_id amenity_id
1 1
1 3
2 2
3 1
I would like to send a List of amenities like [1,3]
and it should return the rest that its id=1
and if I send array [1,2,3] it should return no result
How would such a query look alike?
You can construct the query as:
select ar.rest_id
from amenity_rest ar
where ar.amenity_id in (1, 2, 3)
group by ar.rest_id
having count(*) = 3; -- "3" = size of list
Your controller should have a function like this:
function retrieveData(Request $request){
$data = Amenities_rest::whereIn('amenity_id', $request->amenities)
->groupBy('rest_id')
->havingRaw('COUNT (*) = ' . count($request->amenities))
->get();
return $data
}
That should do it. I hope it helps.

Count does not work as expected in Laravel Query Builder

I am trying to get how many names do I have in database. For this purpose I am using Query Builder like this:
$namesIdsCount = DB::table('names_to_options')
->select('name_id')
->groupBy('name_id')
->havingRaw($having)
->count();
Is says that 24, which is not correct, because if I will write code like this:
$namesIdsCount = DB::table('names_to_options')
->select('name_id')
->groupBy('name_id')
->havingRaw($having)
->get();
result object contains 247 elements, which is correct. I have tried to play with skip/take, but still no results. Where am I wrong? Thanks for any help.
I think it's the other way around, you're not getting 24 groups. You're getting 24 elements within the first group. That configuration results in the following query:
SELECT
COUNT(*) AS 'aggregate',
`name_id`
FROM `names_to_options`
WHERE EXISTS(
{your $havingRaw sub-query}
)
GROUP BY `name_id`;
What you end up with will look something like this:
+---------------+---------+
| aggregate | name_id |
+---------------+---------+
| 24 | 1 |
+---------------+---------+
| 5 | 2 |
+---------------+---------+
| 30 | 3 |
+---------------+---------+
| ... and so on | 4 |
+---------------+---------+
Query\Builder just doesn't realize you can get more than one result back when count() is involved.
You were pretty close to the right answer yourself though.
$namesIdsCount = DB::table('names_to_options')
->select('name_id')
->groupBy('name_id')
->havingRaw($having)
->get();
get() returns an Eloquent\Collection, child of Support\Collection, which has its own version of the count method. So your answer is just:
$namesIdsCount = DB::table('names_to_options')
->select('name_id')
->groupBy('name_id')
->havingRaw($having)
->get()
->count();
If you really want this to happen in MySQL, the query you want to happen would look like this:
SELECT COUNT(*) FROM (
SELECT
`name_id`
FROM `names_to_options`
WHERE EXISTS(
{your $havingRaw sub-query}
)
GROUP BY `name_id`
) AS temp;
For that, you can do this:
$query = DB::table('names_to_options')
->select('name_id')
->groupBy('name_id')
->havingRaw($having);
$sql = $query->toSql();
$values = $query->getBindings();
$count = DB::table(DB::raw('('.$sql.') AS `temp`'))
->selectRaw("COUNT(*) AS 'aggregate'", $values)
->first()
->aggregate;
MySQL performance can get a little hairy when asking it to write temp-tables like that though, so you'll have to experiment to see which option is faster.
Inuyaki is right
(id, name_id),
(1,1),
(2,1),
(3,2),
(4,3)
There are are four rows so get() method will return 4 rows
but there are three groups if you use groupBy [name_id]
1 (1,1)
2 (2)
3 (3)
now count will return 3
hope this will help.

Laravel filter based on has one relationship

I am pulling my hair out over this one, and I feel like I have tried every method!
Here is my problem.
I have 2 tables
USERS
ID | FIRSTNAME | EMAIL_ADDRESS
1 | Joe Bloggs | joe#bloggs.com
STATUS
ID | USER_ID | STATUS | DATE
1 | 1 | 'In' | 2018-06-04 09:01:00
2 | 1 | 'Out' | 2018-06-04 09:00:00
As you can see by the tables above, each user can have many status', but each user has to have 1 most recent status, which I am doing like this (please tell me if I am doing it wrong)
public function statusCurrent(){
return $this->hasOne('App\Status', 'user_id', 'id')->orderBy('date', 'desc')->limit(1);
}
I then a form on in my view, which passes filters to the controller via a $request.
I need to be able to use the filters, and apply them to the 1 most recent status. For example, if someone searches for the date 2018-06-04 09:00:00 and a user id 1, I need it to show NO RESULTS, because the 1 most recent record for that user does not match that date, but at the moment, it will just jump over the one most recent if it doesn't match, and get the next record that does.
I have tried what seems like every method, I have tried like this
$users = Users::with(['StatusCurrent' => function($query){
$query->where('status.status', 'In');
}])->get();
Which gets the correct most recent row, but then if i try status.status, 'out' instead, it just jumps over and gets record number 2 where the status is out.
I've also tried like this
$users = Users::has('StatusCurrent')->paginate(10);
if(!empty($request->statusIn)){
$users = $users->filter(function ($item){
$item = $item->statusCurrent->status == 'in';
return $item;
});
}
return $users;
Which works great but then the pagination breaks when trying to append any GET parameters for the filters.
Plain English
I need to be able to get the most recent status for the user, then once I have it, I need to be able to apply where statements/filters/arguments to it, and if they don't match, completely ignore that user.
You have to combine a JOIN with a subquery:
$users = User::select('users.*')
->join('status', 'users.id', 'status.user_id')
->where('status.status', 'in')
->where('status.id', function($query) {
$query->select('id')
->from('status')
->whereColumn('user_id', 'users.id')
->orderByDesc('date')
->limit(1);
})
->get();
You can get the ID first, then do your query with filters:
$status_id = Users::find($user_id)->statusCurrent()->id;
Now do the actual query, using $status_id in whereHas clause:
$users = Users::with('statusCurrent')
->whereHas('statusCurrent', function($query) use($status_id) {
$query->where('status.status', 'In')
->where('id',$status_id);
})->get();
The relationship should be like:
public function statusCurrent(){
return $this->hasOne('App\Status', 'user_id', 'id')->latestOfMany();}

Multiple leftJoins using Laravel's Query Builder producing incorrect counts

I am using Laravel 5.4's Query Builder to perform a series of leftJoins on three tables. Here are my tables:
items
id type title visibility status created_at
-- ---- ----- ---------- ------ ----------
1 1 This is a Title 1 1 2017-06-20 06:39:20
2 1 Here's Another Item 1 1 2017-06-24 18:12:13
3 1 A Third Item 1 1 2017-06-26 10:10:34
count_loves
id items_id user_id
-- ------- -------
1 1 2
2 1 57
3 1 18
count_downloads
id items_id user_id
-- ------- -------
1 1 879
2 1 323
And here is the code I am running in Laravel:
$items_output = DB::table('items')
->leftJoin('count_loves', 'items.id', '=', 'count_loves.items_id')
->leftJoin('count_downloads', 'items.id', '=', 'count_downloads.items_id')
->where('items.visibility', '=', '1')
->where('items.status', '=', '1')
->orderBy('items.created_at', 'desc')
->select('items.*', DB::raw('count(count_loves.id) as loveCount'), DB::raw('count(count_downloads.id) as downloadCount'))
->groupBy('items.id')
->get();
When I return the results for this query, I am getting the following counts:
count_loves: 6
count_downloads: 6
As you can see, the actual count values should be:
count_loves: 3
count_downloads: 2
If I add another entry to the count_loves table, as an example, the totals move to 8. If I add another entry to the count_downloads table after that, the totals jump to 12. So, the two counts are multiplying together.
If I die and dump the query, here's what I get:
"query" => "select 'items'.*, count(count_loves.id) as loveCount,
count(count_downloads.id) as downloadCount from 'items' left join
'count_loves' on 'items'.'id' = 'count_loves'.'items_id' left join
'count_downloads' on 'items'.'id' = 'count_downloads'.'items_id'
where 'items'.'visibility' = ? and 'items'.'status' = ? group by
'items'.'id' order by 'items'.'created_at' desc"
How do I perform multiple leftJoins using Query Builder and count on several tables to return the proper sums?
NOTE:
This is intended as a HELP answer not the total absolute answer but I could not write the code in a comment. I am not asking for votes (for those who just can't wait to downvote me). I have created your tables and tried a UNION query on raw sql. I got correct results. I dont have laravel installed, but maybe you could try a UNION query in Laravel.
https://laravel.com/docs/5.4/queries#unions
select count(count_downloads.user_id)
from count_downloads
join items
on items.id = count_downloads.items_id
UNION
select count(count_loves.user_id)
from count_loves
join items
on items.id = count_loves.items_id

Yii2 MySQL Order By Relation Count

Table 1 - User:
ID Name
1 Jonh
2 Mark
3 King
Table 2 - Book:
ID user_idstatus ...
1 1 1 ...
2 1 1 ...
3 1 1 ...
4 2 1 ...
5 1 0 ...
6 1 0 ...
Code:
$query = User::find();
$query->joinWith('books');
$query->select(['user.*', 'COUNT(book.id) AS booksCount']);
$query->andWhere(['book.status' => 1]); // Problem Here!
$query->groupBy(['user.id']);
$query->orderBy(['booksCount' => SORT_DESC]);
Problem:
The query is working properly, but it's not returning the user with id = 3.
If I remove the line $query->andWhere(['book.status' => 1]); it works fine and return all users.
What should I change to list all users, even those who do not have a related book with status = 1?
I found the answer:
$query = User::find();
$query->joinWith(['books' => function ($subquery) {
$subquery->onCondition(['book.status' => 1]);
}]);
$query->select(['user.*', 'COUNT(book.id) AS booksCount']);
$query->groupBy(['user.id']);
$query->orderBy(['booksCount' => SORT_DESC]);
Instead of using COUNT(book.id), if the status of the book is either 0 or 1, you can use SUM(book.status) to get the number of books the user has. Then you can remove your WHERE book.status = 1 clause, and it will return all the users with the number of books they have, even in user 3's case where they have 0 books.
The Problem
The real problem is in your where clause. Because WHERE is processed before grouping and user 3 doesn't have any rows where book.status = 1, then the user has no rows which are included in the base query. Therefor the user isn't present during/after the grouping.
If you want a pretty good idea of a catch-all case where you can count rows based on a condition, using COUNT(CASE WHEN book.status IS NULL THEN NULL ELSE NULLIF(0,book.status) END) will also give you the result you're looking for. Because COUNT() will not count rows where the expression is NULL, this would allow the book.status to be -1, 1, 2, and any other number as long as it isn't 0 (or NULL in user 3's case), and still be included in the count.

Categories