Использование SQL_CALC_FOUND_ROWS в SqlDataProvider в Yii2

Я использую SqlDataProvider в Yii2, и вот общий пример :

$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,
    ],
]);

Вы можете видеть, что это делает COUNT в запросе перед фактическим запросом в SqlDataProvider, который получает фактические результаты данных.

Однако я бы предпочел использовать 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.


person Brett    schedule 16.04.2015    source источник
comment
SLC_CALC_FOUND_ROWS замедлит ваш скрипт. count (*) использует индексы и работает намного быстрее, поэтому вам действительно не нужно думать о реальном числе счетчиков, потому что вероятность того, что какой-то запрос окажется между этими двумя запросами, очень мала.   -  person Alexander R.    schedule 18.04.2015
comment
@AlexanderRavikovich Это не обязательно так, это зависит от вашего приложения. Некоторые тесты показали, что он медленнее, некоторые показали, что он быстрее - однако, если у вас большой объем веб-сайта, я думаю, что лучше сделать ставку на точность, а не скорость, нет !?   -  person Brett    schedule 18.04.2015
comment
Я согласен с @AlexanderRavikovich. Вы можете без проблем использовать ArrayDataProvider для своей цели. Я пробовал этот подход, как и год назад, и результаты меня разочаровали. SQL_CALC_FOUND_ROWS работает медленно на томах InnoDB и использует гораздо больше ресурсов. Если у вас большой объем веб-сайта - лучший способ сделать его быстрым и отслеживать использование ресурсов, а не точность. И да, шанс получить вставку за это время очень мал. Все тестировалось на сетке для таблицы рекордов 40м.   -  person ineersa    schedule 20.04.2015
comment
И еще кое-что, покажите мне или объясните, где SQL_CALC_FOUND_ROWS будет быстрее, чем COUNT (). Из моих тестов / опыта работы с высоконагруженными базами данных (управляющий сайт с ~ 500 млн записей на каждую базу данных теперь на yii2), я никогда не видел SQL_CALC_FOUND_ROWS быстро, особенно. на InnoDB, который больше подходит для больших данных, чем MyISAM.   -  person ineersa    schedule 20.04.2015
comment
@ineersa Я не претендую на то, чтобы знать факт, который быстрее, поэтому я сказал, что это зависит от приложения. Я провел небольшое исследование перед тем, как опубликовать этот вопрос, и именно здесь я получил некоторую информацию о том, где люди заявляли, что результаты их тестов показывают, что иногда COUNT(*) был быстрее, а другие говорили, что SQL_CALC_FOUND_ROWS быстрее; так что указание на то, что это зависит от приложения.   -  person Brett    schedule 20.04.2015


Ответы (4)


Насколько я понимаю, функция SqlDataProvider выглядит следующим образом:

  • Если нумерация страниц не задана, поставщик данных запросит базу данных, а затем count() модели, сгенерированные в результате. Это то, что вам нужно.
  • Если установлена ​​разбивка на страницы, она либо будет использовать значение, предоставленное $totalCount, либо если $totalCount == NULL вернет значение SqlDataProvider::prepareTotalCount(), которое настроено на возврат 0. Не то поведение, которое вам нужно.

Я не думаю, что можно одновременно использовать разбиение на страницы в своих запросах и получить точное общее количество без двух запросов. В конце концов, весь смысл разбивки на страницы состоит в том, чтобы не обрабатывать все возвращаемые элементы.

Я вижу две возможности.

Вы либо удаляете пагинацию, либо обрабатываете ее отдельно. Это реально возможно только в том случае, если вы знаете, что ваш возвращаемый набор будет относительно небольшим. В большинстве практических ситуаций это не вариант.

Это приводит к тому, что нам нужно выполнить два запроса. Если вас устраивает идея двух запросов и вы чувствуете, что необходимо выполнять их как можно ближе друг к другу, вот как вы можете продолжить, чтобы получить наилучшие результаты:

  • Расширить 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. Однако он должен либо работать с небольшими настройками, либо указывать на правильный путь. Дайте мне знать, если вам понадобится дополнительная информация.

