Laravel eloquent get the latest rows of grouped rows - laravel

Using Eloquent, trying to find a way to get the latest rows of every row grouped by: exchange, base, quote
Data
exchange base quote price value created_at
bittrex BTC USD 10000 10000 2018-01-05
bittrex BTC USD 9000 9000 2018-01-01
poloniex BTC USD 10001 10001 2018-01-05
poloniex BTC USD 9000 9000 2018-01-01
binance BTC USD 10002 10002 2018-01-05
binance BTC USD 9000 9000 2018-01-01
binance ETH USD 800 800 2018-01-05
binance ETH USD 700 700 2018-01-01
Result:
bittrex BTC USD 10000 10000 2018-01-05
poloniex BTC USD 10001 10001 2018-01-05
binance BTC USD 10002 10002 2018-01-05
binance ETH USD 800 800 2018-01-05
UPDATE
I went with #Cryode solution, raw SQL instead of Eloquent (if anyone can come up with one Eloquent query to replicate the results of the query below, feel free to post).
I've also changed the structure of the table to add id (increments) as the primary key. I also added the following index $table->index(['exchange', 'base', 'quote', 'created_at']);
Here is the solution:
$currencies = DB::select('SELECT *
FROM (
SELECT DISTINCT exchange, base, quote
FROM tickers
) AS t1
JOIN tickers
ON tickers.id =
(
SELECT id
FROM tickers AS t2
WHERE t2.exchange = t1.exchange
AND t2.base = t1.base
AND t2.quote = t1.quote
ORDER BY created_at DESC
LIMIT 1
)
');
Thanks

Let's first determine what this SQL query would actually look like.
This DBA answer provides some great insight into the "greatest-n-per-group" problem, as well as PostgreSQL and MySQL examples. Inspired by this answer, here's what I've come up with for your single table (assuming MySQL as your DB):
SELECT ticker.*
FROM (
SELECT DISTINCT exchange, base, quote
FROM ticker
) AS exchanges
JOIN ticker
ON ticker.id =
(
SELECT id
FROM ticker
WHERE ticker.exchange = exchanges.exchange
AND ticker.base = exchanges.base
AND ticker.quote = exchanges.quote
ORDER BY created_at DESC
LIMIT 1
);
Oh dear. Getting that into Laravel-speak doesn't look easy.
Personally, I wouldn't even try. Complicated SQL queries are just that because they take advantage of your database to do reporting, data gathering, etc. Trying to shove this into a query builder is tedious and likely comes with little to no benefit.
That said, if you'd like to achieve the same result in a simple way using Laravel's query builder and Eloquent, here's an option:
// Get the unique sets of tickers we need to fetch.
$exchanges = DB::table('ticker')
->select('exchange, base, quote')
->distinct()
->get();
// Create an empty collection to hold our latest ticker rows,
// because we're going to fetch them one at a time. This could be
// an array or however you want to hold the results.
$latest = new Collection();
foreach ($exchanges as $exchange) {
$latest->add(
// Find each group's latest row using Eloquent + standard modifiers.
Ticker::where([
'exchange' => $exchange->exchange,
'base' => $exchange->base,
'quote' => $exchange->quote,
])
->latest()
->first()
);
}
Pros: You can use the query builder and Eloquent abstractions; allows you to maintain your Ticker model which may have additional logic needed during the request.
Cons: Requires multiple queries.
Another option could be to use a MySQL View that encapsulates the complicated query, and create a separate Eloquent model which would fetch from that view. That way, your app code could be as simple as TickerLatest::all().

You may pass multiple arguments to the groupBy method to group by multiple columns
Please refer to documentation https://laravel.com/docs/5.6/queries#ordering-grouping-limit-and-offset
$users = DB::table('users')
->groupBy('first_name', 'status')
->having('account_id', '>', 100)
->get();

Since Laravel 5.6.17 you can use joinSub() so a possible Eloqunish solution could maybe be something like this:
Group and find the ticket with the last date
$latest = Ticker::select('exchange', 'base', 'quote', DB::raw('MAX(created_at) as created_at'))
->groupBy('exchange', 'base', 'quote');
And join the latest of each group again all records with joinSub()
$posts = DB::table('tickets')
->joinSub($latest, 'latest_tickets', function ($join) {
$join->on('tickets.exchange', '=', 'latest_tickets.exchange')
->on('tickets.base', '=', 'latest_tickets.base')
->on('tickets.quote', '=', 'latest_tickets.quote')
->on('tickets.created_at', '=', 'latest_posts. created_at');
})->get();

You can fetch the latest rows first then group the collection later.
$items = Ticker::latest()->get();
// exchange, base, quote
$groupedByExchange = $items->groupBy('exchange');
$groupedByBase = $items->groupBy('base');
$groupedByQoute = $items->groupBy('qoute');
UPDATE:
You can get the single item by each group by simple adding ->first() after the groupBy() function.
$latestByExchange= Ticker::latest()->groupBy('exchange')->first(); // and so on

Here is another way to get latest record per group by using a self left join and this query can be easily transformed to laravel's query builder.
It doesn't require any specific version of laravel to work, it can work on older versions of laravel too
No need for N+1 queries (overhead) as suggested in other answer
In plain SQL it can be written as
select a.*
from tickers a
left join tickers b on a.exchange = b.exchange
and a.base = b.base
and a.quote = b.quote
and a.created_at < b.created_at
where b.created_at is null
And in query builder it would look like
DB::table('tickers as a')
->select('a.*')
->leftJoin('tickers as b', function ($join) {
$join->on('a.exchange', '=', 'b.exchange')
->whereRaw(DB::raw('a.base = b.base'))
->whereRaw(DB::raw('a.quote = b.quote'))
->whereRaw(DB::raw('a.created_at < b.created_at'))
;
})
->whereNull('b.created_at')
->get();
Laravel Eloquent select all rows with max created_at
Or you use a correlated sub query to choose latest row
SQL
select a.*
from tickers a
where exists (
select 1
from tickers b
where a.exchange = b.exchange
and a.base = b.base
and a.quote = b.quote
group by b.exchange,b.base,b.quote
having max(b.created_at) = a.created_at
);
Query Builder
DB::table('tickers as a')
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('tickers as b')
->whereRaw(DB::raw('a.exchange = b.base'))
->whereRaw(DB::raw('a.base = b.base'))
->whereRaw(DB::raw('a.quote = b.quote'))
->groupBy(['b.exchange','b.base','b.quote'])
->havingRaw('max(b.created_at) = a.created_at')
;
})
->get();
DEMO

