Why do Eloquent has and whereHas methods use subqueries instead of joins? - php

Why does Laravel use subqueries for the Eloquent has and whereHas methods instead of using joins? For example, I have constructed an Eloquent query like this:
Order::whereHas('orderProducts', function($query)
{
$query->where('product_id', $this->id);
})
->whereHas('status', function($query)
{
$query->where('sales_data', 1);
})
->where('ext_created_at', '>=', $startDate)
->where('ext_created_at', '<=', $endDate.' 23:59:59')
->distinct()
->count('id');
The resulting query is:
select count(distinct `id`) as aggregate from `orders` where `orders`.`deleted_at` is null and (select count(*) from `order_products` where `order_products`.`order_id` = `orders`.`id` and `product_id` = ? and `order_products`.`deleted_at` is null) >= 1 and (select count(*) from `order_statuses` where `orders`.`order_status_id` = `order_statuses`.`id` and `sales_data` = ? and `order_statuses`.`deleted_at` is null) >= 1 and `ext_created_at` >= ? and `ext_created_at` <= ?
This query returns a result of 16. According to DB::getQueryLog(), this takes 23222.14 ms to execute.
I then constructed another query like this:
DB::table('orders')
->leftJoin('order_products', 'orders.id', '=', 'order_products.order_id')
->leftJoin('order_statuses', 'orders.order_status_id', '=', 'order_statuses.id')
->whereNull('orders.deleted_at')
->where('order_products.product_id', $this->id)
->where('orders.ext_created_at', '>=', $startDate)
->where('orders.ext_created_at', '<=', $endDate.' 23:59:59')
->whereNull('order_products.deleted_at')
->where('order_statuses.sales_data', 1)
->distinct()
->count('orders.id')
The resulting query is:
select count(distinct `orders`.`id`) as aggregate from `orders` left join `order_products` on `orders`.`id` = `order_products`.`order_id` left join `order_statuses` on `orders`.`order_status_id` = `order_statuses`.`id` where `orders`.`deleted_at` is null and `order_products`.`product_id` = ? and `orders`.`ext_created_at` >= ? and `orders`.`ext_created_at` <= ? and `order_products`.`deleted_at` is null and `order_statuses`.`sales_data` = ?
This query also returns 16, but takes 3.52 ms to run. Obviously, using joins has much better performance here, but is that only specific to my use case, or are joins generally better performance? If so, why wouldn't Eloquent use joins over subqueries? I would much rather work with models and make use of all the excellent Eloquent methods like has and whereHas, but the performance is so dramatically different that I am being forced to construct the query myself using the Query Builder.
Am I missing something here?
I am using Laravel Framework version 4.2.16.

Related

Eloquent With Nested Where Clauses

I am curious if there is a way using Eloquent's query builder to nest where clauses or if I should just run a raw DB query.
Here is the raw query:
SELECT * FROM `inventory` WHERE (`sold_date` > '2020-12-31' OR `sold_date` IS NULL) AND (`removed_date` > '2020-12-31' OR `removed_date` IS NULL) AND `category` <> 1 AND `purchased_date` <= '2020-12-31'
In Laravel Eloquent you can use the below query:
$inventory = Inventory::where(function($query) {
$query->where('sold_date', '>', '2020-12-31')->orWhereNull('sold_date');
})->where(function($query) {
$query->where('removed_date', '>', '2020-12-31')->orWhereNull('removed_date');
})->where('category', '<>', 1)->where('purchased_date', '<=', '2020-12-31')
->order('id', 'DESC')
->get();
Yes you can pass an array with conditions into to Eloquent's where() and have multiple where()s.
See this answer (including the comments) for how you could build your query: https://stackoverflow.com/a/27522556/4517964
You can try this
Inventory ::where(function($query) use ($d1){
$query->where('solid_date','=',$d1)
->orWhereNull('solid_date');
})->where(function($query2) use ($da1){
$query2->where('removed_date','=',$da1)
->orWhereNull('removed_date');
})->where(function($query3) use (){
$query2->where('category','<>',1)
->where('purchased_date','=','2020-12-31');
}}->get();
I perfer to use parameter in few functions may be you will need it otherwise you can hard code it like I did in the last function

Create subquery in NOT IN

