How can I optimize the following query in laravel eloquent model? - laravel

I am trying to fetch some result from a table in a particular order.
I have a table, with few thousands questions. Each question have a category_id.
My task is, collect 100 questions from this table, in random order. Where first 30 questions will be from category_id =1, next 30 questions from category_id =2, and last 40 question from category_id=3.
My current solution is:
Question::inRandomOrder()->where('category_id',1)->limit(30)->get()
Question::inRandomOrder()->where('category_id',2)->limit(30)->get()
Question::inRandomOrder()->where('category_id',3)->limit(40)->get()
My question is, can I get same result with only one query?

With unions you could do the following.
$questions = Question::inRandomOrder()->where('category_id',1)->limit(30)
->union(Question::inRandomOrder()->where('category_id',2)->limit(30))
->union(Question::inRandomOrder()->where('category_id',3)->limit(40))
->get();
Technically a single query, but I'd rather have 3 queries.

you can use union
$q1 = Question::inRandomOrder()->where('category_id', 1)->limit(30);
$q2 = Question::inRandomOrder()->where('category_id', 2)->limit(30);
$value = Question::inRandomOrder()->where('category_id', 3)->limit(40)->union($q1)->union($q2)->get();

Related

How do I get both a COUNT() and SUM() LEFT JOIN in Laravel Eloquent?

I'm pretty new to joins so excuse me as I get my head round it! I'm trying to join 2 tables onto a links table.
One table is clicks. I want the SUM of any data in the clicks.clicks column where link_id matches. (ie 2 columns, One with 1 in the column and a second with 4 in the clicks column both for link_id=1 would return 5)
And the second table is suggestions which I want the COUNT of any occurrence of link_id to be displayed (ie 4 rows where link_id=1 would return 4)
I'm using eloquent with 2 left joins for this and have managed to get both working independently, however when I put both together, clicks_sum which is the name of my SUM join goes wildly high (Seeming as if the suggestion_count is interfering with what's going on there)
Here's my code so far for what I'm trying to achieve. I'm loading Link ID 2872719 just to test with. It should return 585 for suggestion_count and 4 for clicks_sum but I am getting totally different results than expected
use App\Models\Link;
use App\Models\Suggestion;
return Link::select(
'links.id',
DB::raw('SUM(clicks.clicks) AS click_sum'),
DB::raw('COUNT(suggestions.link_id) AS suggestion_count'),
)
->leftJoin('clicks', 'clicks.link_id', '=', 'links.id')
->leftJoin('suggestions', 'suggestions.link_id', '=', 'links.id')
->where('links.id', 2872719)
->first();
Returned is the following:
App\Models\Link {#1278
id: 2872719,
click_sum: "2340", // Should be 4
suggestion_count: 585, // The presence of this join appears to affect click_sum
}
Any ideas on where I am going wrong?
All my best!
Rather than having two leftJoin instances in your eloquent query you need to combine the two joins together into one leftJoin.
return Link::select(
'links.id',
// get the count of suggestions for each link
DB::raw('(SELECT COUNT(*) FROM suggestions WHERE suggestions.link_id = links.id) AS suggestion_count'),
DB::raw('SUM(clicks.clicks) AS click_sum'),
)
->leftJoin('clicks', 'clicks.link_id', '=', 'links.id', 'suggestions.link_id', '=', 'links.id')
->where('links.id', 2872719)
->groupBy('links.id')
->first();
This should give you the correct result as below:
#original: array:3 [▼
"id" => 2872719
"suggestion_count" => 586
"click_sum" => "4"
]
When performing two leftJoin as you were previously, the click_sum was being multiplied by the total count of suggestions leading to the inflated number being returned.

One eloquent query with whereIn clause with more than 3000 elements works but another one with the same elements and format doesn't

