【问题标题】:Doctrine Paginator selects entire table (very slow)?Doctrine Paginator 选择整个表格(非常慢)?
【发布时间】:2016-09-28 00:16:12
【问题描述】:

这与之前的问题有关: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);

奇怪的是,分页器上的 count($result) 显示了我的表中的总行数(超过 80,000 行),但是使用 foreach 遍历 $result 会输出 10 个 Post 实体,正如预期的那样。我需要做一些额外的配置来正确限制我的分页器吗?

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

(奖励):如何对数组进行水合,例如 $query->getQuery()->getArrayResult();?

编辑:我在函数中遗漏了一个杂散的 orderBy。看起来同时包含 groupBy 和 orderBy 会导致速度变慢(使用 groupBy 而不是分页器)。如果我省略一个或另一个,查询很快。我尝试在表中的“时间”列上添加索引,但没有看到任何改进。

我尝试过的事情

// 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

【问题讨论】:

  • 你能用 KNP-Paginator 吗?

标签: php mysql symfony doctrine-orm pagination


【解决方案1】:

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

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

我使用KnpLabs/KnpPaginatorBundle,对于复杂的查询也可能会遇到速度问题。

通常使用 LIMIT x,z 对于 DB 来说很慢,因为它在整个数据集上运行 COUNT。如果不使用索引,它会非常缓慢。

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

【讨论】:

    【解决方案2】:

    归根结底,我的应用程序中使用的许多查询都过于复杂,无法正确使用 Paginator,而且我无法在 Paginator 中使用数组水合模式。

    根据MySQL documentation,如果在不同的列上使用了 GROUP BY,则索引无法解析 ORDER BY。因此,我最终使用了几个后处理查询来使用一对多关系(如主题标签)填充我的基本结果(ORDERed 和 LIMITed)。

    对于从连接表中加载单行的连接,我能够在基本有序查询中连接所需的值。例如,在加载当前用户的“点赞状态”时,只需要加载点赞集合中的一个点赞来指示当前帖子是否被点赞。类似地,给定帖子只有一个作者会产生一个联合作者行。例如

    $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] => [...]
    ]
    

    然后在第二个查询中,我为在前一个查询中加载的帖子加载主题标签。例如

    // 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

    【讨论】:

      【解决方案3】:

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

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

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

      对我们来说,它做出了巨大的改进,我们无需编写或使用自定义 paginator

      更多详情请访问this site。请注意,我是该网站的所有者。

      【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-05-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-06-15
      • 1970-01-01
      相关资源
      最近更新 更多