Альтернатива COUNT для innodb для предотвращения сканирования таблицы?
Мне удалось собрать запрос, который работает для моих нужд, хотя и более сложный, чем я надеялся. Но для размера таблиц запрос медленнее, чем должен быть (0,17 с). Причина, основанная на приведенном ниже EXPLAIN
, заключается в том, что в таблице meta_relationships
есть сканирование таблицы из-за того, что она имеет COUNT
в предложении WHERE
на движке innodb
.
Query:
SELECT
posts.post_id,posts.post_name,
GROUP_CONCAT(IF(meta_data.type = 'category', meta.meta_name,null)) AS category,
GROUP_CONCAT(IF(meta_data.type = 'tag', meta.meta_name,null)) AS tag
FROM posts
RIGHT JOIN meta_relationships ON (posts.post_id = meta_relationships.object_id)
LEFT JOIN meta_data ON meta_relationships.meta_data_id = meta_data.meta_data_id
LEFT JOIN meta ON meta_data.meta_id = meta.meta_id
WHERE meta.meta_name = computers AND meta_relationships.object_id
NOT IN (SELECT meta_relationships.object_id FROM meta_relationships
GROUP BY meta_relationships.object_id HAVING count(*) > 1)
GROUP BY meta_relationships.object_id
В этом конкретном запросе выбираются сообщения, которые имеют ТОЛЬКО категорию computers
. Цель count > 1
заключается в исключении сообщений, содержащих computers/hardware
, computers/software
и т.д. Чем больше выбранных категорий, тем выше будет счет.
В идеале я бы хотел, чтобы он функционировал следующим образом:
WHERE meta.meta_name IN ('computers') AND meta_relationships.meta_order IN (0)
или
WHERE meta.meta_name IN ('computers','software')
AND meta_relationships.meta_order IN (0,1)
и т.д..
Но, к сожалению, это не работает, потому что оно не учитывает, что может быть meta_relationships.meta_order
= 2.
Я пробовал...
WHERE meta.meta_name IN ('computers')
GROUP BY meta_relationships.meta_order
HAVING meta_relationships.meta_order IN (0) AND meta_relationships.meta_order NOT IN (1)
но он не возвращает правильное количество строк.
EXPLAIN:
id select_type table type possible_keys key key_len ref rows Extra
1 PRIMARY meta ref PRIMARY,idx_meta_name idx_meta_name 602 const 1 Using where; Using index; Using temporary; Using filesort
1 PRIMARY meta_data ref PRIMARY,idx_meta_id idx_meta_id 8 database.meta.meta_id 1
1 PRIMARY meta_relationships ref idx_meta_data_id idx_meta_data_id 8 database.meta_data.meta_data_id 11 Using where
1 PRIMARY posts eq_ref PRIMARY PRIMARY 4 database.meta_relationships.object_id 1
2 MATERIALIZED meta_relationships index NULL idx_object_id 4 NULL 14679 Using index
Таблицы/Индексы:
мета
Эта таблица содержит имена категорий и тегов.
индексы:
ПЕРВИЧНЫЙ КЛЮЧ (meta_id
), КЛЮЧ idx_meta_name
(meta_name
)
meta_dataSTRONG >
В этой таблице содержатся дополнительные данные о категориях и тегах, таких как тип (категория или тег), описание, родительский элемент, счет.
индексы:
ПЕРВИЧНЫЙ КЛЮЧ (meta_data_id
), КЛЮЧ idx_meta_id
(meta_id
)
meta_relationships
Это таблица соединений/поиска. Он содержит внешний ключ для posts_id, внешний ключ для meta_data_id, а также содержит порядок категорий.
индексы:
ПЕРВИЧНЫЙ КЛЮЧ (relationship_id
), КЛЮЧ idx_object_id
(object_id
), КЛЮЧ idx_meta_data_id
(meta_data_id
)
- Счет позволяет мне выбирать только сообщения с правильным уровнем категории. Например, компьютеры категории имеют сообщения только с категорией компьютеров, но также имеют сообщения с компьютерами/оборудованием. Счетчик фильтрует сообщения, содержащие эти дополнительные категории. Надеюсь, это имеет смысл.
- Я считаю, что ключ к оптимизации запроса состоит в том, чтобы полностью уйти от выполнения
COUNT
.
- Альтернативой
COUNT
может быть вместо этого использовать meta_relationships.meta_order
или meta_data.parent
.
- Таблица
meta_relationships
будет расти быстро и с текущим размером (~ 15K строк). Я надеюсь получить время выполнения за 100 секунд, а не за 10 секунд.
- Поскольку в предложении
WHERE
для каждой категории/тега должно быть несколько условий, любой предпочтительный для динамического запроса ответ считается предпочтительным.
- Я создал IDE с пример данных.
Как я могу оптимизировать этот запрос?
EDIT:
Я никогда не мог найти оптимальное решение этой проблемы. Это была комбинация рекомендаций smcjones по улучшению индексов, для которых я бы рекомендовал сделать EXPLAIN
и посмотреть EXPLAIN Output Format, затем изменить индексы к тому, что дает вам лучшую производительность.
Кроме того, рекомендация hpf добавить еще один столбец с общим счетом очень помогла. В конце концов, после изменения индексов, я перешел к этому запросу.
SELECT posts.post_id,posts.post_name,
GROUP_CONCAT(IF(meta_data.type = 'category', meta.meta_name,null)) AS category,
GROUP_CONCAT(IF(meta_data.type = 'tag', meta.meta_name,null)) AS tag
FROM posts
JOIN meta_relationships ON meta_relationships.object_id = posts.post_id
JOIN meta_data ON meta_relationships.meta_data_id = meta_data.meta_data_id
JOIN meta ON meta_data.meta_id = meta.meta_id
WHERE posts.meta_count = 2
GROUP BY posts.post_id
HAVING category = 'category,subcategory'
После избавления от COUNT
, большой производительный убийца был GROUP BY
и ORDER BY
, но индексы - ваш лучший друг. Я узнал, что при выполнении GROUP BY
предложение WHERE
очень важно, тем более конкретным вы можете добиться лучшего.
Ответы
Ответ 1
Поскольку проблема HAVING кажется проблемой, можете ли вы вместо этого создать поле флага в таблице posts и использовать это вместо этого? Если я правильно понял запрос, вы пытаетесь найти сообщения только с одной ссылкой meta_relationship. Если вы создали поле в своей таблице сообщений, которое было либо числом мета-ссылок для этой записи, либо логическим флагом для того, было ли только одно и проиндексировано, конечно, это, вероятно, было бы намного быстрее. Это было бы связано с обновлением поля, если сообщение было отредактировано.
Итак, рассмотрим следующее:
Добавить новое поле в таблицу сообщений, называемую num_meta_rel. Это может быть неподписанный tinyint, если у вас никогда не будет более 255 тегов для любого сообщения.
Обновите поле следующим образом:
UPDATE posts
SET num_meta_rel=(SELECT COUNT(object_id) from meta_relationships WHERE object_id=posts.post_id);
Для выполнения этого запроса потребуется некоторое время, но как только вы закончите, вы получите все подсчеты. Обратите внимание, что это можно сделать лучше с помощью соединения, но SQLite (Ideone) разрешает только подзапросы.
Теперь вы переписываете свой запрос следующим образом:
SELECT
posts.post_id,posts.post_name,
GROUP_CONCAT(IF(meta_data.type = 'category', meta.meta_name,null)) AS category,
GROUP_CONCAT(IF(meta_data.type = 'tag', meta.meta_name,null)) AS tag
FROM posts
RIGHT JOIN meta_relationships ON (posts.post_id = meta_relationships.object_id)
LEFT JOIN meta_data ON meta_relationships.meta_data_id = meta_data.meta_data_id
LEFT JOIN meta ON meta_data.meta_id = meta.meta_id
WHERE meta.meta_name = computers AND posts.num_meta_rel=1
GROUP BY meta_relationships.object_id
Если я сделал это правильно, исполняемый код здесь: http://ideone.com/ZZiKgx
Обратите внимание, что для этого решения требуется обновить num_meta_rel (выберите лучшее имя, это ужасно...), если сообщение имеет новый тег, связанный с ним. Но это должно быть намного быстрее, чем сканирование всей таблицы снова и снова.
Ответ 2
При сочетании оптимизированных запросов И оптимизации ваших таблиц вы получите быстрые запросы. Однако вы не можете иметь быстрые запросы без оптимизированной таблицы.
Я не могу этого достаточно подчеркнуть: если ваши таблицы правильно структурированы с правильным количеством индексов, вы не должны испытывать никаких полных табличных чтений в запросе типа GROUP BY... HAVING, если вы не сделаете это по дизайну.
На основе вашего примера я создал этот SQLFiddle.
Сравните это с SQLFiddle # 2, в котором я добавил индексы и добавил индекс UNIQUE
против meta.meta_naame
.
Из моего тестирования Fiddle # 2 работает быстрее.
Оптимизация запроса
Этот запрос приводил меня в бешенство, даже после того, как я сделал аргумент, что индексы будут лучшим способом его оптимизировать. Несмотря на то, что я все еще считаю, что таблица - это ваша самая большая возможность повысить производительность, казалось, что в MySQL должен быть лучший способ запустить этот запрос. У меня было откровение после спящего режима по этой проблеме, и я использовал следующий запрос (см. в SQLFiddle # 3):
SELECT posts.post_id,posts.post_name,posts.post_title,posts.post_description,posts.date,meta.meta_name
FROM posts
LEFT JOIN meta_relationships ON meta_relationships.object_id = posts.post_id
LEFT JOIN meta_data ON meta_relationships.meta_data_id = meta_data.meta_data_id
LEFT JOIN meta ON meta_data.meta_id = meta.meta_id
WHERE meta.meta_name = 'animals'
GROUP BY meta_relationships.object_id
HAVING sum(meta_relationships.object_id) = min(meta_relationships.object_id);
HAVING sum() = min()
на a GROUP BY
должен проверить, есть ли более одной записи каждого типа. Очевидно, каждый раз, когда появляется запись, она добавит больше к сумме. (Edit: В последующих тестах похоже, что это имеет такое же влияние, как count(meta_relationships.object_id) = 1
. О, ну, я думаю, вы можете удалить подзапрос и получить тот же результат).
Я хочу быть уверенным, что вы не заметите много, если бы какая-либо оптимизация по запросу, который я вам предоставил, если раздел WHERE meta.meta_name = 'animals'
не запрашивает индекс (желательно уникальный индекс, потому что я сомневаюсь, что вам понадобится больше, чем один из них и предотвратит случайное дублирование данных).
Итак, вместо таблицы, которая выглядит так:
CREATE TABLE meta_data (
meta_data_id BIGINT,
meta_id BIGINT,
type VARCHAR(50),
description VARCHAR(200),
parent BIGINT,
count BIGINT);
Вы должны убедиться, что добавили первичные ключи и индексы, например:
CREATE TABLE meta_data (
meta_data_id BIGINT,
meta_id BIGINT,
type VARCHAR(50),
description VARCHAR(200),
parent BIGINT,
count BIGINT,
PRIMARY KEY (meta_data_id,meta_id),
INDEX ix_meta_id (meta_id)
);
Не переусердствуйте, но каждая таблица должна иметь первичный ключ, и в любое время, когда вы агрегируете или запрашиваете конкретное значение, должны быть индексы.
Когда индексы не используются, MySQL будет проходить через каждую строку таблицы, пока не найдет то, что вы хотите. В таком ограниченном примере, как ваш, это не займет слишком много времени (хотя оно все еще заметно медленнее), но когда вы добавляете тысячи и более записей, это станет чрезвычайно болезненным.
В будущем при просмотре ваших запросов попытайтесь определить, где происходят полные сканирование таблицы, и посмотрите, есть ли индекс в этом столбце. Хорошим местом для начала является то, где вы агрегируете или используете синтаксис WHERE
.
Заметка в столбце count
Я не нашел класть столбцы count
в таблицу, чтобы быть полезной. Это может привести к довольно серьезным проблемам с целостностью. Если таблица правильно оптимизирована, должно быть очень просто использовать count()
и получить текущий счетчик. Если вы хотите иметь его в таблице, вы можете использовать VIEW
, хотя это не самый эффективный способ сделать pull.
Проблема с помещением столбцов count
в таблицу состоит в том, что вам нужно обновить этот счет, используя либо логику TRIGGER
, либо, что хуже, приложения. Поскольку ваша программа масштабирует эту логику, можно либо потеряться, либо похоронить. Добавление этого столбца является отклонением от нормализации, и когда произойдет что-то подобное, должна существовать серьезная причина ОЧЕНЬ.
Некоторые дебаты о том, есть ли когда-либо веская причина для этого, но я думаю, что было бы разумно держаться подальше от этих дебатов, потому что есть большие аргументы с обеих сторон. Вместо этого я выберу гораздо меньшую битву и скажу, что вижу, что это вызывает у вас больше головных болей, чем пользы в этом случае, поэтому, вероятно, стоит проверить A/B.
Ответ 3
Посмотрите, дает ли это правильный ответ, возможно, быстрее:
SELECT p.post_id, p.post_name,
GROUP_CONCAT(IF(md.type = 'category', meta.meta_name, null)) AS category,
GROUP_CONCAT(IF(md.type = 'tag', meta.meta_name, null)) AS tag
FROM
( SELECT object_id
FROM meta_relation
GROUP BY object_id
HAVING count(*) = 1
) AS x
JOIN meta_relation AS mr ON mr.object_id = x.object_id
JOIN posts AS p ON p.post_id = mr.object_id
JOIN meta_data AS md ON mr.meta_data_id = md.meta_data_id
JOIN meta ON md.meta_id = meta.meta_id
WHERE meta.meta_name = ?
GROUP BY mr.object_id
Ответ 4
К сожалению, у меня нет возможности проверить производительность,
Но попробуйте мой запрос, используя ваши реальные данные:
http://sqlfiddle.com/#!9/81b29/13
SELECT
posts.post_id,posts.post_name,
GROUP_CONCAT(IF(meta_data.type = 'category', meta.meta_name,null)) AS category,
GROUP_CONCAT(IF(meta_data.type = 'tag', meta.meta_name,null)) AS tag
FROM posts
INNER JOIN (
SELECT meta_relationships.object_id
FROM meta_relationships
GROUP BY meta_relationships.object_id
HAVING count(*) < 3
) mr ON mr.object_id = posts.post_id
LEFT JOIN meta_relationships ON mr.object_id = meta_relationships.object_id
LEFT JOIN meta_data ON meta_relationships.meta_data_id = meta_data.meta_data_id
INNER JOIN (
SELECT *
FROM meta
WHERE meta.meta_name = 'health'
) meta ON meta_data.meta_id = meta.meta_id
GROUP BY posts.post_id
Ответ 5
Использование
sum(1)
вместо
count(*)