person Pomme.Verte    schedule 19.04.2015
comment
Похоже, это неплохой вариант, и похоже, что он должен работать нормально. Я скоро опробую его. - person Brett; 20.04.2015
comment
Просто дошел до того, чтобы попробовать это и не пойдет; получил 1 вместо реального числа. Также попробовал свою версию, оставив SQL_CALC_FOUND_ROWS в основном запросе, а затем поместив return Yii::$app->db->createCommand("SELECT FOUND_ROWS()")->queryScalar(); в метод prepareTotalCount, но получил тот же результат. - person Brett; 20.04.2015
comment
Хорошо, я тоже попробую и поиграю с этим. Я дам тебе знать, если найду что-нибудь - person Pomme.Verte; 20.04.2015
comment
Хорошо, я нашел решение. На самом деле нет необходимости использовать SQL_CALC_FOUND_ROWS в вашем случае, поскольку команда count отделена от логики разбивки на страницы и не имеет предложения LIMIT. (например, потому что в конце мы делаем два отдельных запроса). По этой причине вы можете достичь желаемых результатов, используя COUNT(*), и он всегда должен возвращать точное значение и работать быстрее. Я обновил свой ответ, чтобы показать это. Если вы программно изменяете запросы так, чтобы команда count использовала вариант поставщика данных sql, вам просто нужно удалить LIMIT или установить для него NULL - person Pomme.Verte; 20.04.2015
comment
Интересно; однако чем это отличается от примера Yii в моем исходном вопросе? Мол, оба выполняют отдельные запросы, чтобы получить фактическое количество строк нет !? Кроме того, вы говорите, что нет предложения LIMIT, но разве Yii не добавляет его автоматически в запрос при использовании разбивки на страницы? - person Brett; 20.04.2015
comment
Разница в том, что в первом случае перед построением вы запускаете запрос количества, а затем передаете это число поставщику данных. Во втором случае поставщик данных ждет, пока ему не понадобится информация для выполнения запроса. Если вас беспокоит добавление строк между обоими запросами, то второй вариант - лучший выбор для получения согласованных данных. - person Pomme.Verte; 20.04.2015
comment
Что касается провайдера, добавляющего LIMIT, это правда, но только для запроса sql. Не на нашем кастоме $TotalCountCommand. Так что он будет работать независимо от разбивки на страницы. - person Pomme.Verte; 20.04.2015
comment
Хорошо, круто - так в принципе это лучший метод, но не пуленепробиваемый? ха-ха ... также, как мне установить такое же значение для свойства totalCount экземпляров разбивки на страницы? - person Brett; 20.04.2015
comment
Пагинация totalCount должна быть такой же, как у провайдера данных :) - person Pomme.Verte; 20.04.2015
comment
В качестве другого варианта (и для полного раскрытия информации) у вас также есть возможность сделать то, что предложил @crafter, а затем поместить $queryResults в ArrayDataProvider. Хотя это может сделать фильтрацию / сортировку более сложной и / или менее эффективной. - person Pomme.Verte; 20.04.2015
comment
Что касается разбивки на страницы totalCount - вы имеете в виду, что не устанавливайте ее специально, и она будет учитывать счетчик от dataProvider? - person Brett; 20.04.2015
comment
Я проверил приведенный выше код, и он $dataprovider->pagination->totalCount показывает правильное количество, так что похоже, что он должен работать из коробки. Так что да, не нужно его специально настраивать. - person Pomme.Verte; 20.04.2015
comment
Странно ...... только что проверил; totalCount отображается нормально, но я получаю 0 для totalCount из экземпляра нумерации страниц. - person Brett; 20.04.2015
comment
Исправлено, установив $this->pagination->totalCount внутри метода prepareTotalCount ..... разве вы не можете придумать более чистый способ сделать это? - person Brett; 20.04.2015
comment
Странно .. Думаю, что у вас все хорошо, недостатков точно нет. Я думаю, это может быть ошибка в yii. Похоже, getCount() правильно устанавливает разбивку на страницы totalCount. Но getTotalCount() этого не делает. Я могу создать проблему. - person Pomme.Verte; 20.04.2015
comment
Это интересно. Думаю, мы здесь закончили. Большое спасибо за помощь! - person Brett; 20.04.2015
comment
В качестве дополнительной информации об этом странном поведении сообщалось здесь: github.com/yiisoft/yii2/issues/8143 < / а> - person Pomme.Verte; 01.06.2015