Hello and thank you beforehand for your help.
I've been hitting my head against a wall with this problem for a few days now so decided to ask here. I have two queries in Laravel, one grouping totals by week, and the other by month. The week one works fine but for some reason the month one doesn't, the only difference in essentially the query is that the weekly one is calculated yearly but in a different period (starting in week 48 of last year and ending in week 47 of this year), while the monthly is just the real year. The only other difference is that the week query is inside an if to show the right thata in those final weeks of the year.
$weeklySalesLastYear = Invoice::where(function ($query) use ($year, $client_ids){
$query->where('year', $year-2)->where('week', '>=', 48)->whereIn('client_id', $client_ids);
})->orWhere(function($query) use ($year, $client_ids){
$query->where('year', $year-1)->where('week', '<=', 47)->whereIn('client_id', $client_ids);
})->groupBy('week')->selectRaw('sum(total) as total, week')->get();
That is my weekly query which works perfectly.
$sortedMonthlySalesLastYear = DB::table('invoices')
->where('year', $year-1)->whereIn('client_id', $client_ids)
->groupBy('month')->selectRaw('sum(total) as total, month')->get();
And this is my monthly query which doesn't work. I know that there is an issue with whereIn clauses in eloquent where they don't accept a big number of elements for some reason, but I'm wondering why one works and not the other one and if there is a solution to it. I also want it to be an object, I've tried using a raw query but it throws an array instead, and I would rather avoid using that. This is the one that worked.
$sortedMonthlySalesLastYear = DB::select( DB::raw("SELECT SUM(total) AS total, month FROM invoices WHERE year = '$lastYear' AND client_id IN ($client_ids_query) GROUP BY month"))
Schema::create('invoices', function (Blueprint $table) {
$table->id();
$table->string('month');
$table->integer('year');
$table->integer('week');
$table->integer('client_id')->index()->unsigned();
$table->integer('product_id')->index()->unsigned();
$table->integer('quantity');
$table->float('total');
$table->double('discount');
});
This is what my invoices migration looks like, the client relates to the user and that's how I get the arrays.
This is what the monthly query returns:
[2022-05-02 23:40:05] local.INFO: monthly sales:
[2022-05-02 23:40:05] local.INFO: []
And this is what the weekly one returns (it's a larger set
but this is a sample of what it throws to show its working.)
[2022-05-02 23:42:42] local.INFO: weekly sales:
[2022-05-02 23:42:42] local.INFO:
[{"total":536190.4699999997,"week":1},
{"total":568192.6700000003,"week":2},
{"total":1613808.48,"week":3},
{"total":878447.3600000001,"week":4}...]
An example of a few invoices I'm trying to process is this (there are more than 130K invoices in the database):
I'd appreciate any help and if you have a solution to this, I mostly just prefer to stay using eloquent for the code to look cleaner. Thank you.
I also have to add that the query returns the expected values if I sign in with any other user since the range of clients they have is much smaller.
I figured it out after so long. The only thing I did was implode the client_ids collection and then explode it into an array. No idea why it does accept a big array and not a big collection, and still no idea about the discrepancy between the queries.
$clients = Client::where('user_id', $user_id)->get('id');
$imp = $clients->implode('id', ', ');
$client_ids = explode(', ', $imp);
All queries work with that.

Laravel get row with records above and below it

I have a Laravel 4.2 project where I get data from a SQL DB and I can display onto the page. I can select the single record just fine, but I want to also show the records around the one selected.
For example, I want to show the 5 records above and below the one selected. Im not sure how to do this in Laravel.
$gradschoolrange = MOGRadschool::where('Title', '=', $gradschool)->get();
In the above example $gradschool might be "Test College", it will return that with a value, but I want to show all the other related records around it with those values too. The results should look something like this:
ABC College
Another College
Blah College
Go To College
Test College
Yet Another College
Yo Yo College
College College
Something College
Eating College
As there's no ordering specified in your initial query, I'm assuming you want 5 next/previous records according to primary key (id? - if not, you would obviously need to change that) in the table?
Given that IDs may not be numerically sequential, we can't simply assume that the previous 5 rows will be the ID of the row with title = $gradschool minus 5, so wondered if this might work:
$initial = MOGRadschool::where('Title', $gradschool)->first(); // get the initial row with the title of $gradschool
$result = MOGRadschool::where('id', '<', $initial->id)->take(5)->orderBy('id', 'DESC') // new query getting the previous 5 rows, by ID
->union(MOGRadschool::where('id', '>', $initial->id)->take(5)) // union a second query getting the next 5 rows by ID
->get() // get the result as a collection
->add($initial) // add the initial row to the collection
->sort(); // sort the collection (by id) so that the initial row is in the middle
So the output is a collection containing the initial row in the middle, with up to 5 records either side. You also have the initial row to highlight the output, if you need that.
If you want it based on the IDs, which is what I understand from your issue, something like this should work:
$selectedGradSchool = MOGRadschool::where('Title', '=', $gradschool)->get()->first();
$aboveSelected = MOGRadschool::where('id', '<=', $selectedGradSchool->id)
->orderBy('id', 'desc')
->take('5')
->get();
$belowSelected = MOGRadschool::where('id', '>' $selectedgradSchool->id)
->take('5')
->get();
//Concatenate both results
$schoolRange = $aboveSelected->concat($belowSelected);
Now the collection should look similar to your desired result.

