Laravel using groupBy and unique - php

I am using Laravel and have a music table, an order_items table and a users table. The music items all have a user and a user can have multiple music items. I am trying to get the top 100 selling music, but I want to return only one music item per user (the top selling one of all their music sales).
My code is:
$items = DB::table('order_items')
->join('music', 'music.id', '=', 'order_items.music_id)
->groupBy('music_id')
->select('order_items.user_id', 'order_items.music_id', DB::raw('count(*) as sold'))
->orderBy(DB::raw('sold_count'), 'desc');
$items = collect($items)->unique('user_id')->values()->take(100);
This does seem to return the data that I need, however the first query returns more than 35K records so takes a while to run. I can place a limit on the initial query however then I can not guarantee that there will be 100 items once they are grouped by user.
Is there a better way to do this? Can it be run in one query?

Related

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!

Searching in One to Many

I'm developing a system that have a Users table and a Books table.
I need to implement a Users search, and a results page showing all Users and all Books belonging to each user.
Currently, I'm using GROUP_CONCAT to get the Books of each User.
Alternatively, using LEFT JOIN brings duplicate results; Then I have to manipulate the data in PHP(is this 'better'?) Off course, I can't use GROUP BY as I need all Books.
I dont have any problems with GROUP_CONCAT, but now I need to filter by Book's and putting 'WHERE books.name like "% name%"' will filter the GROUP_CONCAT result, then showing just the searched book in the filter(while I need all users books)
Wich is the best method to do a search with One-to-Many results in PHP / MySQL ?

Get Data from Multiple Tables Laravel

I am trying to build a coupon site in Laravel. Each merchant has their own deals/coupons. I have been able to print deals/coupons for a merchant on their specific pages.
Here's my query
$deals = DB::table('deals')
-> join ('merchants', 'deals.merchant_id', '=', 'merchants.merchant_id')
-> where ('merchant_url_text', $merchant_url_text)
-> get();
So far so good.
Now this is where it starts getting complex.
Each deal has 2 more pieces associated with it. Click counts and Votes associated with deals.
The click counts are in a table called clicks which records each click on the website. The click record will have a click id associated it. So I would need to get a count of clicks each deal gets.
The second piece is votes. The votes around a deal are stored in a deal_votes table. The deal_votes table has deal_id, vote (1 or 0)
How do I combine click counts and deal votes to return in the same query so that I can display the info in my view?
Do you have models and relationships set up for merchants, deals, coupons, and clicks? This is trivial if you use Eloquent models with relationships, for which the docs are here: https://laravel.com/docs/5.2/eloquent-relationships
This would look like:
$merchant = Merchant::where('merchant_url_text', $merchant_url_text)
->with('deals','deals.votes','deals.clicks')
->first();
The with() function adds all of the nested information, ie query joins, into a single query.
In your view:
#foreach($merchant->deals as $deal)
Deal: {{$deal->name}}
Clicks: {{count($deal->clicks)}}
Votes: {{$deal->votes->sum('vote')}}
#endforeach

Laravel Has Many Trough with multiple levels

I have a very specific query that I don't know how to get in Eloquent
I have the following tables
Orders, OrderInvoice,OrderPayment
So each Order has many OrderInvoices and each OrderInvoice has many OrderPayments
Then I have the table turns which has many payments
So what I want is to get all the orders related to a specific turn
I know how to get all the invoices:
$this->belongsToMany('OrderInvoice','orders_payments','turn_id','invoice_id');
But I need the next level and get the Orders,
How can I achieve that in eloquent?
Thank you so much!
EDIT: Tables structure
Orders
id
OrderInvoice
id
order_id
OrderPayment
id
invoice_id
turn_id
Turns
id
Most straightforward:
// load related collections
$turn->load('invoices.orders');
// then
$turn->invoices; // collection of invoices, for each you can do this:
$turn->invoices->first()->orders; // collection of orders for an invoice
If you want to get single collection of orders for given turn, then you need this trick, which is the easiest way (no joins etc), but not the best in terms of performance for sure:
// store orders in a separate variable
$turn->load(['invoices.orders' => function ($q) use (&$orders) {
$orders = $q->get()->unique();
}]);
// then
$orders; // single collection of all the orders related to the given turn
// also still accessible as usually:
$orders->invoices;
$orders->invoices->first()->orders;

User roster, joining a table but loosing data

I need some help with a query (using Laravel framework). I'm building a user roster with a few columns that incude the users ratings. Basically, it selects all the active users who have initials and joins the user ratings table to select the ratings for the respective users. The where_in is to select only specific ratings. My issue is on the roster, it only selects one rating, rather than all of them (if the user has more than one rating). I've also tried without the group_by, but then the users are duplicated on the table depending on the number of ratings they have (example: if the user has 2 ratings, their row is displayed twice on the roster).
Query:
$users = DB::table('users')
->where('users.initials', '!=', '')
->left_join('user_ratings', 'user_ratings.uid', '=', 'users.uid')
->where_in('users_ratings.rid', array(6,17,21,20))
->group_by('users.uid')
->order_by('users.name')
->get();
Tables:
Users
=======
uid name
1 John
2 Jeff
3 Cathy
Ratings
======
rid uid
1 1
2 1
2 2
3 1
4 3
The problem is when you do a left_join, the result you are going to get is multiple rows. So without the group_by clause it will return all the results you want in rows, not columns, and with the group_by it will just return the first rating (which is the expected behavior).
I would suggest you just use Eloquent ORM and set up the models and their relationships (it's a lot easier and cleaner). I'm guessing user_rating is a many-to-many pivot table? In which case you would have two models User and Rating and their relationship will be has_many_and_belongs_to. Also, the naming conventions in laravel have the pivot table in alphabetical order, so it will be called "rating_user". Look here for how to set up relationships: http://laravel.com/docs/database/eloquent#relationships
Once the models with their relationships are setup, what I would do is
$users = User::where('initials', '!=', '')->order_by('name')->get();
Then,
foreach($users as $user) {
echo "Ratings for " .$user->name . ": ";
$ratings = $user->ratings()->pivot()->where_in('rid', array(6,17,21,20))->get();
foreach($ratings as $rating) {
however you want to display the ratings here...
}
}
This may not be the most efficient way, but it should get the job done given my assumptions are true.
A quick look at the documentation reveals that the Fluent Query Builder can only do what MySQL can do, and MySQL cannot return an array to the client.
Take a look at this question for your alternatives.
If you want Laravel to fetch the ratings for you, you need to build a model.

Categories