Related

Laravel where table with relationships

I’m build a app with laravel 8. This app has table order and table tracking. The order and tracking has a one to many relation, the models are like below:
class Order extends Model
{
protected $with = ['tracking'];
public function tracking()
{
return $this->hasMany(Tracking::class);
}
}
class Tracking extends Model
{
public function order()
{
return $this->belongsTo(Order::class);
}
}
Now I want query the orders filtering them by status in the last tracking inserted. For exemple, with the data below:
Order
ID VALUE
1 100.00
2 200.00
3 300.00
Tracking
ID ORDER_ID STATUS CREATED_AT
1 1 Accept 2022-03-01 00:00:00
2 1 Paid 2022-03-02 00:00:00
3 2 Accept 2022-03-01 00:00:00
4 2 Paid 2022-03-02 00:00:00
5 2 Cancel 2022-03-03 00:00:00
6 3 Accept 2022-03-01 00:00:00
7 3 Paid 2022-03-02 00:00:00
If the param in where clause is status = Paid, I want return order with id 1 and 3, with all tracking related.
I try to use whereHas like this:
$query = Order::query();
$query->whereHas('tracking', function ($query) use ($request) {
return $query->where('status', '=', 'Paid');
});
$orders = $query->paginate(10);
but this make a query in all tracking, and I need query only if the last status is like Paid.
And I need data like this:
Order ID VALUE TRACKING
1 100.00 [[ID: 1, STATUS: Accept], [ID: 2, STATUS: Paid]]
3 300.00 [[ID: 6, STATUS: Accept], [ID: 7, STATUS: Paid]]
Notice, that order with id 2 has Paid in 2022-03-02 but in 2022-03-03 00:00:00 its canceled, so this wont appear.
Any help is welcome.
Thanks.
I cannot reproduce the situation right now. So, suggesting the following solution without any performance testing. I hope it will solve your issue.
DB::table("tracking")
->join("order", function($join){$join;})
->select("tracking.order_id", "order.val", "max (tracking.created_at) as status_updated")
->where("order.id", "=", tracking.order_id)
->groupBy("tracking")
->get();
The wanted result is a bit complex to achieve, so we need to pay attention to how we'll achieve it.
To do so, let's decompose the fetching process into parts :
we want to select the following columns :
order_id and value from orders table.
tracking_id, status and created_at from the related tracking records. Those related records will be fetched using an INNER JOIN clause on tracking.order_id = orders.id.
Say we need to filter by status = 'paid' so in the query we'll likely to have something like where tracking.status = 'paid'.
The wanted result can be described as follows: GET ME orders WHERE LATEST TRACKING STATUS IS 'paid' which means if the latest tracking.status for a specific order is not 'paid' then we don't want that order, nor its trackings, at all.
To achieve the above statement, we need to know the latest tracking record related to order and if that related record has a status equals to paid then fetch all the related records for that order.
Here's an SQL query that fetches the wanted results when we need tracking.status = 'paid' :
-- selects the needed columns
SELECT `orders`.`id` AS `order_id`, `value`, `t`.`id` AS `tracking_id`, `t`.`status`
FROM `orders`
-- fetch the related "tracking" records for each order in the set. The table "tracking" is aliased as "t" for simplicity
INNER JOIN `tracking` `t` ON `t`.`order_id` = `orders`.`id`
-- we need to get only the related records ONLY when the latest "tracking" saved for the current "order" is the status we want (in our case "paid")
WHERE (
-- this subquery gets the status of the latest added "tracking" of each "order"
-- check below sub queries
SELECT `status`
FROM `tracking`
WHERE `order_id` = `orders`.`id` AND `id` = (
-- this subquery gets the "id" of the latest added "tracking"
-- check below sub query
SELECT `id`
FROM `tracking`
WHERE `order_id` = `orders`.`id` AND `created_at` = (
-- this subquery filters the records and returns the "created_at" values of the latest added "tracking"
SELECT `created_at`
FROM `tracking`
WHERE `order_id` = `orders`.`id`
-- instead of using "max" function we can simply get the first record in the reverse order (DESC)
ORDER BY `created_at` DESC LIMIT 1
)
)
) = 'paid'
Let me try to translate the above query into Laravel's query builder:
use Illuminate\Support\Facades\DB
/** inform the query builder that the "orders" table is main table in the query */
DB::table('orders')
/** instruct to select those columns */
->select('orders.id as order_id', 'orders.value', 't.status', 't.created_at')
/** have an INNER JOIN clause to get the related "tracking" records */
->join('tracking as t', 't.order_id', '=', 'orders.id')
->where('t.status', function ($q) {
/** creates a subquery to get the status of the last inserted related "tracking" */
$q->select('status')
->from('tracking')
->whereColumn('order_id', 'orders.id')
->where('created_at', function ($q) {
/** a subquery to get the latest inserted tracking record (only the last will be returned) */
$q->select('created_at')
->from('tracking')
->whereColumn('order_id', 'orders.id')
->orderByDesc('created_at')
->limit(1);
});
})->get(); /** execute the query */
The above query was not tested so it's better that you take some time to test it and tweak it the way you want.
Based on the data sample provided in the question, the above SQL query should return :
order_id
value
tracking_id
status
1
100.00
1
Accept
1
100.00
2
Paid
3
300.00
6
Accept
3
300.00
7
Paid
Hope i have pushed you further.
Edit: My previous answer was totally wrong, so here's another attempt:
An easier way to do this probably would be to define an extended relationship in the Order model:
// App\Models\Order
public function active_tracking() {
return $this->tracking()->where('status', 'Paid');
}
Then you can just fetch your orders with with active Tracking records:
Order::with('active_tracking')->whereHas('active_tracking')->get();
Update 1.2
According to my comment, I have updated my answer. I've a nice idea also is that if the last track of the order has not has a Cancel value I'll return this track else, I'll not return it.
use Illuminate\Pagination\LengthAwarePaginator;
$orders = Order::with('tracks')->withCount('tracks')->get();
$filteredOrders = [];
$orders->each(function ($order) use (&$filteredOrders) {
$order->tracks->each(function ($track) use ($order, &$filteredOrders) {
if ($order->tracks[$order->tracks_count - 1]->STATUS != "Cancel") {
array_push($filteredOrders, $order);
}
});
});
// Paginate the filtered array
return new LengthAwarePaginator($filteredOrders, count($filteredOrders), 10);