ОБНОВЛЕНИЕ: я изменил свой ответ, чтобы разрешить проверку страницы.


Я прибыл с опозданием более чем на год, но хотел поделиться, что действительно возможно использовать SQL_CALC_FOUND_ROWS в SqlDataProvider в Yii2.

Вам необходимо расширить класс SqlDataProvider до:

  • получить общее количество от SELECT FOUND_ROWS().
  • изменить способ свойства validatePage объекта pagination объект работает.

В чем проблема?

Если вы посмотрите на последние строки метода prepareModels() класса SqlDataProvider (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();

... вы увидите, что $this->getTotalCount() вызывается перед выполнением запроса. Очевидно, это проблема, если вы хотите использовать SELECT FOUND_ROWS() для общего подсчета.

Но зачем ему звонить заранее? В конце концов, на тот момент он еще не начал создавать пейджер. Итак, объекту pagination требуется общий счет только для проверки индекса текущей страницы.

Вызов метода getOffset () getPage () для расчета. , который вызывает getQueryParam () чтобы получить текущую запрашиваемую страницу. После этого getPage() вызывает setPage ($ page , правда). И здесь, когда необходимо общее количество: setPage() вызовет getPageCount (), чтобы убедиться, что запрашиваемая страница находится в установленных границах.

Каково решение?

Чтобы расширить класс SqlDataProvider, установите для свойства validatePage объекта pagination значение 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's пример ) или потому что он перешел по старой ссылке.

Как поставщик данных управляет ситуацией? Если validatePage установлен на ...

  • false (независимо от поставщика): он попытается запросить страницу 6 с помощью SQL, например: SELECT ... LIMIT 20 OFFSET 100. Он получит пустой набор данных, а виджет выдаст сообщение «Результаты не найдены».
  • true (SqlDataProvider): заранее обнаружит, что последней доступной страницей является номер 5, и запросит ее с SELECT ... LIMIT 20 OFFSET 80.
  • true (CustomDataProvider): он попытается запросить страницу 6, получит пустой набор данных, впоследствии поймет, что страница 6 не существует, и запросит еще раз < / strong> для страницы 5.

ИМО, это не имеет большого значения, потому что попадание на несуществующую страницу будет происходить очень редко.

Это действительно необходимо?

OP хотел, чтобы этот подход гарантировал, что и подсчет, и фактический запрос выполняются как можно ближе. Может быть, вам это нужно для выступления.

В любом случае, вы должны прочитать комментарии к вопросу от @AlexanderRavikovich и @ineersa. Это нелогично, но во многих случаях второй count(*) запрос может быть быстрее, чем использование SQL_CALC_FOUND_ROWS.

Об этом много написано, особо не беспокойтесь: это во многом зависит от вашего запроса и версии базы данных. Лучшее, что вы можете сделать, - это протестировать оба способа, прежде чем внедрять настраиваемый поставщик данных.

Заключительные примечания:

Если вы действительно заботитесь о точности, подумайте о этот сценарий:

  • Если count(*) терпит неудачу, он обычно терпит неудачу для нескольких записей.
  • Если SELECT FOUND_ROWS() терпит неудачу ... ну, это может быть грандиозный провал!

И если вы действительно заботитесь о производительности, есть несколько хороших предложений в ответах на этот другой вопрос (отнеситесь к нему с зерном соли, он очень старый), мне особенно нравится этот.

person David    schedule 17.11.2016
comment
Спасибо за дополнительную информацию, Дэвид. Прошло так много времени с тех пор, как я работал над этой конкретной ситуацией, мне нужно было снова просмотреть код, чтобы вернуться к его внутренней работе и понять, как все это снова работает, лол ... если я когда-нибудь решу вернуться к этому, я обязательно возьму посмотрите, можно ли это улучшить с помощью этого метода. - person Brett; 18.11.2016
comment
Хотя меня беспокоит только отсутствие проверки страницы; нет, я не беспокоюсь о том, что пользователи вручную изменяют номер страницы и получают страницу не результатов, но я предполагаю, что может возникнуть ситуация, когда пользователь позволяет странице некоторое время бездействовать, в это время администратор удаляет некоторый контент, который затем делает последнюю страницу страницей 4, затем пользователь щелкает страницу 5 и не получает страницу результатов, и им остается интересно, что произошло, лол ........ с проверкой страницы они вернутся на страницу 4, но увидят новый контент. - person Brett; 18.11.2016
comment
@ Бретт. Хорошая точка зрения! Я обновил свой ответ. Кажется, вы всегда знаете о возможном изменении количества совпадающих записей между двумя действиями :) - person David; 19.11.2016
comment
Ха-ха, я должен быть :) Спасибо за изменения к вашему ответу, я уверен, что он пригодится многим, и я подумаю о том, чтобы реализовать его, если я решу, что текущее решение должно быть изменено :) - person Brett; 19.11.2016

