0

So I have orders model and table.

it contains integer fields that I want to calculate to find daily, monthly and yearly earnings. My current solution works fine but as shown in the image below it runs 3 queries to find all of them. is there any way better to calculate the earning without running the query 3 times?

I have also tried getting all the orders into one variable then using laravel's collection methods to calculate earnings, that also works but it's slower than my current solution.

    public function index()
{

    $dailyEarning = Order::whereDate('created_at', Carbon::today())->get()->sum(function ($order) {
        return (($order->cost - $order->product->original_cost) * $order->quantity);
    });

    $monthlyEarning = Order::whereBetween('created_at', [
        Carbon::today()->startOfMonth(),
        Carbon::today()->endOfMonth(),
    ])->get()->sum(function ($order) {
        return (($order->cost - $order->product->original_cost) * $order->quantity);
    });

    $yearlyEarning = Order::whereBetween('created_at', [
        Carbon::today()->startOfYear(),
        Carbon::today()->endOfYear(),
    ])->get()->sum(function ($order) {
        return (($order->cost - $order->product->original_cost) * $order->quantity);
    });

    return view('admin.home',[
        'dailyEarning' => $dailyEarning,
        'monthlyEarning' => $monthlyEarning,
        'yearlyEarning' => $yearlyEarning,
    ]);
}

Orders table schema:

   public function up()
{
    Schema::create('orders', function (Blueprint $table) {
        $table->id();

        $table->unsignedBigInteger('product_id');
        $table->foreign('product_id')->references('id')->on('products')->onDelete('restrict');

        $table->unsignedBigInteger('invoice_id');
        $table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');

        $table->double('cost');
        $table->double('quantity');

        $table->timestamps();
    });
}

Products table schema:

  public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();

        $table->string('name');
        $table->string('code');
        $table->string('brand');

        $table->double('quantity')->default(0);
        $table->double('original_cost')->default(0);
        $table->double('cost')->default(0);

        $table->string('photo')->default('no-image');
        $table->timestamps();
    });
}
5
  • 2
    You might find reinink.ca/articles/… informative on this issue. Commented Feb 27, 2021 at 16:16
  • You do a single common query. Then, utilize collection to do specific operations. Commented Feb 27, 2021 at 16:37
  • Would you be able to explain with an example? I have tried that but I got slower results @AbdurRahman Commented Feb 27, 2021 at 16:55
  • 1
    Doing that in a collection is probably going to be slower because it's done in PHP after the fact. If you can calculate that data as part of the query, which should be possible using subqueries, then it will almost certainly be faster. Commented Feb 27, 2021 at 19:52
  • 1
    Also, OP, please don't post pictures of your code as it's hard to read and can't be copied and pasted for editing in answers. Please post the actual code snippet, correctly formatted, and show both implementations that you mention. Commented Feb 27, 2021 at 20:28

2 Answers 2

1

Create an OrderSummary table. The idea should be that you run the monthly (to date) and yearly (to date) queries only once per day (automatically).

YTD Sales = Yesterday's YTD Sales + Today's Sales MTD Sales = Yesterday's MTD Sales + Today's Sales

(Both presume that yesterday is the same year, an the same month. If not, account or it)

Each time, you'll still run the query to get the current day's sales. But you will query an OrderSummary single row for the previous day with columns:

id | date | YTD Sales | Monthly Sales

If you're using caching, you'll cache those results as well.

If it returns zero records for the previous day, then you'll need to execute your queries and update OrderSummary. However, you can execute these in the background with a CRON so they are ready beforehand.

Note: Since you may need to account for order reversals, so the full YTD and MTD queries would have to be run daily (versus just adding the previous day's completed order total to the prior amounts to update them).

Sign up to request clarification or add additional context in comments.

4 Comments

This doesn't strike me as the best approach. You could create a view to put this data together and it would always be up to date and not require a job to run to populate it.
Agree. I'd probably go with a caching optimization. Although, if there are multiple things being done in dashboard environment, I might still consider doing some stuff with stats preparation to keep everything moving fast on the user end. Could also lower the perceived lag using a single-page style app, with something like Laravel Livewire.
I think the most performant way to achieve this is probably by using subqueries in a similar fashion to what is described in reinink.ca/articles/… but it would likely be quite a complex query.
Very nice. Some elegant solutions possible with those subquery approaches.
1

Without seeing the structure of the tables I don't think it's possible to provide a specific working solution for this problem. However, in principle the way to resolve this is almost certainly to move as much of this work as possible to the database and use subqueries to calculate the different values.

Most SQL implementations have a SUM() implementation so it should be possible to use that to move aggregating the values into the database for each individual query. Then make each value a subquery of a larger query, and that should achieve what you want.

3 Comments

I have added the table schemas to the question, I will try to use sub queries but an example on my case would be useful as I really don't understand how to write them in Laravel.
I'd start by reading through laravel.com/docs/8.x/queries#subquery-where-clauses - tbh the syntax for subqueries in Laravel can seem a bit weird but they're very useful for more complex queries. It should then be possible to refactor each query to use SUM() so as to keep the calculation in SQL, then convert each one into a subquery.
Basically it's Step one: Amend each query to use SUM() and move the entire calculation into SQL. Step two: make each query a subquery of a single larger query

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.