вторник, 20 июля 2010 г.

Count(*) vs Triggers

Для подсчета количества комментариев пользователя на сайте, обычно, делают так: "select count(*) from "comment" where user_id = 123". Тоже самое касается подсчета фотографий, личных сообщений, проектов, постов и много другого в зависимости от проекта. До определенного момента это всех устраивает, пока размеры таблиц не увеличатся на столько, что эти запросы (с count) начинают безбожно тормозить.



Самым удачным способом избежать этого - счетчики на триггерах.
Что такое счетчик (в данном контексте)? Это поле которое хранит количество нужных записей и обновляется при каждом изменении считаемых строк (но не с помощью count(*), как некоторые подумали=).

Вернемся к нашим пользователям и их комментариям. В таблице пользователей создадим поле "comments_count". На таблицу комментариев нужно повесить триггер или триггеры, в зависимости от используемой базы данных.

На событие insert:

UPDATE "user" SET comments_count =  comments_count + 1 
  WHERE id = NEW.user_id;

На событие delete:

UPDATE "user" SET comments_count =  comments_count - 1 
  WHERE id = OLD.user_id;

Если у вас данные не удаляются, а используется поле для маркировки удаленных записей, то сюда нужно еще добавить триггер на update, и везде проставить условия.

Если счетчики сбиваются (у покрытых тестами маловероятно), то можно выполнить синхронизацию.

UPDATE "user" SET comments_count = (
  SELECT COUNT(*) FROM "comment" 
    WHERE "user".id = "comment".user_id
);

