【问题标题】:Pagerfanta and Doctrine2 COUNT optimizationPagerfanta 和 Doctrine2 COUNT 优化
【发布时间】:2014-04-18 17:22:01
【问题描述】:

我正在将 Pagerfanta 和 Doctrine Adapters 与 Symfony2 和 Silex 一起使用。随着我的数据库变得越来越大,我注意到管理统计页面上有巨大的负载,这些页面显示带有分页的大数据。我检查了分析器,发现查询效率低得令人难以置信:

SELECT DISTINCT id16
FROM (
    SELECT f0_.username AS username0, ..., f0_.added_on AS added_on20
    FROM fos_user f0_ ORDER BY f0_.id DESC
) dctrn_result
LIMIT 50 OFFSET 0;

SELECT COUNT(*) AS dctrn_count
FROM (
    SELECT f0_.username AS username0, ..., f0_.added_on AS added_on20
    FROM fos_user f0_ ORDER BY f0_.id DESC
) dctrn_result
LIMIT 50 OFFSET 0;`

通过创建DoctrineORMAdapter 类的固定版本很容易修复第一个查询。生成COUNT() 查询的代码更复杂,所以我决定询问是否有任何解决方案。

那么有什么办法可以让 Pagerfanta 不运行嵌套查询?

【问题讨论】:

  • 如果您看到代码生成了不必要的复杂查询,也许您应该在项目站点上写一份错误报告。

标签: php mysql symfony doctrine-orm pagerfanta


【解决方案1】:

迟到总比不到好:我今天遇到了超过 20 万条记录并找到了解决方案。

Pagerfanta 在内部使用 Doctrine\ORM\Tools\Pagination\CountOutputWalker 对对象进行计数,从而产生如下计数查询:

SELECT 
  COUNT(*) AS dctrn_count 
FROM 
  (
    SELECT 
      DISTINCT id_0 
    FROM 
      (
        SELECT 
          m0_.id AS id_0, 
          ...
        FROM 
          messaging_messages m0_ 
        ORDER BY 
          m0_.id DESC
      ) dctrn_result
  ) dctrn_table

要绕过 CountOutputWalker,我们可以在实例化 DoctrineORMAdapter 时传递一个标志。 所以不是简单的

$adapter = new DoctrineORMAdapter($qb);

你会的

$adapter = new DoctrineORMAdapter($qb, true, false);

(第三个参数)。这会将 count 查询变成更有效的查询:

SELECT 
  count(DISTINCT m0_.id) AS sclr_0 
FROM 
  messaging_messages m0_

您必须将 whiteoctober/Pagerfanta 更新到 1.0.3。

Issue

Related commit

【讨论】:

  • 请注意,如果您的 SELECT 查询有 HAVING 子句,这将不起作用
【解决方案2】:

在您的情况下,执行子查询的不是 pagerfanta。它是您的查询生成器实例的来源。

我通常在实体存储库中有一个函数,它返回一个普通的查询构建器实例而不是结果。编写一个高效的查询构建器就交给你了。然后我将该查询生成器输入 DoctrineORMAdapter。

我在整个项目中都使用了这个辅助函数:

/**
 * Pass an array, entity or a custom QueryBuilder instance to paginate.
 * Takes an array of parameters as a second argument.
 * Default parameter values:
 *
 * $params = array(
 *     'curPage' => 1,
 *     'perPage' => 15,
 *     'order' => 'DESC'
 * );
 *
 * @param mixed $object
 * @param array $params
 *
 * @return Pagerfanta
 */
public function paginate($object, $params = array())
{
    if (is_array($object)) {
        $adapter = new ArrayAdapter($object);
    } elseif ($this->isEntity($object)) {
        $qb      = $this->em->createQueryBuilder()
            ->select('s')
            ->from($this->getEntityName($object), 's')
            ->orderBy('s.id', isset($params['order']) ? $params['order'] : 'DESC');
        $adapter = new DoctrineORMAdapter($qb);
    } elseif ($object instanceof QueryBuilder) {
        $adapter = new DoctrineORMAdapter($object);
    }
    $pager = new Pagerfanta($adapter);
    $pager->setMaxPerPage(isset($params['perPage']) ? $params['perPage'] : 15);
    $pager->setCurrentPage(isset($params['curPage']) ? $params['curPage'] : 1);

    return $pager;
}

您可以传递数组、实体或查询构建器实例,它会返回一个适当的分页对象以供使用。

您可能知道它是如何完成的,但无论如何,这就是我的实体存储库中的内容 - 一个函数返回查询构建器实例(非常适合 pagerfanta),另一个函数返回一个要在其他地方使用的数组:

public function getMessageQueryBuilder($campaignId, $eqCriteriaArray = array(), $neqCriteriaArray = array())
{
    $qb = $this->createQueryBuilder('m');
    $qb->select('m')
        ->leftJoin('m.campaign', 'c')
        ->leftJoin('m.sentBy', 'u')
        ->where($qb->expr()->eq('m.campaign', $campaignId));
    foreach ($eqCriteriaArray as $property => $value) {
        $qb->andWhere($qb->expr()->eq($property, $qb->expr()->literal($value)));
    }
    foreach ($neqCriteriaArray as $property => $value) {
        $qb->andWhere($qb->expr()->neq($property, $qb->expr()->literal($value)));
    }

    return $qb->orderBy('m.id', 'DESC');
}

public function filterMessages($campaignId, $eqCriteriaArray = array(), $neqCriteriaArray = array())
{
    return $this->getMessageQueryBuilder($campaignId, $eqCriteriaArray, $neqCriteriaArray)->getQuery()->getResult();

然后我将这两者结合起来得到实际的寻呼机对象:

$singleSmsPager = $this->pagerUtil->paginate(
    $this->em->getRepository('TreasureForgeMessageBundle:Message')
        ->getMessageQueryBuilder(CcToolSender::CAMPAIGN_ID, array(), array('u.username' => 'admin')),
    array(
        'curPage' => $singleSmsPage,
        'perPage' => 10
    )
);

【讨论】:

  • 我不这么认为。我的 queryBuilder(我传递给适配器的那个)只负责原始选择查询。 Pagerfanta 从它创建 COUNT 查询的方式是不同的。
  • 抱歉,我检查了我自己的分页查询,它们似乎执行相同类型的子查询。但是后来我没有注意到性能和我的应用程序记录数之间的任何关系。在这种情况下,我会认为 mysql 优化器也会限制内部结果集。很有趣。
  • 当我的表达到数十万条记录时,我注意到了一些问题。它也有很高的更新率,所以我认为结果不能被缓存。在其他网站上,我每天分页和更新大约 2000 个项目,没有发现任何问题
猜你喜欢
  • 2014-04-14
  • 1970-01-01
  • 2016-01-10
  • 2016-11-04
  • 2016-07-27
  • 1970-01-01
  • 1970-01-01
  • 2012-01-28
  • 1970-01-01
相关资源
最近更新 更多