[英]Poor whereHas performance in Laravel
我想对关系应用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
表现如此糟糕,为什么它是默认行为
试试这个:
Replay::hasByNonDependentSubquery('players', function ($query) {
$query->where('battletag_name', 'test');
})->limit(100);
仅此而已。 快乐的雄辩人生!
这与mysql而不是laravel有关。 您可以使用这两个选项joins和subqueries 执行上述操作。 子查询通常比joins慢得多。
子查询是:
以上事实就是为什么像 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-雄辩的加入
WhereHas() 查询真的和懒龟一样慢,所以我创建并仍然使用一个特性,我粘在任何需要简单连接请求的 Laravel 模型上。 这个特性创建了一个作用域函数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;
}
}
您可以像这样使用它(示例中的 Model Goods 有一个专用的扩展数据模型 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 我没有为此运行任何自动化测试,所以在使用时要小心。 在我的情况下它工作得很好,但在你的情况下可能会有意想不到的行为。
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表有大量数据时会出现性能问题,因为上面的sql select * 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
,但在其他地方是一样的,比如参数和呼叫模式甚至是代码实现,并且可安全使用.
我认为性能不取决于 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']
);
您可以使用左连接
$replies = Replay::orderBy('replays.id')
->leftJoin('players', function ($join) {
$join->on('replays.id', '=', 'players.replay_id');
})
->take(100)
->get();
whereHas 在没有索引的表上性能很差,把索引放在上面,开心吧!
Schema::table('category_product', function (Blueprint $table) {
$table->index(['category_id', 'product_id']);
});
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.