Need guidance on how to build a Laravel database query

In Laravel 6.18 I'm trying to figure out how to recreate the following Postgres query.
with data as (
select date_trunc('month', purchase_date) as x_month, date_trunc('year', purchase_date) AS x_year,
sum (retail_value) AS "retail_value_sum"
from coins
where user_email = 'user#email.com' and sold = 0
group by x_month, x_year
order by x_month asc, x_year asc
)
select x_month, x_year, sum (retail_value_sum) over (order by x_month asc, x_year asc rows between unbounded preceding and current row)
from data
I know how to build the main part of the query
$value_of_all_purchases_not_sold = DB::table('coins')
->select(DB::raw('date_trunc(\'month\', purchase_date) AS x_month, date_trunc(\'year\', purchase_date) AS x_year, sum(retail_value) as purchase_price_sum'))
->where('user_email', '=', auth()->user()->email)
->where('sold', '=', 0)
->groupBy('x_month', 'x_year')
->orderBy('x_month', 'asc')
->orderBy('x_year', 'asc')
->get();
but how do you build out the with data as ( and the second select?
I need the data to be cumulative and I'd rather do the calculation in the DB than in PHP.
Laravel doesn't have built-in method(s) for common table expression. You may use a third party package such as this - it has a very good documentation. If you don't want to use an external library, then you need use query builder's select method with bindings such as
$results = DB::select('your-query', ['your', 'bindings']);
return Coin::hydrate($results); // if you want them as collection of Coin instance.

How to link subquery in Laravel QueryBuilder to outer table

I'm looking on how I can have a subquery in Laravel QueryBuilder join with the outer table. For instance, we have a table with Exchange Rates, with a currency and a value_date.
For each currency, we want to be able to fetch the valid exchange rate on a certain valuedate. Since exchange rates do not change on non working days, that means that for saturday and sunday, the value of friday is still valid. A bit of an obvious and standard case.
In SQL, I would query this as follows:
SELECT currency, value_date, rate
FROM exchange_rates er
WHERE value_date = (
SELECT max(value_date)
FROM exchange_rates
WHERE currency = er.currency
AND value_date <= '2019-02-03'
)
This would return a list of all exchange rates (one record for each currency), with the rate that is valid on 2019-02-03, and the associate value date (which would probably be 2019-02-01, since 2019-02-03 is a sunday...)
I have no idea however how I can do this with the Eloquent QueryBuilder without falling back to doing raw sql queries...
$value_date = '2019-02-03';
App\ExchangeRate::where('value_date', function($query) use ($value_date) {
... ??? ...
});
where() can also accept a closure that would allow you to do this:
return ExchangeRate::select('currency', 'value_date', 'rate')
->where('value_date', function($query) {
$query->from('exchange_rates AS er')
->selectRaw('max(value_date)')
->whereColumn('exchange_rates.currency', 'er.currency')
->where('value_date', '<=', '2019-02-03');
})->get();
Here's the output of the query.
Edit: To assign an alias to the outer table, you could do that with the DB Query Builder.
return DB::table('exchange_rates AS alias')
->select('currency', 'value_date', 'rate')
->where('value_date', function($query) {
$query->from('exchange_rates AS er')
->selectRaw('max(value_date)')
->whereColumn('alias.currency', 'er.currency')
->where('value_date', '<=', '2019-02-03');
})->get();

Laravel - Get the last entry of each UID type

I have a table that has 100's of entries for over 1000 different products, each identified by a unique UID.
ID UID MANY COLUMNS CREATED AT
1 dqwdwnboofrzrqww1 ... 2018-02-11 23:00:43
2 dqwdwnboofrzrqww1 ... 2018-02-12 01:15:30
3 dqwdwnbsha5drutj5 ... 2018-02-11 23:00:44
4 dqwdwnbsha5drutj5 ... 2018-02-12 01:15:31
5 dqwdwnbvhfg601jk1 ... 2018-02-11 23:00:45
6 dqwdwnbvhfg601jk1 ... 2018-02-12 01:15:33
...
I want to be able to get the last entry for each UID.
ID UID MANY COLUMNS CREATED AT
2 dqwdwnboofrzrqww1 ... 2018-02-12 01:15:30
4 dqwdwnbsha5drutj5 ... 2018-02-12 01:15:317
6 dqwdwnbvhfg601jk1 ... 2018-02-12 01:15:33
Is this possible in one DB call?
I have tried using DB as well as Eloquent but so far I either get zero results or the entire contents of the Table.
Andy
This is easy enough to handle in MySQL:
SELECT t1.*
FROM yourTable t1
INNER JOIN
(
SELECT UID, MAX(created_at) AS max_created_at
FROM yourTable
GROUP BY UID
) t2
ON t1.UID = t2.UID AND
t1.created_at = t2.max_created_at;
Translating this over to Eloquent would be some work, but hopefully this gives you a good starting point.
Edit: You may want to use a LEFT JOIN if you expect that created_at could ever be NULL and that a given UID might only have null created values.
You can use a self join to pick latest row for each UID
select t.*
from yourTable t
left join yourTable t1 on t.uid = t1.uid
and t.created_at < t1.created_at
where t1.uid is null
Using laravel's query builder it would be similar to
DB::table('yourTable as t')
->select('t.*')
->leftJoin('yourTable as t1', function ($join) {
$join->on('t.uid','=','t1.uid')
->where('t.created_at', '<', 't1.created_at');
})
->whereNull('t1.uid')
->get();
Laravel Eloquent select all rows with max created_at
Laravel Eloquent group by most recent record
SELECT p1.* FROM product p1, product p2 where p1.CREATED_AT> p2.CREATED_AT group by p2.UID
You can achieve this with eloquent using orderBy() and groupBy():
$data = TblModel::orderBy('id','DESC')->groupBy('uid')->get();
SOLVED
Thanks to Tim and M Khalid for their replies. It took me down the right road but I hit a snag, hence why I am posting this solution.
This worked:
$allRowsNeeded = DB::table("table as s")
->select('s.*')
->leftJoin("table as s1", function ($join) {
$join->on('s.uid', '=', 's1.uid');
$join->on('s.created_at', '<', 's1.created_at');
})
->whereNull('s1.uid')
->get();
However I got an Access Violation so I had to go in to config/database.php and set
'strict' => false,
inside the 'mysql' config, which removes ONLY_FULL_GROUP_BY from the SQL_MODE.
Thanks again.
You have to use ORDER BY, and LIMITSQL parameters, which will lead you to an easy SQL request :
for exemple, in SQL you should have something like this :
SELECT *
FROM table_name
ORDER BY `created_at` desc
LIMIT 1
This will returns everything in the table. The results will be ordering by the column "created_at" descending. So the first result will be what you're looking for. Then the "LIMIT" tells to return only the first result, so you won't have all your database.
If you wanna make it with eloquent, here is the code doing the same thing :
$model = new Model;
$model->select('*')->orderBy('created_at')->first();

Eloquent query alternative

How can I write the following in Laravel's Eloquent?
SELECT *
FROM
( SELECT real_estate.property_id,
real_estate.amount_offered,
payee.summa
FROM real_estate
LEFT JOIN
(SELECT property_id,
SUM(amount) AS summa
FROM payments
GROUP BY property_id) payee ON payee.property_id = real_estate.property_id ) yoot
WHERE summa = 0.05 * amount_offered
Been on this for a while now and really can't get around it. Lemme explain the whole cause for the panic.
I have two tables, one for property and another for payments made on those properties. Now at any given time I will like to query for what properties have been paid for to a certain percentage hence the 0.05 which reps 5%. As it is the query works but I need an Eloquent alternative for it. Thanks
Anywhere you have subqueries in your SQL you'll need to use DB::raw with Eloquent. In this case you have a big subquery for the FROM statement, so the easiest way would be to do this:
DB::table(
DB::raw('SELECT real_estate.property_id, real_estate.amount_offered, payee.summa FROM real_estate LEFT JOIN (SELECT property_id, SUM(amount) AS summa FROM payments GROUP BY property_id) payee ON payee.property_id = real_estate.property_id)')
)
->where('summa', DB::raw('0.05 * amount_offered'))->get();
Notice I used DB::raw for the WHERE statment value as well. That's because you are doing a multiplication using a column name, and the value would otherwise be quoted as a string.
If you want to go a step further and build each subquery using Eloquent, then convert it to an SQL string and injecting it using DB::raw, you can do this:
$joinQuery = DB::table('payments')
->select('property_id', 'SUM(amount) AS summa')
->groupBy('property_id')
->toSql();
$tableQuery = DB::table('real_estate')
->select('real_estate.property_id', 'real_estate.amount_offered', 'payee.summa')
->leftJoin(DB::raw('(' . $joinQuery . ')'), function ($join)
{
$join->on('payee.property_id', '=', 'real_estate.property_id');
})
->toSql();
DB::table(DB::raw('(' . $tableQuery . ')'))->where('summa', DB::raw('0.05 * amount_offered'))->get();
In this case, the second approach doesn't have any benefits over the first, except perhaps that it's more readable. However, building subqueries using Eloquent, does have it's benefitfs when you'd need to bind any variable values to the query (such as conditions), because the query will be correctly built and escaped by Eloquent and you would not be prone to SQL injection.

Resources