Comments 27
Код вроде
$banner->hits++;
$banner->save();
точно имеет смысл рефакторить, т.к. если он будет исполняться параллельно, то значение счетчика будет неверным. Если два пользователя получат одно и то же текущее значение счетчика (например, 1000), прибавят к нему единицу и попытаются сохранить, то получится 1001, а не 1002, как должно бы быть
$banner->hits++;
$banner->save();
точно имеет смысл рефакторить, т.к. если он будет исполняться параллельно, то значение счетчика будет неверным. Если два пользователя получат одно и то же текущее значение счетчика (например, 1000), прибавят к нему единицу и попытаются сохранить, то получится 1001, а не 1002, как должно бы быть
у меня этот код исполняется по крону, соответственно, в единственно экземпляре. в статье об этом есть фраза.
если бы мне нужно было параллельное выполнение, то сделал как-то так:
если бы мне нужно было параллельное выполнение, то сделал как-то так:
$sql = 'SELECT hits FROM {{banners}} WHERE id = :id FOR UPDATE';
$command = $connection->createCommand($sql);
$hits = $command->queryScalar(array(':id' => xxx));
$hits++;
$sql = 'UPDATE {{banners}} SET hits = :hits WHERE id = :id';
$command = $connection->createCommand($sql);
$command -> execute(array(':id' => xxx, ':hits' => $hits));
Не заметил, прошу прощения…
А вы не пробовали выносить статистику в отдельную таблицу?
А вы не пробовали выносить статистику в отдельную таблицу?
Не совсем понял, что вы предлагаете вынести?
Чтобы счетчики хитов были в другой таблице и сгруппированные, например, по суткам? В рамках задачи, которую я привел в качестве примера, ничего не требовалось :)
В другой подобной задаче делал так:
сырые данные складывались в memory-таблицу, по крону вызывался скрипт, который агрегировал в отдельную таблицу значения счетчиков таким хитрым запросом:
Чтобы счетчики хитов были в другой таблице и сгруппированные, например, по суткам? В рамках задачи, которую я привел в качестве примера, ничего не требовалось :)
В другой подобной задаче делал так:
сырые данные складывались в memory-таблицу, по крону вызывался скрипт, который агрегировал в отдельную таблицу значения счетчиков таким хитрым запросом:
INSERT INTO {{stats}} SET id=:id, media=:media, created_at=NOW(), hits=1 ON DUPLICATE KEY UPDATE hits=hits+1
UPDATE `banners` SET `id`=1, `name`='test', `info`='long-long description', `hits`=3560, `iduser`='2', `created_at`='2012-02-17 13:14:02', ... WHERE `banners`.`id`=1
Как я понял из этого запроса, есть таблица с баннерами, в которой хранится информация о баннере (название, какое-то описание, дата создания) + общее число хитов этого баннера.
Мне было бы интересно вынесение поля hits из этой таблицы, т.е. создание отдельной таблицы banners_hits (banner_id, hits), в которой хранятся те же самые данные, но отдельно от всех остальных данных по баннерам. Логика такая: данные по хитам (скорее всего) нужны не всегда и обновляются гораздо чаще, чем остальные данные, поэтому может иметь смысл хранить их отдельно в сравнительно небольшой табличке.
В общем это только мои предположения и мне хотелось знать, не пробовали ли вы что-то подобное.
а может секрет в том, что save() помимо сохранения всех полей сначала производит валидацию?
код выше даст вам примерно тот же прирост производительности
$banner->hits++;
$banner->update(array('hits'));
код выше даст вам примерно тот же прирост производительности
Между таблицами banners и banners_hits вы хотите сделать связь 1:1? Если так, думаю, выигрыша ни в чем не будет, в том числе в скорости обновления. Конечно, если обновлять через saveCounters или подобным быстрым способом.
>небольшой табличке.
так и banners небольшая… там ни в коем случает не хранится картинка или флэшка.
В отдельную таблицу стоит вынести счетчики, если связь 1: многим. Например, хиты сгруппированные по дням.
>небольшой табличке.
так и banners небольшая… там ни в коем случает не хранится картинка или флэшка.
В отдельную таблицу стоит вынести счетчики, если связь 1: многим. Например, хиты сгруппированные по дням.
Если в save() передавать список сохраняемых атрибутов, то код SQL ведь будет генерироваться такой же, как при помощи saveCounters()? Выглядит как синтаксический сахар, а не серьёзная оптимизация.
Именно.
$banner->save(true,array('hits'));
$banner->save(true,array('hits'));
и не save а update, чтобы не выполнялась валидация и колбеки на неё
Для пущей оптимизации тогда уж можно сразу updateByPk — тогда и beforeSave/afterSave пропустятся. :)
В update вызываются колбэки beforeSave и afterSave.
А вот сравнение скорости таких конструкций
$banner->hits++;
$banner->updateByPk($banner->id,array('hits'=>$banner->hits));
и
$banner->saveCounters(array('hits'=>1));
показало одинаковый результат. saveCounters выглядит проще.
Сахар в общем :)
А вот сравнение скорости таких конструкций
$banner->hits++;
$banner->updateByPk($banner->id,array('hits'=>$banner->hits));
и
$banner->saveCounters(array('hits'=>1));
показало одинаковый результат. saveCounters выглядит проще.
Сахар в общем :)
Не по теме Yii но всё же: что касается счётчиков, лучше промежуточные значения аггрегировать в кеше, например memcached и redis умеют делать атомарный инкремент.
А еще лучше не в каждой итерации вызывать save() или updateCounters(), а собрать это куда-нибудь, а потом сделать updateCounters(array('hit'=>$count))
Что значит собрать куда-нибудь? :)
updateCounters отличается от saveCounters тем, что изменяет значения во всей таблице, если не определено условие.
Ваш пример $banner->updateCounters(array('hits'=>$count)) увеличит счетчик у всех баннеров в таблице на +$count. Это как бы не то, что требуется :)
Чтобы изменить только некоторые строки, нужно написать кондишен, например, $banner->updateCounters(array('hits'=>1), 'id in (1,2,3)').
Это в итоге сформирует такой запрос UPDATE `banners` SET `hits`=`hits`+1 WHERE id in (1,2,3)
updateCounters отличается от saveCounters тем, что изменяет значения во всей таблице, если не определено условие.
Ваш пример $banner->updateCounters(array('hits'=>$count)) увеличит счетчик у всех баннеров в таблице на +$count. Это как бы не то, что требуется :)
Чтобы изменить только некоторые строки, нужно написать кондишен, например, $banner->updateCounters(array('hits'=>1), 'id in (1,2,3)').
Это в итоге сформирует такой запрос UPDATE `banners` SET `hits`=`hits`+1 WHERE id in (1,2,3)
пардон, ошибся с методом, конечно saveCounters.
А собрать куда-нибудь это в смысле вот так (утрированно):
А собрать куда-нибудь это в смысле вот так (утрированно):
var $hit_counter = 0;
for($i=0;$i<1000;$i++) {
$banner_counter++;
}
$banner->saveCounters(array('hits'=>$hit_counter ));
А не проще его вручную сформировать, если уж такое внимательное отношение к производительности?
Зачем поднимать под каждый баннер объект ActiveRecord — только лишь, чтобы сделать updateCounters?
Yii::app()->db-> createCommand('UPDATE banners SET hits = hits + 1 WHERE id IN(1, 2, 3)')-> execute();
Зачем поднимать под каждый баннер объект ActiveRecord — только лишь, чтобы сделать updateCounters?
Yii::app()->db-> createCommand('UPDATE banners SET hits = hits + 1 WHERE id IN(1, 2, 3)')-> execute();
Я, конечно, малоопытный «специалист», но почему вы не используете операции инкремента/декремента в адаптерах кэша?
Когда же в CActiveRecord появится UnitOfWork?
1) почему бы при отработке кроном не собирать промежуточные суммы в массиве, и уже потом в конце записывать эти изменения в базу? т.е. не 2000 раз увеличивать на 1, а один раз сразу на 2000.
2) чтобы метод save() работал быстро, фреймворк должен поддерживать т.н. dirty статусы для полей. Модель должна знать какие поля изменились после её загрузки из БД, и при сохранении записывать только эти поля
2) чтобы метод save() работал быстро, фреймворк должен поддерживать т.н. dirty статусы для полей. Модель должна знать какие поля изменились после её загрузки из БД, и при сохранении записывать только эти поля
Цель статьи рассказать о том, что один метод AR сильно быстрее другого, а не о способах создать крутую баннерокрутилку. Возможно, я привел неудачный пример.
По п.1.: А если 2000 _разных_ баннеров показались 1 раз? :)
Насчет п.2 согласен абсолютно, yii мог бы отслеживать сам, что надо обновить только счетчик.
По п.1.: А если 2000 _разных_ баннеров показались 1 раз? :)
Насчет п.2 согласен абсолютно, yii мог бы отслеживать сам, что надо обновить только счетчик.
Sign up to leave a comment.
Быстрые счетчики в Yii