I am using Laravel Framework 6.16.0.
I have the following sql query:
SELECT DISTINCT
`companies`.*
FROM
`companies`
LEFT JOIN `trx` ON `trx`.`companies_id` = `companies`.`id`
WHERE
`trx`.`transaction_date` >= 2020-11-12 AND companies.symbol NOT IN (SELECT DISTINCT
companies.symbol
FROM
`companies`
LEFT JOIN articles a ON a.companies_id = companies.id
WHERE
a.created_at >= 2020-11-12
ORDER BY
created_at
DESC)
ORDER BY
transaction_date
DESC
I have created the following eloquent query:
DB::connection('mysql_prod')->table('companies')->select('companies.symbol')
->leftJoin('trx', 'trx.companies_id', '=', 'companies.id')
->where('trx.transaction_date', '>=', Carbon::today()->subDays(1)->startOfDay())
->orderBy('transaction_date', 'desc')
->distinct()
->get('symbol');
However, I am not sure how to pack the in my eloquent query to get all the symbol back that should be excluded.
I highly appreciate your replies!
You should try something like this:
$date = Carbon::today()->subDays(1)->startOfDay();
DB::connection('mysql_prod')->table('companies')->select('companies.symbol')
->leftJoin('trx', 'trx.companies_id', '=', 'companies.id')
->where('trx.transaction_date', '>=', $date)
->whereNotIn('companies.symbol', function ($q) use ($date) => {
$q->select('companies.symbol')
->from('companies')
->leftJoin('articles', 'articles.companies_id', 'companies.id')
->where('articles.created_at', '>', $date)
->distinct()
->get()
})
->orderBy('transaction_date', 'desc')
->distinct()
->get();
It will provide a similar query as you mentioned.
Reference from here.
Also, you can read how to write sub Query from Laravel docs.
Check this one more good answer for that what you need.

laravel eloquent ->whereHas - write your own exists( subquery )