laravel ->count() vs ->get()->count()

Why are the two statement below behaving differentlY? The first returns 3 the second returns 1 for $progress. I thought that the first aggregates on the DB and the second on the server. So they should still return the same value.
$progress = $this->user->userActivities()->select('date')
->groupBy('date')
->get()->count();
$progress = $this->user->userActivities()->select('date')
->groupBy('date')
->count();
->get()->count() will load Eloquent model objects into memory and then will count those.
->count() will use DB aggregate function, so it will definitely be more efficient:
select count(*) as aggregate ...
It's quite late answer but I had faced the same issue. These two examples are returning two different counts because of groupBy you have used.
$progress = $this->user->userActivities()->select('date')
->groupBy('date')
->get()->count();
This takes the rows first, then count the row numbers and returns the count.
$progress = $this->user->userActivities()->select('date')
->groupBy('date')
->count();
Whereas this counts the rows by its group in the DB and returns the first group's row record count only from the list.
the solution I used instead of get()->count() for my case is like
$progress = $this->user->userActivities()
->distinct('date')
->count('date');
Use ->get()->count() is wrong. I used ->get()->count() in my web app and when my database records have more than 80000 records I get error 500 in my app.
The first counts the number of returned records, the second counts the records and returns the number. If you are using a paging of 10, for example, then the first will yield 10 and the second the actual number if the number of mathcing elements is greater than 10.
$count = Model::all()->count();
return view('home',compact('count'));
this will function for laravel 6;
Both of them return the same result. But as said for preventing to crash or decrease the speed of response it's better to use count() instead of get()->count()

Sort by average value of an one to many related table column

I have 2 models; Post and Rating
The Rating model contains an amount column which specifies how high something has been rated. This is based on 5 star rating so the amount can be a value from 1-5
The Post model has a one to many relation with the rating model and function called Ratings that returns the hasMany.
I'd like to get the 5 latest posts based on the average rating. For the average rating I've created a function that can be seen below
Note: the plural(Ratings) returns the hasMany relation where as the singular(Rating) returns a value which is the average rating
public function Rating(){
return floor($this->Ratings()->avg('rating'));
}
Is it possible to retrieve posts ordered by the avg rating using the Eloquent QueryBuilder?
Currently I'm retrieving all posts and then using the sortBy method on the collection object in order get the ones with the highest average rating. The way I'm doing this can be seen below.
$posts = Post::all();
$posts = $posts->sortByDesc(function ($post, $key) {
return $post->Rating();
});
Now if I'd only want to show 5 I still have to retrieve and sort everything which doesn't seem very resource friendly(In my eyes. I don't have any proof of this or say it is true).
So my question is the following: Is this doable using Eloquent instead of sorting the FULL collection.
Sub question: Will doing this with Eloquent instead of sorting the collection have any impact on efficiency?
You may use query builder
DB::table('post')
->select('post.id', 'AVG(rating.amount)')
->join('rating', 'post.id', '=', 'rating.post_id')
->groupBy('post.id')
->orderByRaw('AVG(rating.amount) DESC');

Resources