Reducing queries in laravel - php

Quick question to which I haven't been able to find a solution for myself, nor by googling it.
On my dashboard view I show my payment plans, simply by doing $plans = Plan::all() and I return that to the view. Nothing fancy there. But, on my actual dashboard view, I want to show the total amount in dollars from the transactions that have been done. So on my view, I do the following (stripped example):
#foreach($plans as $plan)
{{ $plan->paidAmount() }}
#endforeach
Which is using the following method from my model:
private function paidAmount()
{
return $this->transactions->sum('amount');
}
Which basically just grabs the total amount of all related transactions to that plan, and displays it on the page. However, this generates a new query for each of my plans. Let's say I have 5 plans, it will generate 5 additional queries for each amount I need to be displayed.
Same goes for the remaining amount that I want to display:
private function paidAmount()
{
return $this->amount - $this->paidAmount();
}
Which will generate an additional 5 queries. So for a simple table in which I display some information,a whopping 11 queries are being executed.
Is there any way that I can include the paidAmount and remainingAmount in my original query? I know there should be a way, I just haven't been able to find it yet. So just something along the lines of:
$plans = Plan::with('paidAmount')->get();

You can use sub queries inside your main query Like this to calculate the sum
Like this -
$plans = Plan::all('*', DB::RAW("(SELECT SUM(amount) FROM transactions WHERE plan_id=plans.id ) as amount") );
This way you will already have sum when rendering in views

Related

Does DB raw sum function help optimized data?

In the application, we have a feature where it will display all the years as tab. Each tab has amount(where the amount is the summarized) but at the same time it is have data.
Scenario
Just to give a background about the application, it is a car seller
Imagine you are the customer, you started to use this application way back 2019. You clicked this module to check the summarized of each year
Let's assume we have 2500 data for each year.
In current year I want to display the total amount of all previous years
2023 tab is the active, from 2019 - 2022 you have sold 300,000 each year
In getting the previous amount, I used the DB raw sum function to get the summation.
Question: Does DB raw sum function lessen the heavy query to get the summation?
You can accomplish this with eloquent, without having to load all of the inner relationship data.
Normally programmers do the following:
Model:
public function children()
{
return $this->hasMany(Children::class);
}
In the Controller:
$models = Model::with('children')->get();
Then they load the data normally in blade and count:
#foreach($models as $model)
<p>{{ $model->children->count() }}</p>
#endforeach
Instead you can do the following:
In the Controller:
$models = Model::withCount(['children'])->get();
In the blade you load the data with the count:
#foreach($models as $model)
<p>{{ $model->children_count }}</p>
#endforeach
The withCount method will place a {relation}_count attribute on the resulting models
Use Barry DebugBar to test out the speed with heavy data, you will notice that doing it this way will reduce heavily the time needed to load the relationship, since you are not loading all of the inner relationship, rather you are just counting the inner relationship and displaying it.
For withCount Reference: https://laravel.com/docs/9.x/eloquent-relationships#aggregating-related-models
For withSum Reference: https://laravel.com/docs/9.x/eloquent-relationships#other-aggregate-functions
You can accomplish the same thing using withSum() and you can do this for deeper level of relationships as well.
Create and maintain a summary table, with year, and the sum for that year.
Working from that table, the query would be much faster, even if you had to do multiple summing.
Or, that table could have the cumulative amount since day-1. Then, the sum for "prior to 2023" is "cumulative through 2022" minus the "cumulative through 2018".
Summary Tables

How to get total user count in rateable?

I implemented a rating system looking at the rateable laravel package and it works as it should. I have avg rating for each product but want to add besides it the number of users that rated that product. For instance an avg rating of 4.5 that was rated by 5 users.
The package has userSumRating below which outputs the sum of a user's rating but that is not the same with what i am looking for
public function userSumRating()
{
return $this->ratings()->where('user_id', \Auth::id())->sum('rating');
}
I have tried a couple of things but none has worked yet.
I would utilise group by and count from SQl terminology. This is rough idea, not ranned but i think this should solve it. Group by squashes all rows with the same user into one, then we just count how many there is after that.
public function usersRated()
{
return $this->ratings()->groupBy('user_id')->count('user_id');
}
After looking at package you have mentioned in the question, ratings() gives all the ratings associated with the product so you can count those ratings like this:
$product->ratings()->count();
I am not sure but this might work. I have assumed user rates particular product only once.
Well got it working. First made a user only able to rate once with subsequent ratings as updates then
$this['count'] = Advert::with(
[
'ratings' => function ($query) {
$query->count('rating');
}
])
->where('slug', $slug)
->groupBy('user_id')
->first();
then in twig
{{ count.ratings|length }}

How to make sales report more "scalable"?

