UPDATE:
I have modified my answer to allow page validation.
I arrive over one year late, but I wanted to share that it is actually possible to use SQL_CALC_FOUND_ROWS within a SqlDataProvider in Yii2.
You need to extend the SqlDataProvider
class to:
- get the total count from
SELECT FOUND_ROWS()
.
- modify how the validatePage property of the
pagination
object works.
What is the problem?
If you have a look to the last lines of the prepareModels()
method of the SqlDataProvider class (Yii 2.0.10)...
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();
...you will see that $this->getTotalCount()
is called before running the query. Obviously, that is a problem if you want to use SELECT FOUND_ROWS()
for the total count.
But, why does it need to call it in advance? After all, at that point it hasn't started to build the pager yet. Well, the pagination
object needs the total count just for validating the current page index.
The 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() to ensure that the requested page is within the boundaries.
What is the solution?
To extend the SqlDataProvider
class, setting the validatePage
property of the pagination
object to false
until we have executed our query. Then we can get the total count from SELECT FOUND_ROWS()
and enable a custom page validation.
Our new custom data provider could be something like this:
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;
}
}
And we can use it in this way:
$dataProvider = new CustomDataProvider([
'sql' => 'SELECT SQL_CALC_FOUND_ROWS ...';
//'totalCount' => $count, // this is not necessary any longer!
// more properties like 'pagination', 'sort', 'params', etc.
]);
Is there any drawback?
Well, our new custom page validation is less efficient: if the page doesn't pass the validation it will need one extra query.
How does the page validation work?
Imaging that you have 100 items and you use a data provider with pageSize => 20
to display data in a ListView
. The pager will show links to navigate through 5 pages but, in some cases, the user can try to access page 6: because he manually modifies the URL, because the number of records changes since he last loaded the page (like in @Brett's example) or because he follows an old link.
How does the data provider manage the situation? If validatePage
is set to...
- false (whatever provider): it will try to query for the page 6 with a SQL like:
SELECT ... LIMIT 20 OFFSET 100
. It will get an empty dataset and the widget will output 'No results found.'
- true (
SqlDataProvider
): it will detect beforehand that the last available page is the number 5 and it will query for it with SELECT ... LIMIT 20 OFFSET 80
.
- true (
CustomDataProvider
): it will try to query for the page 6, get an empty dataset, realize afterwards that page 6 doesn't exist and query again for page 5.
IMO, it is not a big deal because getting into a non-existing page will happen very rarely.
Is this actually necessary?
The OP wanted this approach to ensure that both the counting and the actual query are executed as close as possible. Maybe you want it for performance.
In any case, you should read the comments to the question from @AlexanderRavikovich and @ineersa. It is counterintuitive, but in a lot of cases a second count(*)
query can be faster than using SQL_CALC_FOUND_ROWS
.
There is a lot written about it, don't bother too much: it depends a lot on your query and the database version. The best you can do is to test both ways before implementing a custom data provider.
Final notes:
If you really care about precision, consider this scenario:
- If
count(*)
fails, it will normally fail for a few records.
- If
SELECT FOUND_ROWS()
fails... well, it can be an epic fail!
And if you really care about performance, there are some nice suggestions in the answers to this other question (take it with a grain of salt, it is very old), I like this one specially.