加速Laravel Eloquent / MySQL查询

时间:2016-05-21 18:32:36

标签: mysql eloquent relational-database

我有一个当前正在运行 700ms 的Eloquent查询,它只会随着我向用户帐户添加更多网站而增加。我试图了解优化它的最佳方法是什么,以便它可以更快地运行。

我真的不想保存"结果"我的计算然后只是稍后在较小的查询中获取它们,因为它们可以随时更新,这意味着它们在100%的时间内都不准确。虽然我很确定会加快查询速度,但我并不想牺牲准确性而不是性能。

这基本上是运行的原始查询:

select  *
    from  
      ( SELECT  `positions`.*,
                @rank := IF(@group = keyword_id, @rank+1,
                                   1) as rank_e0686ae02a55b8ad75aec0c7aaec0a21,
                @group := keyword_id as group_e0686ae02a55b8ad75aec0c7aaec0a21
            from  
              ( SELECT  @rank:=0, @group:=0 ) as vars,
            positions
            order by  `keyword_id` asc, `created_at` desc
      ) as positions
    where  `rank_e0686ae02a55b8ad75aec0c7aaec0a21` <= '2'
      and  `positions`.`keyword_id` in ('hundreds of IDs listed here')

使用解决方案mentioned here生成查询,以获得每条记录的N个关系数。

我尝试运行一个更简单的查询,每条记录没有N个关系,实际上它甚至更慢,因为它获取了更多的数据。所以我认为问题是有太多ID试图在查询的IN方法中进行匹配。

在我的控制器中我有:

$user = auth()->user();
$websites = $user->websitesAndKeywords();

在我的User模型中:

public function websitesAndKeywords() {

        $user = auth()->user();
        $websites = $user->websites()->orderBy('url')->get();

        $websites->load('keywords', 'keywords.latestPositions');

        return $websites;
}

我很感激任何人都可以帮助我提高速度。

编辑:所以我想我明白了。问题是Laravel每次使用预先加载来加载关系时使用的IN子句。所以我需要找到一种方法来做JOIN而不是急切加载。

基本上需要转换它:

$websites->load('keywords', 'keywords.latestPositions');

分为:

$websites->load(['keywords' => function($query)
{
    $query->join('positions', 'keywords.id', '=', 'positions.keyword_id');
}]);

这不起作用,所以我不确定在当前集合上执行JOIN的最佳方法是什么。理想情况下,我也只会获取最新的N个位置而不是所有数据。

以下是positions表格中的索引:

enter image description here

以下是查询返回的内容:

enter image description here

2 个答案:

答案 0 :(得分:0)

找到“每个分组中的前2名”的代码是我见过的最好的代码。它与my blog on such中的内容基本相同。

但是,我们可以能够改进其他两件事。

  • keyword_id in...从外部查询移至内部。我假设您的索引以keyword_id开头?
  • “懒惰评价”。也就是说,不要执行SELECT positions.*, ...,而只需SELECT id, ... id PRIMARY KEY positions JOIN。然后,在外部查询中,positions返回SHOW CREATE TABLE以获取其余列。看不到IN并知道 #dataframe d<-data.frame(a=LETTERS[1:26],b=rnorm(26,50,10),c=rnorm(26,50,1)) #dotplot dotplot(a~b,d,col="blue",pch=16, #simple plot comparing two vectors panel = function(x,col,y,...){ panel.dotplot(x,col="blue",y,...) panel.xyplot(x=d$c,col="darkblue",y,...) mins=NULL maxs=NULL #a line showing the difference between "measures" for(i in 1:nrow(d)){ mins[i]<-min(d$c[i],d$b[i]) maxs[i]<-max(d$c[i],d$b[i]) } panel.segments(x0=mins,y0=as.numeric(y), x1=maxs,y1=as.numeric(y),col="red") }, #the challenge of the conditional Y-axis yscale.components = function(...){ temp <- yscale.components.default(...) loc <- temp$left$labels$at print( temp$left$labels$labels <- sapply( temp$left$labels$labels, function(x) if(a> x){ as.expression(bquote( bold(.(x)))) }else{ as.expression(bquote(.(x)))} )) temp }, #a legend key = list(columns=2, points=list(pch=16,col=c("blue","darkblue")), text=list(c("measure","fitted")))) 列表中表格的百分比,我无法确定这会有多大帮助。

