【问题标题】:Using SQL_CALC_FOUND_ROWS within a SqlDataProvider in Yii2在 Yii2 的 SqlDataProvider 中使用 SQL_CALC_FOUND_ROWS
【发布时间】:2015-04-16 07:32:07
【问题描述】:

我在Yii2 中使用SqlDataProvider,这是一般示例:

$count = Yii::$app->db->createCommand('
    SELECT COUNT(*) FROM user WHERE status=:status
', [':status' => 1])->queryScalar();

$dataProvider = new SqlDataProvider([
    'sql' => 'SELECT * FROM user WHERE status=:status',
    'params' => [':status' => 1],
    'totalCount' => $count,
    'sort' => [
        'attributes' => [
            'age',
            'name' => [
                'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
                'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
                'default' => SORT_DESC,
                'label' => 'Name',
            ],
        ],
    ],
    'pagination' => [
        'pageSize' => 20,
    ],
]);

您可以看到,这在获取实际数据结果的SqlDataProvider 中的实际查询之前在查询中执行了COUNT

但是我更喜欢使用SLC_CALC_FOUND_ROWS,因为此数字是一种更可靠的方法,可以获取与 DataProvider 中查询返回的实际行数相匹配的正确数字,因为匹配的行可能会被添加或删除在COUNT 查询和SqlDataProvider 查询之间,因此我需要更可靠的东西。

我可以锁定表格,但我认为这不是明智之举,因此我需要使用 SQL_CALC_FOUND_ROWS 来获得正确的数量,但我不确定如何使用 dataProvider 来做到这一点。

这将是执行我想要的代码:

$sql = $this->db->createCommand("SELECT FOUND_ROWS()");
$count = $sql->queryScalar();

$dataProvider->totalCount = $count;

...但这不起作用,所以正如我所说,我不确定如何实现代码以使用SqlDataProvider

【问题讨论】:

  • SLC_CALC_FOUND_ROWS 会减慢您的脚本速度。 count(*) 使用索引并且工作得更快,因此您实际上不需要考虑“实际计数”,因为在这两个查询之间插入某些查询的可能性非常小。
  • @AlexanderRavikovich 这不一定是真的,这取决于您的应用程序。一些测试表明它较慢,有些则表明它更快 - 但是,如果您运行的网站流量比我认为的要高,那么最好追求准确性而不是速度,不是吗!?
  • 我同意@AlexanderRavikovich。您可以毫无问题地使用 ArrayDataProvider 来实现您的目标。我像一年前一样尝试过这种方法,结果令人失望。 SQL_CALC_FOUND_ROWS 在 InnoDB 卷上运行缓慢并使用更多资源。如果您运行大量网站 - 保持快速并监控资源使用的最佳方式,而不是精确度。是的,在那个时候插入的机会真的很小。所有东西都在网格上测试了 40m 记录表。
  • 还有一件事,告诉我或解释 SQL_CALC_FOUND_ROWS 在哪里比 COUNT() 更快。根据我在高负载数据库上的测试/经验(现在在 yii2 上管理每个 db 约 500m 记录的站点),我从未见过 SQL_CALC_FOUND_ROWS 快,尤其是。在 InnoDB 上,它比 MyISAM 更适合大数据。
  • @ineersa 我不会假装知道更快的事实,这就是为什么我说这取决于应用程序。在发布这个问题之前我也做了一些研究,在那里我获得了一些关于人们说他们的测试结果表明有时COUNT(*) 更快而其他人说SQL_CALC_FOUND_ROWS 更快的信息;所以这表明它取决于应用程序。

标签: php mysql yii yii2


【解决方案1】:

据我了解,SqlDataProvider的作用方式如下:

  • 如果没有设置分页,dataprovider 将查询数据库,然后count()结果生成的模型。 这是您想要的行为
  • 如果设置了分页,它将使用$totalCount 提供的值,或者如果$totalCount == NULL 将返回设置为返回0SqlDataProvider::prepareTotalCount() 的值。 不是您想要的行为

我认为不可能既利用查询中的分页,又在没有两个查询的情况下获得确切的总数。毕竟分页的重点是不必处理所有返回的元素。

我看到了两种可能性。

您可以删除分页并单独处理。这只是 如果你知道你的回报集将是相对的,那么真的可行 小的。在大多数实际情况下,这不是一种选择。

这导致我们不得不运行两个查询。如果你没问题 两个查询的想法,您觉得有必要将它们执行为 尽可能靠近,这是您如何继续获得 最佳结果:

  • SqlDataProvider 扩展为一个新类.. 让我们称之为CustomSqlDataProvider
  • 设置新的public $totalCountCommand 属性。
  • 编写prepareTotalCount() 方法来覆盖默认行为

类似的东西:

protected function prepareTotalCount()
{
    return $this->totalCountCommand->queryScalar();
}

然后您可以按照以下方式简单地创建数据提供者:

$countCommand = Yii::$app->db->createCommand('
    SELECT COUNT(*) FROM user WHERE status=:status
', [':status' => 1]);

$dataProvider = new CustomSqlDataProvider([
    'sql' => 'SELECT * FROM user WHERE status=:status',
    'params' => [':status' => 1],
    'totalCountCommand' => $countCommand,
    'sort' => [
        'attributes' => [
            'age',
            'name' => [
                'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
                'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
                'default' => SORT_DESC,
                'label' => 'Name',
            ],
        ],
    ],
    'pagination' => [
        'pageSize' => 20,
    ],
]);

这应该做的是,当数据提供者获取结果集时运行您的计数查询,而不是在数据提供者设置时进行计数>(从技术上讲,甚至在设置之前)

PS:我没有测试过这段代码,只是看了一下yii2的代码。但是,它应该可以进行细微的调整,或者让您走上正确的道路。如果您需要任何额外信息,请告诉我。

【讨论】:

  • 这看起来是个不错的选择,看起来应该可以正常工作。我很快就会试一试。
  • 刚开始尝试,没有去;返回了1,而不是实数。还尝试了我自己的版本,将SQL_CALC_FOUND_ROWS 留在主查询中,然后将return Yii::$app->db->createCommand("SELECT FOUND_ROWS()")->queryScalar(); 放入prepareTotalCount 方法中,但得到了相同的结果。
  • 好的,我也试试这个并玩一下。如果我找到任何东西,我会告诉你
  • 这很有趣。好吧,猜猜我们已经完成了。非常感谢您的帮助!
  • 作为额外信息,这里报告了这种奇怪的行为:github.com/yiisoft/yii2/issues/8143
【解决方案2】:

更新: 我已修改答案以允许页面验证。


我迟到了一年多,但我想分享一下实际上可以在 Yii2 的 SqlDataProvider 中使用 SQL_CALC_FOUND_ROWS。

您需要将SqlDataProvider 类扩展为:

  • SELECT FOUND_ROWS() 获取总数。
  • 修改pagination 对象的validatePage 属性的工作方式。

有什么问题?

如果您查看 SqlDataProvider 类 (Yii 2.0.10) 的 prepareModels() 方法的 last lines...

if ($pagination !== false) {
    $pagination->totalCount = $this->getTotalCount();
    $limit = $pagination->getLimit();
    $offset = $pagination->getOffset();
}

$sql = $this->db->getQueryBuilder()->buildOrderByAndLimit($sql, $orders, $limit, $offset);

return $this->db->createCommand($sql, $this->params)->queryAll();

...您将看到$this->getTotalCount() 在运行查询之前被调用。显然,如果您想使用SELECT FOUND_ROWS() 作为总数,那将是一个问题。

但是,为什么需要提前调用呢?毕竟,那时它还没有开始构建寻呼机。好吧,pagination 对象只需要总计数来验证当前页面索引

getOffset()方法调用getPage()进行计算,调用getQueryParam()获取当前请求的页面。这样做之后,getPage() 会调用 setPage($page, true)。这里是需要总计数的时候:setPage() 将调用getPageCount() 以确保请求的页面在边界内。

解决办法是什么?

要扩展SqlDataProvider 类,将pagination 对象的validatePage 属性设置为false,直到我们执行了查询。然后我们可以从SELECT FOUND_ROWS() 获取总数并启用自定义页面验证。

我们新的自定义数据提供者可能是这样的:

use Yii;
use yii\data\SqlDataProvider;

class CustomDataProvider extends SqlDataProvider
{
    protected function prepareModels()
    {
        // we set the validatePage property to false temporarily 
        $pagination = $this->getPagination();
        $validatePage = $pagination->validatePage;
        $pagination->validatePage = false;

        // call parent method 
        $dataModels = parent::prepareModels();

        // get total count
        $count = Yii::$app->db->createCommand( 'SELECT FOUND_ROWS()' )->queryScalar();

        // both the data provider and the pagination object need to know the total count 
        $this->setTotalCount( $count );
        $pagination->totalCount = $count;

        // custom page validation       
        $pagination->validatePage = $validatePage;
        // getPage(true) returns a validated page index if $validatePage is also true
        if ( $pagination->getPage(false) != $pagination->getPage(true) ) { 
            return $this->prepareModels();
            // or if you favor performance over precision (and fear recursion) *maybe* is better:
            //$this->sql = str_replace( 'SQL_CALC_FOUND_ROWS ', '', $this->sql);
            //return parent::prepareModels();
        }

        return $dataModels;
    }
}

我们可以这样使用它:

$dataProvider = new CustomDataProvider([
    'sql' => 'SELECT SQL_CALC_FOUND_ROWS ...';
    //'totalCount' => $count, // this is not necessary any longer!
    // more properties like 'pagination', 'sort', 'params', etc.
]);

有什么缺点吗?

好吧,我们新的自定义页面验证效率较低:如果页面未通过验证,则需要一个额外的查询。

页面验证是如何工作的?

假设您有 100 个项目,并且您使用带有 pageSize => 20 的数据提供程序在 ListView 中显示数据。寻呼机将显示浏览 5 个页面的链接,但在某些情况下,用户可以尝试访问第 6 页:因为他手动修改了 URL,因为自从他上次加载页面后记录数发生了变化(就像在 @Brett 的 @ 987654329@) 或者因为他使用的是旧链接。

数据提供者如何管理这种情况?如果validatePage 设置为...

  • false(无论提供者):它将尝试使用 SQL 查询第 6 页,例如:SELECT ... LIMIT 20 OFFSET 100。它将获得一个空数据集,小部件将输出“No results found.
  • true (SqlDataProvider):它会事先检测到最后一个可用页面是数字 5,然后使用SELECT ... LIMIT 20 OFFSET 80 进行查询。
  • true (CustomDataProvider):它会尝试查询第 6 页,得到一个空数据集,之后意识到第 6 页不存在并查询再次查看第 5 页。

IMO,这没什么大不了的,因为进入一个不存在的页面很少发生。

这真的有必要吗?

OP 希望这种方法能够确保计数和实际查询都尽可能接近地执行。也许你想要它的性能。

无论如何,您都应该阅读来自@AlexanderRavikovich 和@ineersa 的问题的cmets。这是违反直觉的,但在很多情况下,第二个 count(*) 查询可能比使用 SQL_CALC_FOUND_ROWS 更快。

有很多关于它的文章,不要打扰太多:它很大程度上取决于您的查询和数据库版本。您可以做的最好的事情是在实现自定义数据提供程序之前测试这两种方式。

最后说明:

如果您真的关心精度,请考虑this scenario

  • 如果count(*) 失败,通常会失败几条记录。
  • 如果SELECT FOUND_ROWS() 失败......好吧,这可能是一场史诗般的失败!

如果你真的很在意性能,this other question 的回答里也有一些不错的建议(持保留态度,它很老了),我特别喜欢this one

【讨论】:

  • 感谢大卫提供的额外信息。自从我处理这种特殊情况以来已经很久了,我需要再次检查代码以重新审视它的内部工作原理并了解它是如何再次工作的,哈哈......如果我决定重新审视它,我肯定会采取看看是否可以用这种方法改进。
  • 虽然我唯一担心的是缺少页面验证;不,我不担心用户手动修改页码并获得非结果页面,但我想可能存在用户让页面闲置一段时间的情况,当时管理员删除了一些内容,然后使最后一页为“第 4 页”,然后用户单击“第 5 页”并没有得到任何结果页面,他们想知道发生了什么哈哈........通过页面验证,他们最终会回到第 4 页,但看到新内容。
  • @Brett。好点子!我已经更新了我的答案。您似乎总是意识到两个操作之间的匹配记录数量可能会发生变化:)
  • 哈哈,我必须是 :) 感谢您对答案的修改,我相信它会对很多人有用,如果我决定当前的解决方案应该是我将考虑实施的东西被改变:)