Создайте свой запрос следующим образом:

$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.';
person crafter    schedule 17.04.2015
comment
Я не уверен, что понимаю. Не могли бы вы привести пример использования SqlDataProvider? Кроме того, в вашем примере используется Yii1, я использую Yii2. - person Brett; 17.04.2015
comment
Использование SQL_CALC_FOUND_ROWS не принесет вам пользы от использования SqlDataProvider. Вместо этого используйте 'totalCount' = ›$ this-› db- ›createCommand (SELECT * FROM user) -› where (status =: status, array (: status = ›1) -› queryScalar (), - person crafter; 18.04.2015
comment
@crafter Надеюсь, вы имеете в виду ВЫБРАТЬ СЧЕТЧИК (*) ОТ пользователя - person Alexander R.; 18.04.2015
comment
Да и нет. Если вы хотите отображать данные в сетке, например, с 50% активных пользователей, и хотите отображать только активных пользователей, вы можете указать отображение 1–10 из 1000 (активных) пользователей или отображение 1–10 из 2000 (все пользователей). Во второй части сетка покажет 1000 пользователей, значение 2000 здесь может не подходить. Вам решать. - person crafter; 18.04.2015
comment
В любом случае несколько запросов COUNT (*) будут работать намного быстрее и будут использовать гораздо меньше памяти, чем при использовании SELECT * FROM только для подсчета! Если у меня есть таблица пользователей 100k даже с 10-20 полями, и скажем, что 50% активно, ваш пользователь SELECT * FROM убьет сервер. Это неверно для SELECT данных для подсчета, вместо того, чтобы делать COUNT, который был разработан для того, чтобы делать это эффективным способом. - person Alexander R.; 18.04.2015
comment
Мое плохое, @AlexanderRavikovich. Я имел в виду select count (). .... Я не прочитал ваш комментарий должным образом и сосредоточился на предложении WHERE. Невозможно редактировать комментарий сейчас. - person crafter; 19.04.2015

Вам нужно 2 запроса, чтобы получить общее количество строк, если вы хотите использовать SQL_CALC_FOUND_ROWS вместо COUNT. Здесь 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 - количество элементов, выбранных для страницы.

person Tony    schedule 18.04.2015
comment
Прочтите, пожалуйста, весь мой вопрос, я уже указывал, что знаю, как получить значение из SQL_CALC_FOUND_ROWS в обычных экземплярах, но я пытаюсь заставить его работать с SqlDataProvider. - person Brett; 18.04.2015
comment
Я не уверен, чем это отличается от выполнения COUNT(*) заранее; SQL_CALC_FOUND_ROWS должен появиться после выполнения запроса внутри SqlDataProvider. - person Brett; 18.04.2015
comment
возможно, вам стоит проверить отладчик yii2 и просмотреть журнал запросов. если вы хотите выполнить SELECT FOUND_ROWS() после запроса в SqlDataProvider, убедитесь, что он идет точно после этого запроса. - person Tony; 18.04.2015
comment
вы можете создать свой собственный dataprovider класс, унаследованный от SqlDataProvider, и переопределить prepareTotalCount() метод - person Tony; 19.04.2015
comment
Полагаю, есть на что посмотреть :) - person Brett; 19.04.2015