How to link subquery in Laravel QueryBuilder to outer table - laravel

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();

Related

Convert DB::Select to Query Builder

i has raw query in laravel like this
public function getPopularBook(){
$book = DB::select("
with totalReview as(
SELECT r.book_id , count(r.id)
FROM review r
GROUP BY r.book_id
)
SELECT *
from totalReview x
left JOIN (
SELECT b.*,
case when ((now() >= d.discount_start_date and now() <= d.discount_end_date) or (now() >= d.discount_start_date and d.discount_end_date is null)) then (b.book_price-d.discount_price)
ELSE b.book_price
end as final_price
FROM discount d
right JOIN book b
on d.book_id = b.id
) as y
on x.book_id = y.id
ORDER BY x.count DESC, y.final_price ASC
LIMIT 8"
);
return $book;
}
so when i want to return a paginate, it doesn't work so can i convert this to query build to use paginate
This is a very un-optimized raw query in itself. You are performing too many Join in Subquery just to sort by price
i'm assuming the database table:
books[ id, name, price ]
reviews[ id, book_id ]
discounts[ id, book_id, start_date, end_date, discount_price]
Look how easy it is if you just use Eloquent:
Book::withCount('reviews')->orderBy('reviews_count')->get();
this will give you all the Books order by number of reviews
now with the final price, this can be a bit tricky, let's take a look at a query when we don't consider discount time
Book::withCount('reviews')
->withSum('discounts', 'discount_price') //i'm assuming a book can have many discount at the same time, so i just sum them all
->addSelect(
DB::raw('final_price AS (books.price - discounts_sum_discount_price)')
)
->orderBy('reviews_count', 'asc') // =you can specify ascending or descending
->orderBy('final_price', 'desc') //in laravel chaining multiple orderBy to order multiple column
->get();
I dont even need to use Subquery!! But how do we actually only add the "active" discount?, just need to modify the withSum a bit:
Book::withCount('reviews')
->withSum(
[
'discounts' => function($query) {
$query->where('start_date', '<=', Carbon::now())
->where('end_date', '>=', Carbon::now())
}
],
'discount_price'
)
->addSelect(
DB::raw('final_price AS (books.price - discounts_sum_discount_price)')
)
->orderBy('reviews_count', 'asc') // =you can specify ascending or descending
->orderBy('final_price', 'desc') //in laravel chaining multiple orderBy to order multiple column
->get();
and it is done
What about pagination? just replace the get() method with paginate():
Book::withCount('reviews')
->withSum(['discounts' => function($query) {
$query->where('start_date', '<=', Carbon::now())->where('end_date', '>=', Carbon::now())
}],'discount_price')
->addSelect(DB::raw('final_price AS (books.price - discounts_sum_discount_price)')) //just format to be a bit cleaner, nothing had changed
->orderBy('reviews_count', 'asc')
->orderBy('final_price', 'desc')
->paginate(10); //10 books per page
WARNING: this is written with ELoquent ORM, not QueryBuilder, so you must define your relationship first

How to get the default price of the product with different prices from different date periods from the period closest to today?

my sqlfiddle eample
Hello there,
according to the above sqlfiddle example;
I have a table A where the products are listed and a table B with different prices for different periods associated with these products.
Here I show these prices according to the date the user has chosen. There is no problem.
However, if the user has not selected a date, I cannot show the price of the period closest to today by default.
In the example I gave, the sql query does this successfully, but I cannot write it successfully in the form of laravel query. Or as an Eloquent orm query
How can I do that?
$query->select(['tableA.*', 'tableB.start_date', 'tableB.end_date', 'tableB.price'])
->join('tableB', function($join) {
$join->on('tableA.id', '=', 'tableB.pro_id');
})->where(function($sq) use ($postFrom) {
$sq->when($postFrom[0]=='0', function ($syq) {
$syq->whereRaw('DAYOFYEAR(curdate()) <= DAYOFYEAR(tableB.end_date)');
}, function ($stq) use ($postFrom) {
$stq->whereDate('tableB.start_date', '<=', $postFrom[0])
->whereDate('tableB.end_date', '>=', $postFrom[0]);
});
})->orWhere(function($ssq) use ($postTo) {
$ssq->whereDate('tableB.start_date', '<=', $postTo[0])
->whereDate('tableB.end_date', '>=', $postTo[0]);
})->groupBy('tableA.id')->orderBy('tableB.price', $sortDirection);
note1: $postFrom and $postTo are the start and end dates from the user. If the user did not submit a date, $postFrom is displayed as 0.
note2: I show the default price when the $postFrom[0] == '0' condition is met.
note3: The '2021-03-07' value in the sqlfiddle example is used for example instead of the dynamic present value.
note4: According to this query, it takes the price value of the first period as default. But that's not what I want.
note5: I can't use 'joinSub' because Laravel version is 5.5.
note6:In the example I want to convert to Laravel Query form, the sql query that works without any problems:
select `tableA`.*, `tableB`.`start_date`, `tableB`.`end_date`, `tableB`.`price`
from `tableA`
right join(
SELECT id, start_date, end_date, pro_id, price, DATEDIFF(`tableB`.`end_date`, '2021-03-07') diff
FROM `tableB` GROUP BY id order by diff asc
) `tableB` on `tableA`.`id` = `tableB`.`pro_id` where (date(`end_date`) >= '2021-03-07')
group by `tableA`.`id` order by `price` desc
This is an equivalent query of your query. I haven't executed.
If Laravel Version is greater then 5.5
$query1 = DB::table('tableB')
->selectRaw("id, start_date, end_date, pro_id, price, DATEDIFF(end_date, '2021-03-07') AS diff")
->groupBy('id')->orderBy('diff','ASC');
TableA::select('tableA.*', 'tableB.start_date', 'tableB.end_date', 'tableB.price')
->joinSub($query1, 'tableB', function ($join)
{
$join->on('tableA.id', '=', 'tableB.pro_id');
})
->whereDate('tableB.end_date','>=','2021-03-07')
->groupBy('tableA.id')->orderBy('price','DESC')->get();
For Laravel 5.5
TableA::select('tableA.*', 'tableB.start_date', 'tableB.end_date', 'tableB.price')
->join(DB::raw("(SELECT id, start_date, end_date, pro_id, price,
DATEDIFF(`tableB`.`end_date`, '2021-03-07') diff
FROM `tableB` GROUP BY id order by diff asc) table2 "), function ($join)
{
$join->on('tableA.id', '=', 'table2.pro_id');
})
->whereDate('table2.end_date','>=','2021-03-07')
->groupBy('tableA.id')->orderBy('price','DESC')->get();

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.

Laravel eloquent get the latest rows of grouped rows

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

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