Pull to refresh

Comments 27

Код вроде
$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-таблицу, по крону вызывался скрипт, который агрегировал в отдельную таблицу значения счетчиков таким хитрым запросом:
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'));

код выше даст вам примерно тот же прирост производительности
save вызовет колбэки beforeSave и afterSave, а это может сильно отразится на скорости, если они определены. false отрубает валидацию это конечно плюс, но saveCounters или updateByPk оптимальнее. см.коммент ниже.
Между таблицами banners и banners_hits вы хотите сделать связь 1:1? Если так, думаю, выигрыша ни в чем не будет, в том числе в скорости обновления. Конечно, если обновлять через saveCounters или подобным быстрым способом.
>небольшой табличке.
так и banners небольшая… там ни в коем случает не хранится картинка или флэшка.

В отдельную таблицу стоит вынести счетчики, если связь 1: многим. Например, хиты сгруппированные по дням.
Вам ещё не будет запросы к основной таблице из query cache вымывать. Что тоже полезно.
Если в save() передавать список сохраняемых атрибутов, то код SQL ведь будет генерироваться такой же, как при помощи saveCounters()? Выглядит как синтаксический сахар, а не серьёзная оптимизация.
и не save а update, чтобы не выполнялась валидация и колбеки на неё
Для пущей оптимизации тогда уж можно сразу updateByPk — тогда и beforeSave/afterSave пропустятся. :)
В update вызываются колбэки beforeSave и afterSave.
А вот сравнение скорости таких конструкций
$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)
пардон, ошибся с методом, конечно 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 в Yii.
А то так и до nosql, и до шейдеров договоримся :)
Предлагайте на github, посмотрим, что можно сделать.
1) почему бы при отработке кроном не собирать промежуточные суммы в массиве, и уже потом в конце записывать эти изменения в базу? т.е. не 2000 раз увеличивать на 1, а один раз сразу на 2000.

2) чтобы метод save() работал быстро, фреймворк должен поддерживать т.н. dirty статусы для полей. Модель должна знать какие поля изменились после её загрузки из БД, и при сохранении записывать только эти поля
Цель статьи рассказать о том, что один метод AR сильно быстрее другого, а не о способах создать крутую баннерокрутилку. Возможно, я привел неудачный пример.
По п.1.: А если 2000 _разных_ баннеров показались 1 раз? :)
Насчет п.2 согласен абсолютно, yii мог бы отслеживать сам, что надо обновить только счетчик.
Sign up to leave a comment.

Articles