在 Yii2 的 SqlDataProvider 中使用 SQL_CALC_FOUND_ROWS

Using SQL_CALC_FOUND_ROWS within a SqlDataProvider in Yii2

我在 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

像这样创建查询:

$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.';

如果要使用 SQL_CALC_FOUND_ROWS 而不是 COUNT,则需要 2 个查询才能获得总行数。这里 SELECT SQL_CALC_FOUND_ROWS * FROM tbl_name WHERE status = 1 LIMIT 10; 将 return 给你的查询结果被限制为 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 是为页面

获取的项目数

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

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

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

我看到两种可能性。

您要么删除分页并单独处理。这只是 如果你知道你的 return 集会相对 小的。在大多数实际情况下,这不是一个选项。

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

  • SqlDataProvider 扩展为一个新的 class.. 我们称之为 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,
    ],
]);

这应该做的是,当数据提供者 获取 结果集时,运行 您的计数查询与您最初拥有的结果相反,当dataprovider set(从技术上讲甚至在设置之前)

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

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


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

您需要将 SqlDataProvider class 扩展为:

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

有什么问题?

如果您查看 SqlDataProvider class (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 object 需要总计数只是为了 验证当前页面索引 .

getOffset() method calls getPage() for the calculation, which calls getQueryParam() to obtain the current requested page. After doing so, getPage() calls setPage($page, true). And here is when the total count is necessary: setPage() will call getPageCount()确保请求的页面在边界内。

解决方法是什么?

要扩展 SqlDataProvider class,请将 pagination object 的 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 中显示数据。寻呼机将显示 links 以浏览 5 页,但在某些情况下,用户可以尝试访问第 6 页:因为他手动修改了 URL,因为自上次以来记录数发生了变化加载了页面(就像@Brett 的 )或者因为他遵循了旧的 link.

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

  • false(无论提供者是什么):它将尝试使用 SQL 查询第 6 页,例如:SELECT ... LIMIT 20 OFFSET 100。它将获得一个空数据集,并且小部件将输出“未找到结果。
  • true (SqlDataProvider):它会预先检测到最后可用的页面是数字5,它将使用 SELECT ... LIMIT 20 OFFSET 80.
  • 查询
  • true (CustomDataProvider):会尝试查询第6页,得到一个空数据集,实现afterwards 第 6 页不存在,再次查询 第 5 页。

IMO,这没什么大不了的,因为进入 non-existing 页面的情况很少见。

这真的有必要吗?

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

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

更快

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

最后的笔记:

如果您真的很在意精度,请考虑 this scenario:

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

如果你真的很关心性能,在 this other question (take it with a grain of salt, it is very old), I like this one 的答案中有一些很好的建议。