Laravel 的 whereHas 表现不佳
Posted
技术标签:
【中文标题】Laravel 的 whereHas 表现不佳【英文标题】:Poor whereHas performance in Laravel 【发布时间】:2018-03-28 20:58:43 【问题描述】:我想将where
条件应用于关系。这是我的工作:
Replay::whereHas('players', function ($query)
$query->where('battletag_name', 'test');
)->limit(100);
它会生成以下查询:
select * from `replays`
where exists (
select * from `players`
where `replays`.`id` = `players`.`replay_id`
and `battletag_name` = 'test')
order by `id` asc
limit 100;
在 70 秒内执行。如果我像这样手动重写查询:
select * from `replays`
where id in (
select replay_id from `players`
where `battletag_name` = 'test')
order by `id` asc
limit 100;
它在 0.4 秒内执行。如果where exists
这么慢,为什么它是默认行为?有没有办法使用查询生成器生成正确的where in
查询,还是我需要注入原始 SQL?也许我做错了什么?
replays
表有 4M 行,players
有 40M 行,所有相关列都被索引,数据集不适合 mysql 服务器内存。
更新:发现正确的查询可以生成为:
Replay::whereIn('id', function ($query)
$query->select('replay_id')->from('players')->where('battletag_name', 'test');
)->limit(100);
还有一个问题,为什么exists
表现如此糟糕,为什么它是默认行为
【问题讨论】:
我建议不要选择 * .. 尝试选择特定属性而不是全部。 我需要为我的案例选择所有这些。即使只选择id
列,查询性能也会提高不到 1%,因此可以忽略不计
***.com/a/24932/916000 将帮助您了解差异。
【参考方案1】:
我认为性能不取决于 whereHas 仅取决于您选择了多少条记录
另外尝试优化你的mysql服务器
https://dev.mysql.com/doc/refman/5.7/en/optimize-overview.html
同时优化你的 php 服务器
如果你有更快的查询,为什么不使用来自 larval 的原始查询对象
$replay = DB::select('select * from replays where id in (
select replay_id from players where battletag_name = ?)
order by id asc limit 100', ['test']
);
【讨论】:
由于limit
子句,两个查询都恰好选择了 100 行。 whereHas 在 70 秒内完成,而 whereIn 在 0.4 秒内完成。优化与问题无关,因为它们会减少两个查询的执行时间。
那么也许你可以使用上面提到的原始查询
实际应用程序中的查询比有很多条件的查询要复杂得多,我真的需要查询构建器。从大量原始字符串部分构建它会将我的代码变成意大利面条。【参考方案2】:
这与mysql有关,与laravel无关。您可以使用 joins 和 subqueries 这两个选项执行上述操作。 子查询通常比连接慢得多。
子查询是:
不那么复杂 优雅 更容易理解 更容易编写 逻辑分离以上事实就是为什么像 eloquent 这样的 ORM 使用 suquries。 但速度较慢!尤其是当数据库中有很多行时。
您的查询的加入版本是这样的:
select * from `replays`
join `players` on `replays`.`id` = `players`.`replay_id`
and `battletag_name` = 'test'
order by `id` asc
limit 100;
但是现在您必须更改 select 和 add group by 并在许多其他事情上小心,但为什么会这样,所以它超出了这个答案。新查询将是:
select replays.* from `replays`
join `players` on `replays`.`id` = `players`.`replay_id`
and `battletag_name` = 'test'
order by `id` asc
group by replays.id
limit 100;
这就是为什么加入更复杂的原因。
您可以在 laravel 中编写原始查询,但对连接查询的 eloquent 支持没有得到很好的支持,也没有多少包可以帮助您,例如:https://github.com/fico7489/laravel-eloquent-join
【讨论】:
我认为第一个查询如此慢的主要原因是索引到replay_id
字段,因为它只请求ID,并且在它使where has clausule 之后
可能是这样,但是 whereHas 确实比 join 慢得多......
我真的同意whereHas()
比蜗牛慢,尤其是在您处理与中间表的关系时。如果您要处理大量记录,建议简单地使用联接。如果您担心您的代码会有非 eloquent 查询,您可以将这个查询封装到它自己的类中 + 使用 DB
查询生成器就可以了。【参考方案3】:
你可以使用左连接
$replies = Replay::orderBy('replays.id')
->leftJoin('players', function ($join)
$join->on('replays.id', '=', 'players.replay_id');
)
->take(100)
->get();
【讨论】:
【参考方案4】:试试这个:
mpyw/eloquent-has-by-non-dependent-subquery: Convert has() and whereHas() constraints to non-dependent subqueries. mpyw/eloquent-has-by-join: Convert has() and whereHas() constraints to join() ones for single-result relations.Replay::hasByNonDependentSubquery('players', function ($query)
$query->where('battletag_name', 'test');
)->limit(100);
就是这样。祝你生活愉快!
【讨论】:
【参考方案5】:whereHas 在没有索引的表上性能很差,把索引放在上面就开心了!
Schema::table('category_product', function (Blueprint $table)
$table->index(['category_id', 'product_id']);
);
【讨论】:
【参考方案6】:WhereHas() 查询真的和懒惰的乌龟一样慢,所以我创建并仍在使用我粘合到任何需要简单连接请求的 laravel 模型的特征。这个特征产生了一个scope function whereJoin()。您可以只传递一个连接模型类名称,where 子句参数并享受。此特征负责查询中的表名和相关详细信息。好吧,它仅供我个人使用,并且可以随意修改这个怪物。
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
/** @mixin Model */
trait ModelJoinTrait
/**
* @param string|\Countable|array $on
* @param $column
* @param $whereOperator
* @param $value
* @param Model $exemplar
* @return array
*/
function _modelJoinTraitJoinPreset($on, $column, $whereOperator, $value, $exemplar)
$foreignTable = $exemplar->getTable();
$foreignId = $exemplar->getKeyName();
$localTable = $this->getTable();
$localId = $this->getKeyName();
//set up default join and condition parameters
$joinOn =[
'local' => $localTable.'.'.$localId,
'foreign'=> $foreignTable.'.'.$foreignId,
'operator' => '=',
'type'=>'inner',
'alias'=>'_joint_id',
'column'=>$column,
'where_operator'=>$whereOperator,
'value'=>$value
];
//config join parameters based on input
if(is_string($on))
//if $on is string it treated as foreign key column name for join clause
$joinOn['foreign'] = $foreignTable.'.'.$on;
elseif (is_countable($on))
//if $is array or collection there can be join parameters
if(isset($on['local']) && $on['local'])
$joinOn['local'] = $localTable.'.'.$on['local'];
if(isset($on['foreign']) && $on['foreign'])
$joinOn['foreign'] = $localTable.'.'.$on['foreign'];
if(isset($on['operator']) && $on['operator'])
$joinOn['operator'] = $on['operator'];
if(isset($on['alias']) && $on['alias'])
$joinOn['alias'] = $on['alias'];
//define join type
$joinTypeArray = ['inner', 'left', 'right', 'cross'];
if(is_countable($on) && isset($on['type']) && in_array($on['type'], $joinTypeArray))
$joinOn = $on['type'];
return $joinOn;
/**
* @param Model $exemplar
* @param string|array|\Countable $joinedColumns
* @param string|array|\Countable $ownColumns
* @param string $jointIdAlias
* @return array
*/
function _modelJoinTraitSetColumns($exemplar, $joinedColumns, $ownColumns, $jointIdAlias = '_joint_id')
$foreignTable = $exemplar->getTable();
$foreignId = $exemplar->getKeyName();
$localTable = $this->getTable();
$localId = $this->getKeyName();
if(is_string($joinedColumns))
$foreignColumn = ["$foreignTable.$joinedColumns"];
else if(is_countable($joinedColumns))
$foreignColumn = array_map(function ($el) use ($foreignTable)
return "$foreignTable.$el";
, $joinedColumns);
else
$foreignColumn = ["$foreignTable.*"];
if(is_string($ownColumns))
$ownColumns = ["$localTable.$ownColumns"];
elseif(is_countable($ownColumns))
$ownColumns = array_map(function ($el) use ($localTable)
return "$localTable.$el";
, $ownColumns);
else
$ownColumns = ["$localTable.*"];
$columns = array_merge($foreignColumn, $ownColumns);
if($foreignId == $localId)
$columns = array_merge(["$foreignTable.$foreignId as $jointIdAlias"], $columns);
return $columns;
/**
* @param Builder $query
* @param string|array|\Countable $on
* @param Model $exemplar
*/
function _modelJoinTraitJoinPerform($query, $on, $exemplar)
$funcTable = ['left'=>'leftJoin', 'right'=>'rightJoin', 'cross'=>'crossJoin', 'inner'=>'join'];
$query->$funcTable[$on['type']]($exemplar->getTable(),
function(JoinClause $join) use ($exemplar, $on)
$this->_modelJoinTraitJoinCallback($join, $on);
);
function _modelJoinTraitJoinCallback(JoinClause $join, $on)
$query = $this->_modelJoinTraitJoinOn($join, $on);
$column = $on['column'];
$operator = $on['where_operator'];
$value = $on['value'];
if(is_string($column))
$query->where($column, $operator, $value);
else if(is_callable($column))
$query->where($column);
/**
* @param JoinClause $join
* @param array|\Countable $on
* @return JoinClause
*/
function _modelJoinTraitJoinOn(JoinClause $join, $on)
//execute join query on given parameters
return $join->on($on['local'], $on['operator'], $on['foreign']);
/**
* A scope function used on Eloquent models for inner join of another model. After connecting trait in target class
* just use it as ModelClass::query()->whereJoin(...). This query function forces a select() function with
* parameters $joinedColumns and $ownColumns for preventing overwrite primary key on resulting model.
* Columns of base and joined models with same name will be overwritten by base model
*
* @param Builder $query Query given by Eloquent mechanism. It's not exists in
* ModelClass::query()->whereJoin(...) function.
* @param string $class Fully-qualified class name of joined model. Should be descendant of
* Illuminate\Database\Eloquent\Model class.
* @param string|array|\Countable $on Parameter that have join parameters. If it is string, it should be foreign
* key in $class model. If it's an array or Eloquent collection, it can have five elements: 'local' - local key
* in base model, 'foreign' - foreign key in joined $class model (default values - names of respective primary keys),
* 'operator' = comparison operator ('=' by default), 'type' - 'inner', 'left', 'right' and 'cross'
* ('inner' by default) and 'alias' - alias for primary key from joined model if key name is same with key name in
* base model (by default '_joint_id')
* @param Closure|string $column Default Eloquent model::where(...) parameter that will be applied to joined model.
* @param null $operator Default Eloquent model::where(...) parameter that will be applied to joined model.
* @param null $value Default Eloquent model::where(...) parameter that will be applied to joined model.
* @param string[] $joinedColumns Columns from joined model that will be joined to resulting model
* @param string[] $ownColumns Columns from base model that will be included in resulting model
* @return Builder
* @throws \Exception
*/
public function scopeWhereJoin($query, $class, $on, $column, $operator = null, $value=null,
$joinedColumns=['*'], $ownColumns=['*'])
//try to get a fake model of class to get table name and primary key name
/** @var Model $exemplar */
try
$exemplar = new $class;
catch (\Exception $ex)
throw new \Exception("Cannot take out data of '$class'");
//preset join parameters and conditions
$joinOnArray = $this->_modelJoinTraitJoinPreset($on, $column, $operator, $value, $exemplar);
//set joined and base model columns
$selectedColumns = $this->_modelJoinTraitSetColumns($exemplar, $joinedColumns, $ownColumns, $joinOnArray['alias']);
$query->select($selectedColumns);
//perform join with set parameters;
$this->_modelJoinTraitJoinPerform($query, $joinOnArray, $exemplar);
return $query;
您可以这样使用它(示例中的模型商品有一个专用的扩展数据模型 GoodsData 与它们之间的 hasOne 关系):
$q = Goods::query();
$q->whereJoin(GoodsData::class, 'goods_id',
function ($q) //where clause callback
$q->where('recommend', 1);
);
//same as previous exmple
$q->whereJoin(GoodsData::class, 'goods_id',
'recommend', 1); //where clause params
// there we have sorted columns from GoodsData model
$q->whereJoin(GoodsData::class, 'goods_id',
'recommend', 1, null, //where clause params
['recommend', 'discount']); //selected columns
//and there - sorted columns from Goods model
$q->whereJoin(GoodsData::class, 'goods_id',
'recommend', '=', 1, //where clause params
['id', 'recommend'], ['id', 'name', 'price']); //selected columns from
//joined and base model
//a bit more complex example but still same. Table names is resolved
//by trait from relevant models
$joinData = [
'type'=>'inner' // inner join `goods_data` on
'local'=>'id', // `goods`.`id`
'operator'=>'=' // =
'foreign'=>'goods_id', // `goods_data`.`goods_id`
];
$q->whereJoin(GoodsData::class, $joinData,
'recommend', '=', 1, //where clause params
['id', 'recommend'], ['id', 'name', 'price']); //selected columns
return $q->get();
生成的 SQL 查询将是这样的
select
`goods_data`.`id` as `_joint_id`, `goods_data`.`id`, `goods_data`.`recommend`,
`goods`.`id`, `goods`.`name`, `goods`.`price` from `goods`
inner join
`goods_data`
on
`goods`.`id` = `goods_data`.`goods_id`
and
-- If callback used then this block will be a nested where clause
-- enclosed in parenthesis
(`recommend` = ? )
-- If used scalar parameters result will be like this
`recommend` = ?
-- so if you have complex queries use a callback for convenience
你的情况应该是这样的
$q = Replay::query();
$q->whereJoin(Player::class, 'replay_id', 'battletag_name', 'test');
//or
$q->whereJoin(Player::class, 'replay_id',
function ($q)
$q->where('battletag_name', 'test');
);
$q->limit(100);
为了更有效地使用它,你可以这样:
// Goods.php
class Goods extends Model
use ModelJoinTrait;
//
public function scopeWhereData($query, $column, $operator = null,
$value = null, $joinedColumns = ['*'], $ownColumns = ['*'])
return $query->whereJoin(
GoodsData::class, 'goods_id',
$column, $operator, $value,
$joinedColumns, $ownColumns);
// -------
// any.php
$query = Goods::whereData('goods_data_column', 1)->get();
PS 我没有为此运行任何自动化测试,所以使用时要小心。在我的情况下它工作得很好,但在你的情况下可能会有意想不到的行为。
【讨论】:
【参考方案7】:laravel has(whereHas)
有时慢的原因是用 where exists 语法实现的。
例如:
// User hasMany Post
Users::has('posts')->get();
// Sql: select * from `users` where exists (select * from `posts` where `users`.`id`=`posts`.`user_id`)
'exists'语法是循环到外部表,然后每次查询内部表(subQuery)。
但是当users表有大量数据时会出现性能问题,因为上面的sqlselect * from 'users' where exists...
无法使用索引。
这里可以用where in
代替where exists
而不破坏结构。
// select * from `users` where exists (select * from `posts` where `users`.`id`=`posts`.`user_id`)
// =>
// select * from `users` where `id` in (select `posts`.`user_id` from `posts`)
这将大大提高性能!
我建议你试试这个包hasin,在上面的例子中,你可以使用hasin
而不是has
。
// User hasMany Post
Users::hasin('posts')->get();
// Sql: select * from `users` where `id` in (select `posts`.`user_id` from `posts`)
hasin
与框架 has
相比,只是使用了 where in 语法而不是 where exists,但其他地方都相同,例如 参数和调用方式连代码实现,都可以放心使用。
【讨论】:
以上是关于Laravel 的 whereHas 表现不佳的主要内容,如果未能解决你的问题,请参考以下文章
Laravel - whereHas 查询没有返回正确的记录
在 Laravel 5 中合并 'with' 和 'whereHas'
Laravel:具有 whereHas 和多对多关系的全局范围
Laravel Eloquent multiple whereHas on relationship where column 'order' 高于前一个 whereHas