答案 1 :(得分:0)

如果您还没有,则需要索引positions(keyword_id)positions(keyword_id,created_at),具体取决于您是否要继续使用“懒惰评估”,以及取决于您是否要使用触发器解决方案。

正如Rick建议的那样,你必须将keyword_id in...移到内部查询中,因为mysql无法将其优化到子查询中,因为优化器不理解IF(@group = keyword_id, @rank+1, 1)将会不需要其他关键字才能正常工作。

这应该在不到700毫秒的时间内为具有数百万行的表(如果您不想在IN中全部检索它们)提供结果,并且可以通过删除“懒惰评估”来改进Rick建议(因此,对索引中未包含的列执行较少的表查找),具体取决于您的数据。

如果您仍然遇到麻烦,则可以通过使用触发器实际预先计算数据而不会降低准确性。它会为你的插入/更新添加一个(很可能是很小的)开销,所以如果你插入/更新很多并且偶尔只查询一次,你可能不想这样做。

为此,您应该使用索引positions (keyword_idcreated_at)。

添加另一个表格keywordrank,其中包含keyword_id, rank, primarykeyofpositionstable列,主键keyword_idrank。您需要另一个表,因为在触发器中,mysql无法更新您正在更新的表中的其他行。

创建一个触发器,在每个插入positions - 表格上更新这些等级:

delimiter $$
create trigger tr_positions_after_insert_updateranks after insert on positions 
for each row
begin
  delete from keywordrank where keyword_id = NEW.keyword_id;
  insert into keywordrank (keyword_id, rank, primarykeyofpositionstable)
  select NEW.keyword_id, ranks.rank, ranks.position_pk
  from  
    (select  NEW.keyword_id, 
             @rank := @rank+1 as rank,
             `positions`.primarykeyofpositionstable as position_pk
     from  
       (SELECT  @rank:=0, @group:=0 ) as vars,
       positions
     where `positions`.keyword_id = NEW.keyword_id
     order by `keyword_id` asc, `created_at` desc
    ) as ranks
    where  ranks.rank <= 2;
end$$
delimiter ;

如果您希望能够更新或删除条目(或者如果您一次这样做是安全的,那么无论如何这可能都是个好主意),请添加update / { {1}} - 触发器,只为deleteold.keyword_id执行此操作 - 您可能希望将代码放入过程中以便重复使用它。例如。创建一个过程new.keyword_id,将整个触发器代码放入其中,但将所有fctname(kwid int)替换为NEW.keyword_id,然后只需为kwid fctname(new.keyword_id)调用insert,{{1} } fctname(new.keyword_id)用于fctname(old.keyword_id)update用于fctname(old.keyword_id)

您需要初始化该表一次(如果您决定可能需要更多排名或其他订单),您可以使用任何版本的代码,例如

delete

您可以将触发器和init都放在迁移文件中(不带分隔符)。

然后,您可以使用联接来获取所需的行。

更新不使用触发器的代码,使用delete from keywordrank; insert into keywordrank (keyword_id, rank, primarykeyofpositionstable) select ranks.keyword_id, ranks.rank, ranks.position_pk from ( SELECT `positions`.primarykeyofpositionstable as position_pk, @rank := IF(@group = keyword_id, @rank+1, 1) as rank, @group := keyword_id as keyword_id from ( SELECT @rank:=0, @group:=0 ) as vars, positions order by `keyword_id` asc, `created_at` desc ) as ranks where ranks.rank <= 2; 上的索引。您可以从索引中完全计算内部查询,然后只在tabledata中查找找到的ID。它取决于结果中的行数(与整个表相关),删除惰性求值的效果有多少。

(keyword_id, created_at)

检查select positions.*, poslist.rank, poslist.group from positions join ( SELECT `positions`.id, @rank := IF(@group = keyword_id, @rank+1, 1) as rank, @group := keyword_id as group from ( SELECT @rank:=0, @group:=0 ) as vars, positions where `positions`.`keyword_id` in ('hundreds of IDs listed here') order by `keyword_id` asc, `created_at` desc ) as poslist on positions.id = poslist.id where poslist.rank <= 2; 是否确实使用了正确的索引explain。如果这还不够快,您应该尝试触发解决方案。 (或者添加新的(keyword_id, created_at) - 输出和explain - 输出,让我们深入了解一下。)