I built an application to keep track of the sales. In my customers view, I want a column with total sales per customer, but as the customer base is growing, the list is loading more and more slowly. This is what I did (simplified):
Controller:
$customers = App\Customer::get();
View:
#foreach ($customers as $customer)
{{ $customer->name }} {{ $customer->totalSales() }}
#endforeach
Model:
public function totalSales()
{
$invoiceLines = InvoiceLine::whereHas('invoice', function ($query) {
$query->where('customer_id', $this->id);
})->get();
$sales = $invoiceLines->reduce(function ($carry, $invoiceLine) {
return $carry + ($invoiceLine->quantity * $invoiceLine->pricePerUnit);
});
return $sales ?: 0;
}
What would be the best way to make this view/report more "scalable"?
I have been thinking in creating command that calculates the total sales per customer overnight and put the result in the customer table, but that means that the numbers won't be accurate during the day...
this seems like a very interesting problem.
I have been thinking in creating command that calculates the total
sales per customer overnight and put the result in the customer table
this is a good option.
but that means that the numbers won't be accurate during the day...
You can keep the numbers accurate by doing the following:
by incrementing the customers table count every time a invoice is made.
This should work for total sales.
Make sure you have an index on the customer_id column.
Search for ways to do a "SQL SUM on 2 columns using laravel".
Try and find some way to do "SQL SUM on 2 with a GROUP BY. Doing this will replace #2
A good way to speed up your application is to avoid making calls to the database in a loop. That is what #3 is suggesting (the loop in this case is the #foreach in your View and the database call is the InvoiceLine::...->get(); in totalSales()
Adding the index (if missing) and reducing the # of calls to the DB will yield the best results.
I have limited knowledge of Laravel but one way to do this with raw SQL would be:
SELECT c.name, ts.totalSales
FROM customer c
INNER JOIN (
SELECT customer_id, SUM(quantity * pricePerUnit) as totalSales
FROM invoice
GROUP BY customer_id
) ts ON c.id = ts.customer_id
You can see how all the data you're trying to print is pulled at once? I assume you'd want to try and write that using Laravel's Eloquent thingy.
Based on the answers above, I came to the following solution:
I created an event: App\Events\InvoiceSaved which is dispatched every time an invoice is "touched" (created, updated or deleted). The App\Events\InvoiceSaved event will calculate the total sales for the customer and add the result to the customers table (extra field total_sales). Now I can just query my customers table and need to query a relation. The loading time dropped from 7 seconds to 0.5 second!

Concept of table relations in Laravel

I have two tables:
Cards
Notes
Each Card has multiple Notes. So there is a relation between them like this:
class Card extends Model {
public function notes ()
{
return $this->hasMany(Note::class);
}
}
Ok well, all fine.
Now I need to understand the concept of these two lines:
$card()->$notes()->first();
and
$card()->$notes->first();
What's the difference between them? As you see in the first one $note() is a function and in the second one $note isn't a function. How will they be translated in PHP?
The first one points out to the card table and the second one points out to the notes table, right? or what? Anyway I've stuck to understand the concept of tham.
I don't know about $ before the $notes in your code but if you trying to say something like this.
1- $card->notes()->first();
2- $card->notes->first();
In the code in line 1, first you have a $card model and then you wanted to access all notes() related to that $card, and because of adding () after notes you simply call query builder on notes, show you can perform any other database query function after that, something like where, orderBy, groupBy, ... and any other complicated query on database.
But in the second one you actually get access to a collection of notes related to that $card, we can say that you get all related notes from database and set it into laravel collections and you are no more able to perform database query on notes.
Note: because laravel collections have some methods like where(), groupBy(), whereIn(), sort(), ... you can use them on the second one, but in that case you perform those methods on collections and not database, you already get all results from database

Laravel Eloquent Sum of relation's column

I've been working on a shopping cart application and now I've come to the following issue..
There is a User, a Product and a Cart object.
The Cart table only contains the following columns: id, user_id, product_id and timestamps.
The UserModel hasMany Carts (because a user can store multiple products).
The CartModel belongsTo a User and CartModel hasMany Products.
Now to calculate the total products I can just call: Auth::user()->cart()->count().
My question is: How can I get the SUM() of prices (a column of product) of the products in cart by this User?
I would like to accomplish this with Eloquent and not by using a query (mainly because I believe it is a lot cleaner).
Auth::user()->products->sum('price');
The documentation is a little light for some of the Collection methods but all the query builder aggregates are seemingly available besides avg() that can be found at http://laravel.com/docs/queries#aggregates.
this is not your answer but is for those come here searching solution for another problem.
I wanted to get sum of a column of related table conditionally.
In my database Deals has many Activities
I wanted to get the sum of the "amount_total" from Activities table where activities.deal_id = deal.id and activities.status = paid
so i did this.
$query->withCount([
'activity AS paid_sum' => function ($query) {
$query->select(DB::raw("SUM(amount_total) as paidsum"))->where('status', 'paid');
}
]);
it returns
"paid_sum_count" => "320.00"
in Deals attribute.
This it now the sum which i wanted to get not the count.
I tried doing something similar, which took me a lot of time before I could figure out the collect() function. So you can have something this way:
collect($items)->sum('amount');
This will give you the sum total of all the items.
you can do it using eloquent easily like this
$sum = Model::sum('sum_field');
its will return a sum of fields,
if apply condition on it that is also simple
$sum = Model::where('status', 'paid')->sum('sum_field');
You can pass this as UserModel attribute. Add this code to UserModel.
public function getProductPriceAttribute(){
return $this->cart()->products()->sum('price');
}
I assume something like this:
UserModel has a one to many relationship with a CartModel named cart
CartModel has a one to many relationship with ProductModel named products
And then you can get sum price of the product like this:
Auth::user()->product_price
Since version 8, there is a withSum method on Eloquent, so you could use this.
Auth::user()->withSum('products', 'price')->products_sum_price;
This won't load all products into memory and then sum it up with collection method. Rather it will generate a sub query for the database, so it's quicker and uses less memory.
Also using query builder
DB::table("rates")->get()->sum("rate_value")
To get summation of all rate value inside table rates.
To get summation of user products.
DB::table("users")->get()->sum("products")
For people who just want to quickly display the total sum of the values in a column to the blade view, you can do this:
{{ \App\Models\ModelNameHere::sum('column_name') }}
You can also do it for averages:
{{ \App\Models\ModelNameHere::avg('column_name') }}
Min:
{{ \App\Models\ModelNameHere::min('column_name') }}
Max:
{{ \App\Models\ModelNameHere::max('column_name') }}
To get the Count of a table:
{{ \App\Models\ModelNameHere::count() }}

Categories