Laravel Eloquent ->whereHas() uses anexists() subquery - https://dev.mysql.com/doc/refman/8.0/en/exists-and-not-exists-subqueries.html - in order to return your results.
I would like to write my own subquery, but I do not know how to tell Eloquent to ->where it.
If I do:
$query->where( DB::raw(' exists( subquery ) ')
Laravel instead writes the subquery as:
where exists( subquery ) is null
So I'm just wondering what $query->method() could be used to add an exists() subquery to the 'where' statements. The subquery would be just the same kind that laravel generates, but written out:
... and exists ( select * from `tbl` inner join `assets` on `custom_assets`.`id` = `tbl`.`asset_id` where `assets`.`deleted_at` is null and `users`.`id` = `assets`.`client_id` and `field_id` = ? and (`value` = ? and `assets`.`deleted_at` is null )
Use whereRaw():
$query->whereRaw('exists( subquery )')
Read WhereHas Description Here
You can find this code example there. You can also add a closure for you custom query in whereHas.
// Retrieve all posts with at least one comment containing words like foo%
$posts = App\Post::whereHas('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();

convert SQL query to query builder style

Im trying days to understand how I can convert a SQL query to a query builder style in laravel.
My SQL query is:
$tagid = Db::select("SELECT `id` FROM `wouter_blog_tags` WHERE `slug` = '".$this->param('slug')."'");
$blog = Db::select("SELECT *
FROM `wouter_blog_posts`
WHERE `published` IS NOT NULL
AND `published` = '1'
AND `published_at` IS NOT NULL
AND `published_at` < NOW()
AND (
SELECT count( * )
FROM `wouter_blog_tags`
INNER JOIN `wouter_blog_posts_tags` ON `wouter_blog_tags`.`id` = `wouter_blog_posts_tags`.`tags_id`
WHERE `wouter_blog_posts_tags`.`post_id` = `wouter_blog_posts`.`id`
AND `id`
IN (
'".$tagid[0]->id."'
)) >=1
ORDER BY `published_at` DESC
LIMIT 10
OFFSET 0");
Where I now end up to convert to the query builder is:
$test = Db::table('wouter_blog_posts')
->where('published', '=', 1)
->where('published', '=', 'IS NOT NULL')
->where('published_at', '=', 'IS NOT NULL')
->where('published_at', '<', 'NOW()')
->select(Db::raw('count(*) wouter_blog_tags'))
->join('wouter_blog_posts_tags', function($join)
{
$join->on('wouter_blog_tags.id', '=', 'wouter_blog_posts_tags.tags_id')
->on('wouter_blog_posts_tags.post_id', '=', 'wouter_blog_posts.id')
->whereIn('id', $tagid[0]->id);
})
->get();
I have read that I can't use whereIn in a join. The error i now get:
Call to undefined method Illuminate\Database\Query\JoinClause::whereIn()
I realy dont know how I can convert my SQL to query builder. I hope when I see a good working conversion of my query I can understand how I have to do it next time.
This work for me:
DB::table('wouter_blog_posts')
->whereNotNull('published')
->where('published', 1)
->whereNotNull('published_at')
->whereRaw('published_at < NOW()')
->whereRaw("(SELECT count(*)
FROM wouter_blog_tags
INNER JOIN wouter_blog_posts_tags ON wouter_blog_tags.id = wouter_blog_posts_tags.tags_id
WHERE wouter_blog_posts_tags.post_id = wouter_blog_posts.id
AND id
IN (
'".$tagid."'
)) >=1")
->orderBy('published_at', 'desc')
->skip(0)
->take(10)
->paginate($this->property('postsPerPage'));
The following Query Builder code will give you the exact SQL query you have within your DB::select:
DB::table('wouter_blog_posts')
->whereNotNull('published')
->where('published', 1)
->whereNotNull('published_at')
->whereRaw('`published_at` < NOW()')
->where(DB::raw('1'), '<=', function ($query) use ($tagid) {
$query->from('wouter_blog_tags')
->select('count(*)')
->join('wouter_blog_posts_tags', 'wouter_blog_tags.id', '=', 'wouter_blog_posts_tags.tags_id')
->whereRaw('`wouter_blog_posts_tags`.`post_id` = `wouter_blog_posts`.`id`')
->whereIn('id', [$tagid[0]->id]);
})
->orderBy('published_at', 'desc')
->skip(0)
->take(10)
->get();
The subquery condition had to be reversed because you can't have a subquery as the first parameter of the where method and still be able to bind the condition value. So it's 1 <= (subquery) which is equivalent to (subquery) >= 1. The query generated by the above code will look like this:
SELECT *
FROM `wouter_blog_posts`
WHERE `published` IS NOT NULL
AND `published` = 1
AND `published_at` IS NOT NULL
AND `published_at` < Now()
AND 1 <= (SELECT `count(*)`
FROM `wouter_blog_tags`
INNER JOIN `wouter_blog_posts_tags`
ON `wouter_blog_tags`.`id` =
`wouter_blog_posts_tags`.`tags_id`
WHERE `wouter_blog_posts_tags`.`post_id` =
`wouter_blog_posts`.`id`
AND `id` IN ( ? ))
ORDER BY `published_at` DESC
LIMIT 10 offset 0
My process when creating more complex queries is to first create them and try them out in a SQL environment to make sure they work as indended. Then I implement them step by step with the Query Builder, but instead of using get() at the end of the query, I use toSql() which will give me a string representation of the query that will be generated by the Query Builder, allowing me to compare that to my original query to make sure it's the same.

Different results using same query with DB::raw and Eloquent

I'm getting some unexpected results when running an Eloquent join query. I get two different results from using the exact same query. One running with DB::raw(), the second with Eloquent.
In the Eloquent query, the Users that matches the
where squad_user.leave_time >= seasons.start_time
are missing and will not be included in the result set. The users that matches the
or squad_user.leave is null
will be included, however.
That's the only difference in the results from the two queries. The raw query actually produces the desired result set.
What really puzzles me is, if I check the query logs, both Laravel's and MySQL, I get the exact same query when running both the raw and Eloquent query.
Raw query (the actual query i get from the query log when running the Eloquent query)
return \DB::select(\DB::raw('
select users.*
from users
inner join squad_user on users.id = squad_user.user_id
inner join seasons on squad_user.squad_id = seasons.squad_id
where squad_user.join_time <= seasons.end_time
and (squad_user.leave_time >= seasons.start_time or squad_user.leave_time is null)
and seasons.id = :seasonId
'),
['seasonId' => 3]
);
Eloquent query
return User::join('squad_user', 'users.id', '=', 'squad_user.user_id')
->join('seasons', 'squad_user.squad_id', '=', 'seasons.squad_id')
->where('squad_user.join_time', '<=', 'seasons.end_time')
->where(function ($query)
{
$query->where('squad_user.leave_time', '>=', 'seasons.start_time')
->orWhereNull('squad_user.leave_time');
})
->where('seasons.id', 3)
->get(['users.*']);
Laravel's Eloquent query log
select `users`.*
from `users`
inner join `squad_user` on `users`.`id` = `squad_user`.`user_id`
inner join `seasons` on `squad_user`.`squad_id` = `seasons`.`squad_id`
where `squad_user`.`join_time` <= seasons.end_time
and (`squad_user`.`leave_time` >= seasons.start_time or `squad_user`.`leave_time` is null)
and `seasons`.`id` = 3
{"bindings":["seasons.end_time","seasons.start_time",3],"time":0.38,"name":"mysql"}
MySQL's general_log on the Eloquent query
select `users`.*
from `users`
inner join `squad_user` on `users`.`id` = `squad_user`.`user_id`
inner join `seasons` on `squad_user`.`squad_id` = `seasons`.`squad_id`
where `squad_user`.`join_time` <= ?
and (`squad_user`.`leave_time` >= ? or `squad_user`.`leave_time` is null)
and `seasons`.`id` = ?
MySQL's general_log on the Raw query
select users.*
from users
inner join squad_user on users.id = squad_user.user_id
inner join seasons on squad_user.squad_id = seasons.squad_id
where squad_user.join_time <= seasons.end_time
and (squad_user.leave_time >= seasons.start_time or squad_user.leave_time is null)
and seasons.id = ?
I would appreciate any pointers here, as I am very lost.
where binds 3rd param and treats it usually as a string, unless you tell it not to by using raw statement. DB::raw or whereRaw will work for you:
return User::join('squad_user', 'users.id', '=', 'squad_user.user_id')
->join('seasons', 'squad_user.squad_id', '=', 'seasons.squad_id')
->where('squad_user.join_time', '<=', DB::raw('seasons.end_time'))
->where(function ($query)
{
$query->where('squad_user.leave_time', '>=', DB::raw('seasons.start_time'))
->orWhereNull('squad_user.leave_time');
})
->where('seasons.id', 3)
->get(['users.*']);
Since Laravel Verion 5.2 you can also use whereColumn to verify that two columns are equal (or pass a comparison operator to the method):
->whereColumn('squad_user.join_time', '<=', 'seasons.end_time')

Categories