Обновите кэш счетчика «многие ко многим» для нескольких строк и всех их родителей в одном запросе - PullRequest
1 голос
/ 25 ноября 2010

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

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

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

Мое приложение дошло до того, что после того, как пост создан с категориями, или его категории отредактированы, или тот, у которого были категории, удалены, у меня есть список идентификаторов категорий старой и новой категорий, чья post_count поле нуждается в обновлении. Я надеялся, что в следующий раз смогу выполнить один запрос: обновить поля under_post_count для всех указанных категорий и всех их родителей, указав количество отдельных сообщений, назначенных каждой категории или ее дочерним элементам *. 1019 *.

Вот SQL, необходимый для создания таблиц и некоторых тестовых данных для категорий:

CREATE TABLE `categories` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `parent_id` int(11) DEFAULT NULL,
  `lft` int(11) DEFAULT NULL,
  `rght` int(11) DEFAULT NULL,
  `name` varchar(255) NOT NULL,
  `post_count` int(11) NOT NULL DEFAULT '0',
  `under_post_count` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM;

CREATE TABLE `categories_posts` (
  `category_id` int(11) NOT NULL,
  `post_id` int(11) NOT NULL,
  PRIMARY KEY (`category_id`,`post_id`)
) ENGINE=MyISAM;

INSERT INTO `categories` (`id`, `parent_id`, `lft`, `rght`, `name`) VALUES
(1, NULL, 1, 8, 'Cat 1'),
(4, 1, 2, 3, 'Cat 1.1'),
(5, 1, 4, 5, 'Cat 1.2'),
(6, 1, 6, 7, 'Cat 1.3'),
(2, NULL, 9, 16, 'Cat 2'),
(7, 2, 10, 11, 'Cat 2.1'),
(8, 2, 12, 13, 'Cat 2.2'),
(9, 2, 14, 15, 'Cat 2.3'),
(3, NULL, 17, 24, 'Cat 3'),
(10, 3, 18, 19, 'Cat 3.1'),
(11, 3, 20, 21, 'Cat 3.2'),
(12, 3, 22, 23, 'Cat 3.3');

Выполните это несколько раз, чтобы создать тестовые данные для таблицы categories_posts:

INSERT IGNORE INTO `categories_posts` (`category_id`, `post_id`) 
SELECT `id`, CEILING(10 * RAND()) FROM `categories` ORDER BY RAND() LIMIT 6

Может кто-нибудь понять это, ваша помощь будет высоко ценится?

1 Ответ

3 голосов
/ 25 ноября 2010

ну, есть несколько способов снять кожу с кошки (при условии 5.1 и триггеры)

  • вы можете обновить все с уровня приложения

  • вы можете запускать обновления до post_count с categories_posts и запускать обновления (каскад) до under_post_count с categories

  • наконец, вы можете запускать все обновления с categories_posts

Также, в зависимости от фактического количества категорий, вам может не потребоваться денормализовать under_post_count, так как получить его с помощью

довольно просто и недорого.
SELECT c.id, SUM(cc.post_count) 
FROM categories c 
LEFT JOIN categories cc ON c.lft <= cc.lft AND c.rght >= cc.rght 
GROUP BY c.id;

Извлечение фактического количества точных совпадений

SELECT c.id, COUNT(*) 
FROM categories c 
LEFT JOIN categories_posts cp ON c.id = cp.post_id 
GROUP BY c.id;

Объединение двух дает подсчет, включая иерархии

SELECT c.id, COUNT(*) 
FROM categories c 
LEFT JOIN categories cc ON c.lft <= cc.lft AND c.rght >= cc.rght 
LEFT JOIN categories_posts cp ON cc.id = cp.post_id
GROUP BY c.id;

EDIT

Построение операторов обновления из вышеперечисленного не должно быть таким сложным

UPDATE categories 
SET post_count = (SELECT COUNT(*) 
                  FROM categories_posts cp 
                  WHERE cp.post_id = categories.id)

должно работать на post_count

Ситуация для under_post_count отличается, так как mysql не любит слышать, что целевая таблица упоминается в части where, поэтому вы должны сделать какое-то чудовище, подобное этому

UPDATE categories LEFT JOIN 
       (SELECT c.id, COUNT(*) AS result 
        FROM categories c 
        LEFT JOIN categories cc ON c.lft <= cc.lft AND c.rght >= cc.rght 
        INNER JOIN categories_posts cp ON cc.id = cp.post_id
        GROUP BY c.id) AS x ON categories.id = x.id
SET under_post_count = x.result

EDIT2
На самом деле во всех вышеперечисленных запросах есть ошибка - всякий раз, когда я присоединялся к категориям и сообщениям, я должен был присоединиться к cc.id = cp.category_id, а не cp.post_id, что я тогда не проверял. Не хочется исправляться ... но только в этом последнем запросе

UPDATE categories LEFT JOIN 
       (SELECT c.id, COUNT(*) AS result 
        FROM categories c 
        LEFT JOIN categories cc ON c.lft <= cc.lft AND c.rght >= cc.rght 
        INNER JOIN categories_posts cp ON cc.id = cp.category_id
        INNER JOIN posts p ON cp.post_id = p.id
        WHERE p.status = 'published'
        GROUP BY c.id) AS x ON categories.id = x.id
SET under_post_count = x.result,
    post_count = (SELECT COUNT(*) 
                  FROM categories_posts cp 
                  WHERE cp.category_id = categories.id)

EDIT3
Всего несколько заметок:

  • Приведенный выше запрос исправит under_post_count и post_count независимо от состояния данных,
  • существуют более дешевые запросы, которые, если ваши уровни доступа к данным должным образом абстрагированы, защищены и если вы можете гарантировать атомарность - эти запросы будут выполнять post_count = post_count +/- 1 только для соответствующих записей в статусах (аналогично under_post_count),
  • в случае, если вы не можете надежно эмулировать триггеры с уровня приложения, все равно может быть дешевле проверить, нужно ли вам выполнять вышеуказанные запросы (даже если mysql довольно хорош в этом отношении, но если вы хотите быть независимым от БД), или принять какую-то стратегию, при которой вы обычно просто увеличиваете / уменьшаете счетчики и только периодически пересчитываете число.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...