Doctrine Paginator选择整个表格(非常慢)?

时间:2016-09-28 00:16:12

标签: php mysql symfony doctrine-orm pagination

这与之前的问题相关:Doctrine/Symfony query builder add select on left join

我想使用Doctrine ORM执行复杂的连接查询。我想选择10个分页博客帖子,左边加入一个作者,比如当前用户的值,以及帖子上的主题标签。我的查询构建器如下所示:

$query = $em->createQueryBuilder()
            ->select('p')              
            ->from('Post', 'p')
            ->leftJoin('p.author', 'a')
            ->leftJoin('p.hashtags', 'h')
            ->leftJoin('p.likes', 'l', 'WITH', 'l.post_id = p.id AND l.user_id = 10')
            ->where("p.foo = bar")
            ->addSelect('a AS post_author')
            ->addSelect('l AS post_liked')
            ->addSelect('h AS post_hashtags')
            ->orderBy('p.time', 'DESC')
            ->setFirstResult(0)
            ->setMaxResults(10);

// FAILS - because left joined hashtag collection breaks LIMITS
$result = $query->getQuery()->getResult(); 

// WORKS - but is extremely slow (count($result) shows over 80,000 rows)
$result = new \Doctrine\ORM\Tools\Pagination\Paginator($query, true);

奇怪的是,paginator上的count($ result)显示了我的表中的总行数(超过80,000),但是正如预期的那样遍历带有foreach输出10 post实体的$ result。我是否需要进行一些额外配置才能正确限制我的分页器?

如果这是paginator类的限制,我还有其他选择吗?编写自定义分页器代码或其他分页器库?

(奖金):如何给阵列加水,比如$ query-> getQuery() - > getArrayResult();?

编辑:我在我的函数中遗漏了一个错误的命令。看起来包括groupBy和orderBy会导致减速(使用groupBy而不是paginator)。如果我省略其中一个,查询速度很快。我尝试在"时间"上添加一个索引。在我的表中列,但没有看到任何改进。

我尝试的事情

// works, but makes the query about 50x slower
$query->groupBy('p.id');
$result = $query->getQuery()->getArrayResult();

// adding an index on the time column (no improvement)
indexes:
    time_idx:
        columns: [ time ]

// the above two solutions don't work because MySQL ORDER BY
// ignores indexes if GROUP BY is used on a different column
// e.g. "ORDER BY p.time GROUP BY p.id is" slow

3 个答案:

答案 0 :(得分:4)

您应该简化查询。这样可以节省一些执行时间。我无法测试您的查询,但这里有一些指示:

  • 执行count()
  • 时不要排序
  • 您可以按 orderBy('p.id','DESC')排序,将使用索引
  • 而不是 leftJoin()如果连接表中始终存在至少一条记录,则可以使用 join()。否则,该记录将被跳过。
  • KNP / Paginator使用DISTINCT()只读取不同的记录,但这可能导致使用磁盘tmp表
  • $ query-> getArrayResult()使用数组隐藏模式,该模式返回多维数组,对于大型结果集,它比对象隐藏更快
  • 你可以使用部分 select('partial p。{id,其他用过的字段}'),这样你只需要加载所需的字段,或者在使用对象水化时跳过不需要的关系
  • 在doctrine部分下的给定查询中检查SF profiler EXPLAIN,可能不使用索引
  • p.hashtags和p.likes只返回一行或者是oneToMany,它会将结果相乘
  • 也许是一些Posts设计更改,会删除一些连接:
    • 将p.hashtags字段定义为 @ORM \ Column(type =“array”)并存储了标记的字符串值。稍后可能会在序列化阵列上使用全文搜索。
    • 将p.likesCount字段定义为 @ORM \ Column(type =“integer”),其中包含喜欢的内容

我使用KnpLabs/KnpPaginatorBundle并且还可以解决复杂查询的速度问题。

通常使用LIMIT x,z对于DB来说很慢,因为它在整个数据集上运行COUNT。如果没有使用索引,那就太慢了。

您可以使用不同的方法并通过ID推进进行一些自定义分页,但这会使您的方法复杂化。我已经将它用于像SYSLOG表这样的大型数据集。但是你放弃了排序和总记录数功能。

答案 1 :(得分:0)

在一天结束时,我的应用程序中使用的许多查询都太复杂而无法正确使用Paginator,而且我无法使用Paginator进行阵列水化模式。

根据MySQL documentation,如果GROUP BY用于其他列,则无法通过索引解析ORDER BY。因此,我最终使用了几个后处理查询来填充我的基本结果(ORDERed和LIMITed)与一对多关系(如hashtags)。

对于从连接表加载单行的连接,我能够在基本有序查询中加入所需的值。例如,在加载" like状态"对于当前用户,只需要加载喜欢该喜欢的一组用户以指示当前帖子是否已被喜欢。同样,给定帖子只有一位作者的存在会产生一个连接的作者行。 e.g。

$query = $em->createQueryBuilder()
        ->select('p')              
        ->from('Post', 'p')
        ->leftJoin('p.author', 'a')
        ->leftJoin('p.likes', 'l', 'WITH', 'l.post_id = p.id AND l.user_id = 10')
        ->where("p.foo = bar")
        ->addSelect('a AS post_author')
        ->addSelect('l AS post_liked')
        ->orderBy('p.time', 'DESC')
        ->setFirstResult(0)
        ->setMaxResults(10);

// SUCCEEDS - because joins only join a single author and single like
// no collections are joined, so LIMIT applies only the the posts, as intended
$result = $query->getQuery()->getArrayResult(); 

这会产生以下形式的结果:

[
  [0] => [
    ['id'] => 1
    ['text'] => 'foo',
    ['author'] => [
       ['id'] => 10,
       ['username'] => 'username',
    ],
    ['likes'] => [
       [0] => [
         ['post_id'] => 1,
         ['user_id'] => 10,
       ]
    ],
  ], 
  [1] => [...],
  ...
  [9] => [...]
]

然后在第二个查询中,我加载了上一个查询中加载的帖子的主题标签。 e.g。

// we don't care about orders or limits here, we just want all the hashtags
$query = $em->createQueryBuilder()
        ->select('p, h')              
        ->from('Post', 'p')
        ->leftJoin('p.hashtags', 'h')
        ->where("p.id IN :post_ids")
        ->setParameter('post_ids', $pids);

产生以下内容:

[
  [0] => [
    ['id'] => 1
    ['text'] => 'foo',
    ['hashtags'] => [
       [0] => [
         ['id'] => 1,
         ['name'] => '#foo',
       ],
       [2] => [
         ['id'] => 2,
         ['name'] => '#bar',
       ],
       ...
    ],
  ], 
  ...
]

然后我只是遍历包含主题标签的结果并将它们附加到原始(有序和有限)结果。这种方法最终会更快(即使它使用更多查询),因为它避免了GROUP BY和COUNT,充分利用了MySQL索引,并允许更复杂的查询,例如我发布的here

答案 2 :(得分:0)

您可以通过执行以下一项或多项优化,将paginator配置为使用更简单的'count' sql策略。

$paginator = new Paginator($query, false);
$paginator->setUseOutputWalkers(false);

如果结果出乎意料,则可能需要DISTINCT选择(select('DISTINCT p'))

对我们来说,它进行了重大改进,因此我们无需编写或使用自定义paginator

可以在this site上找到更多详细信息。请注意,我是该网站的所有者。