【解决方案3】:

像这样创建您的查询:

$queryResults  = Yii::app()->db->createCommand()
    ->select('SQL_CALC_FOUND_ROWS (0), ' .
             'table1.column_1, table1.column_n')
    ->from('table1')
    ->where('status=1')
    ->queryAll();

$totalRecords =  Yii::app()->db
    ->createCommand('SELECT FOUND_ROWS()')
    ->queryScalar();
$totalFetched =  count($queryResults);

echo 'Fetched '.$totalFetched.' of '.$totalRecords.' records.';

【讨论】:

  • 我不确定我是否理解。您能否在使用SqlDataProvider 时提供一个示例?另外,您的示例使用Yii1,我使用的是Yii2
  • 使用 SQL_CALC_FOUND_ROWS 不会给你带来好处是使用 SqlDataProvider。相反,使用 'totalCount' => $this->db->createCommand("SELECT * FROM user")->where("status=:status", array(":status"=>1)->queryScalar() ,
  • @crafter 我希望你的意思是 SELECT COUNT(*) FROM user
  • 是的,也不是。如果您想在网格中显示数据,例如有 50% 的活跃用户,并且只想显示活跃用户,您可能想说“显示 1-10 的 1000 个(活跃)用户”,或显示 1-10 的 2000 (所有用户)”。在后面部分,网格将显示 1000 个用户,此处显示值 2000 可能不合适。您决定。
  • 在任何情况下,与仅使用 SELECT * FROM 进行计数相比,少数 COUNT(*) 查询会运行得更快,并且使用更少的内存!如果我有 100k 个用户表,即使有 10-20 个字段,并且说 50% 处于活动状态,那么您的 SELECT * FROM 用户将终止服务器。选择数据进行计数是错误的,而是执行 COUNT 以有效地执行此操作。
【解决方案4】:

如果您想使用 SQL_CALC_FOUND_ROWS 而不是 COUNT,则需要 2 个查询才能获得总行数。这里 SELECT SQL_CALC_FOUND_ROWS * FROM tbl_name WHERE status = 1 LIMIT 10; 将向您返回限制为 10 行的查询结果,但由于您使用的是 SQL_CALC_FOUND_ROWS,它将计算适合查询条件的项目总数并记住它。之后,您使用SELECT FOUND_ROWS(); 从 dbms 获取此号码。

添加

我为在我的一个项目中测试它而编写的代码:

Yii::$app->db->createCommand('SELECT SQL_CALC_FOUND_ROWS * FROM {{%articles}} LIMIT 1')->queryScalar();
$count = Yii::$app->db->createCommand('SELECT FOUND_ROWS()')->queryScalar();
$dataProvider = new \yii\data\SqlDataProvider([
    'sql' => 'SELECT * FROM {{%articles}}',
    'totalCount' => $count,
    'pagination' => [
        'pageSize' => 2,
    ],
]);
echo $count . ' ' . count($dataProvider->getModels());

它输出我5 2,其中5 是项目总数,2 是为页面获取的项目数

【讨论】:

  • 请阅读我的整个问题,我已经表明我知道如何在 正常情况下SQL_CALC_FOUND_ROWS 获取值,但我正在尝试让它与它一起工作SqlDataProvider.
  • 我不确定这与事先做 COUNT(*) 有何不同; SQL_CALC_FOUND_ROWS 必须在运行SqlDataProvider 内的查询之后
  • 也许你应该检查 yii2 调试器并查看查询日志。如果您想在SqlDataProvider查询之后执行SELECT FOUND_ROWS(),请确保它恰好在该查询之后进行
  • 您可以创建自己的dataprovider 类继承自SqlDataProvider 并覆盖prepareTotalCount() 方法
  • 我想这是值得一看的:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-09
  • 1970-01-01
  • 2021-05-22
  • 2012-03-09
相关资源
最近更新 更多