20 коммент.:

  1. АнонимныйJul 21, 2010 01:02 AM
    А если сделать поле comments_count и обновлять его на уровне модели при вставке и удаления комментария..
    ОтветитьУдалить
  2. КириллJul 21, 2010 01:42 AM
    Да, об этом тоже стоило упомянуть. Это плохой ход. Тогда если вставка идет напрямую в базу, а такое возможно при тестировании, то счетчики собьются. После этого можно долго гадать что не так произошло. Ну и в коде, опять же, можно обойти этот механизм, например, крон скрипт который удаляет определенные комменты.
    ОтветитьУдалить
  3. АнонимныйJul 21, 2010 01:45 AM
    Доводы не убедительные. Тестировать необходимо бизнес логику и удалять крон скрипт тоже должен использую бизнес логику.
    ОтветитьУдалить
  4. КириллJul 21, 2010 01:53 AM
    К сожалению я не карнеги).
    ОтветитьУдалить
  5. lcfJul 21, 2010 06:16 AM
    To Анонимный, если это и делать на уровне модели, то это должен делать Data Access Object, правильно? В бизнес логике будет только $user->getCommentsCount(). - а внутри что там уже зависит от выбранного хранилища данных, от того как реализованн подсчет комментариев и так далее.

    Получается, что с точки зрения бизнес логики - нет никакой разницы как это реализованно уже в Data Access Object. Значит, концептуальной разницы, разницы с точки зрения дизайна приложения - нет и можно говорить только о таких вещах как разница в производительности и удобстве. Я думаю по обоим пунктам решение Кирилла выигрывает по сравнению с ручным обновлением.
    ОтветитьУдалить
  6. АнонимныйJul 25, 2010 11:18 PM
    А что делать со сменой хранилище данных? Переписывать триггеры или отказываться от них если хранилище данных не поддерживает триггеры?
    На ASP.Net я постоянно использую только триггеры и процедуры, простых запросов у меня почти нет. Но на пыхе все не так просто.
    ОтветитьУдалить
  7. КириллJul 25, 2010 11:38 PM
    Смена базы бывает очень редко в проектах любого уровня. Кстати, в проекте где я работаю, такое было) и мы переносили порядка 300 триггеров.

    На php существует не мало проектов где вся бизнес-логика находится в базе. Более того при определенных условиях это единственный способ построения приложения, например, при использовании pl/proxy http://habrahabr.ru/blogs/postgresql/45475/ и не важно что использует приложение php, asp или еще что то.
    ОтветитьУдалить
  8. ИгорьJul 26, 2010 12:36 AM
    Вот бы мне научится говорить так хороша как lcf :)
    Я скажу просто :) То что может делать база, она и должна делать. Как по мне триггеры более быстрый вариант + бизнес логика становится проще. Сейчас в одном проекте джуниор делал пересчет счетчика элементов в каталогах и в бизнес логике получилось довольно много кода. Но триггеры у нас в конторе пока не прижились.

    Кстати Саш, а почему пересчет должен быть именно в getCommentsCount()? Или я тебя не правильно понял? Как писали в выше почему бы не делать при C(R)UD? Ведь выбор кол-ва может происходить в интерфейсе часто, а вот C(R)UD не так часто.
    ОтветитьУдалить
  9. openidAug 10, 2010 01:01 PM
    Метод неплох, но будет работать только если нужно знать общее количество записей в таблице.

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

    В общем случае индексы помогают решить эту задачу достаточно эффективно.
    ОтветитьУдалить
  10. КириллAug 10, 2010 10:52 PM
    > Но чаще возникает необходимость считать записи по определенным критериям. В этом случае триггеры не помогут (как вариант, конечно, можно хранить счетчик на каждый из критериев).

    Это кстати очень сильно зависит от проекта. А вообще удобство данного подхода в простых и удобных запросах. Например если мы выводим список тем и кол-во комментариев к ним, нам нужно будет брать данные всего из одной таблицы.

    > В общем случае индексы помогают решить эту задачу достаточно эффективно.

    Хм, насколько я знаю индексы не влияют на count(), в чистом виде. Они влияют на условия которые используются вместе с ним.
    ОтветитьУдалить
  11. Oleg LobachAug 11, 2010 11:21 PM
    > Хм, насколько я знаю индексы не влияют на count(), в чистом виде. Они влияют на условия которые используются вместе с ним.

    Если условия каунта совпадут с индексом, будет использоваться он.
    ОтветитьУдалить
  12. КириллAug 12, 2010 12:26 AM
    Я об этом и написал), но при подсчете строк совпавших по условию индекс все равно не используется. То есть если в условие попадут миллионы строк то count будет работать очень медленно несмотря ни на какие индексы.
    ОтветитьУдалить
  13. Oleg LobachAug 12, 2010 03:00 AM
    > То есть если в условие попадут миллионы строк то count будет работать очень медленно несмотря ни на какие индексы.

    Хм... а эксплейн с тобой не согласен
    ОтветитьУдалить
  14. КириллAug 12, 2010 03:13 AM
    postgresql

    SELECT COUNT(*) FROM user_view WHERE id > 10
    Кол-во выбранных записей: 9405830
    Время работы: 3498 мс

    "Aggregate (cost=1295906.46..1295906.47 rows=1 width=8)"
    " -> Seq Scan on user_view (cost=0.00..1272125.36 rows=9512438 width=8)"
    " Filter: (id > 10)"


    Используется Seq Scan

    Здесь описано почему так происходит http://wiki.postgresql.org/wiki/Slow_Counting. А так же то что where всего лишь уменьшает кол-во строк, которые все равно придется считать.
    ОтветитьУдалить
  15. КириллAug 12, 2010 03:21 AM
    Кстати там же ниже приводятся ссылки на статьи в которых описан этот же способ.

    Another popular approach for applications that need a row count but can tolerate it not including transactions that are in the middle of being committed is to use a trigger-based mechanism to count the rows in the table. One or both of these techniques are covered in the following:
    http://www.varlena.com/GeneralBits/120.php
    http://www.varlena.com/GeneralBits/49.php
    ОтветитьУдалить
  16. Oleg LobachAug 12, 2010 03:24 AM
    Гы... про постгрес спорить небуду - практически не имел с ним дел. Я говорил про мускуль. :)
    ОтветитьУдалить
  17. КириллAug 12, 2010 03:27 AM
    В мускуле в innodb тоже самое если я не ошибаюсь. Вот в myisam, да там каунт моментально считается это связано с отсутствием транзакций. А в крупных проектах myisam не используется.
    ОтветитьУдалить
  18. КириллAug 12, 2010 03:31 AM
    Точнее там так http://denis.boltikov.ru/2008/03/12/count-dlya-tablic-innodb-perevod-s-mysql-performance-blog/
    ОтветитьУдалить
  19. Oleg LobachAug 12, 2010 03:32 AM
    Эксплейн явно показывает, что используется индекс, если это возможно

    А в myisam-е каунт считается моментально только всех записей - там просто счетчик записей хранится в свойствах таблицы
    ОтветитьУдалить
  20. КириллAug 12, 2010 03:36 AM
    Да, говоря про отсутствие транзакций я как раз имел ввиду что кол-во строк в любой момент известно и счетчик хранится в метаданных таблицы.
    ОтветитьУдалить