繁体   English   中英

Laravel 中的 whereHas 性能不佳

[英]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有关。 您可以使用这两个选项joinssubqueries 执行上述操作 子查询通常比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`)

这将大大提高性能!

你可以看看这个包HASINhasin将只使用其中的语法,而不是在那里存在比较与框架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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM