Mysql slow count в запросе соединения
поэтому у меня есть две таблицы, которые мне нужно, чтобы иметь возможность подсчитывать. Один из них содержит контент, а другой - отношения между ним и таблицей категорий. Вот DDl:
CREATE TABLE content_en (
id int(11) NOT NULL AUTO_INCREMENT,
title varchar(100) DEFAULT NULL,
uid int(11) DEFAULT NULL,
date_added int(11) DEFAULT NULL,
date_modified int(11) DEFAULT NULL,
active tinyint(1) DEFAULT NULL,
comment_count int(6) DEFAULT NULL,
orderby tinyint(4) DEFAULT NULL,
settings text,
permalink varchar(255) DEFAULT NULL,
code varchar(3) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY id (id),
UNIQUE KEY id_2 (id) USING BTREE,
UNIQUE KEY combo (id,active) USING HASH,
KEY code (code) USING BTREE
) ENGINE=MyISAM AUTO_INCREMENT=127126 DEFAULT CHARSET=utf8;
а для другой таблицы
CREATE TABLE content_page_categories (
catid int(11) unsigned NOT NULL,
itemid int(10) unsigned NOT NULL,
main tinyint(1) DEFAULT NULL,
KEY itemid (itemid),
KEY catid (catid),
KEY combo (catid,itemid) USING BTREE
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Выполняемый запрос:
SELECT count(*)
FROM content_page_categories USE INDEX (combo)
INNER JOIN content_en USE INDEX (combo) ON (id = itemid)
WHERE catid = 1 AND active = 1 ;
Обе таблицы имеют 125k строк, и я не могу заставить запрос count работать достаточно быстро. Лучшее время, которое я получаю, - 0.175, что ужасно для этого количества строк. Выбор 100 строк выполняется так же быстро, как 0.01. Я пробовал как 3 или 4 варианта этого запроса, но в итоге тайминги примерно одинаковы. Кроме того, если я не делаю, то время, затрачиваемое USE, уменьшается на 3 раза медленнее.
Также попробовал следующее:
SELECT COUNT( *) FROM content_page_categories
INNER JOIN content_en ON id=itemid
AND catid = 1 AND active = 1 WHERE 1
и:
SELECT SQL_CALC_FOUND_ROWS catid,content_en.* FROM content_page_categories
INNER JOIN content_en ON (id=itemid)
WHERE catid =1 AND active = 1 LIMIT 1;
SELECT FOUND_ROWS();
Определения индексов:
content_en 0 PRIMARY 1 id A 125288 BTREE
content_en 0 id 1 id A 125288 BTREE
content_en 0 id_2 1 id A 125288 BTREE
content_en 0 combo 1 id A BTREE
content_en 0 combo 2 active A YES BTREE
content_en 1 code 1 code A 42 YES BTREE
content_page_categories 1 itemid 1 itemid A 96842 BTREE
content_page_categories 1 catid 1 catid A 10 BTREE
content_page_categories 1 combo 1 catid A 10 BTREE
content_page_categories 1 combo 2 itemid A 96842 BTREE
Любые идеи?
[EDIT]
Я загрузил образцы данных для этих таблиц здесь
результат объяснения:
mysql> explain SELECT count(*) FROM content_page_categories USE INDEX (combo) I<br>
NNER JOIN content_en USE INDEX (combo) ON (id = itemid) WHERE catid = 1 AND act<br>
ive = 1 ;
+----+-------------+-------------------------+-------+---------------+-------+---------+--------------------------+--------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------------------+-------+---------------+-------+---------+--------------------------+--------+--------------------------+
| 1 | SIMPLE | content_en | index | combo | combo | 6 | NULL | 125288 | Using where; Using index |
| 1 | SIMPLE | content_page_categories | ref | combo | combo | 8 | const,mcms.content_en.id | 1 | Using where; Using index |
+----+-------------+-------------------------+-------+---------------+-------+---------+--------------------------+--------+--------------------------+
2 rows in set (0.00 sec)
Ответы
Ответ 1
Слишком много записей для подсчета.
Если вам требуется более быстрое решение, вам нужно будет хранить агрегированные данные.
MySQL не поддерживает материализованные представления (или индексированные представления в терминах SQL Server), поэтому вам нужно будет создавать и поддерживать их самостоятельно.
Создать таблицу:
CREATE TABLE
page_active_category
(
active INT NOT NULL,
catid INT NOT NULL,
cnt BIGINT NOT NULL,
PRIMARY KEY
(active, catid)
) ENGINE=InnoDB;
затем заполнить его:
INSERT
INTO page_active_category
SELECT active, catid, COUNT(*)
FROM content_en
JOIN content_page_categories
ON itemid = id
GROUP BY
active, catid
Теперь, каждый раз, когда вы вставляете, удаляете или обновляете запись в content_en
или content_page_categories
, вы должны обновить соответствующую запись в page_active_category
.
Это можно сделать с двумя простыми триггерами как на content_en
, так и на content_page_categories
.
Таким образом, ваш исходный запрос может быть переписан как простой:
SELECT cnt
FROM page_active_category
WHERE active = 1
AND catid = 1
который является единственным поиском первичного ключа и, следовательно, мгновенным.
Ответ 2
Я загрузил ваши данные и попробовал несколько экспериментов. Я запускаю MySQL 5.6.12 на виртуальной машине CentOS на Macbook Pro. Времена, которые я наблюдал, могут использоваться для сравнения, но ваша система может иметь разную производительность.
Базовый футляр
Сначала я пробовал без предложений USE INDEX, потому что я избегаю переопределения оптимизаторов, где это возможно. В большинстве случаев простой запрос, например, должен использовать правильный индекс, если он доступен. Жесткое кодирование выбора индекса в запросе затрудняет использование лучшего индекса позже.
Я также использую имена корреляции (псевдонимы таблиц), чтобы сделать запрос более понятным.
mysql> EXPLAIN SELECT COUNT(*) FROM content_en AS e
INNER JOIN content_page_categories AS c ON c.itemid = e.id
WHERE c.catid = 1 AND e.active = 1\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: c
type: ref
possible_keys: combo,combo2
key: combo
key_len: 4
ref: const
rows: 71198
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: e
type: eq_ref
possible_keys: PRIMARY,combo2,combo
key: PRIMARY
key_len: 4
ref: test.c.itemid
rows: 1
Extra: Using where
- Это выполняется за 0,36 секунды.
Индекс покрытия
Я бы также хотел получить "Использование индекса" во второй таблице, поэтому мне нужен индекс (active, id) в этом порядке. Я должен был использовать INDEX в этом случае, чтобы убедить оптимизатора не использовать первичный ключ.
mysql> ALTER TABLE content_en ADD KEY combo2 (active, id);
mysql> explain SELECT COUNT(*) FROM content_en AS e USE INDEX (combo2)
INNER JOIN content_page_categories AS c ON c.itemid = e.id
WHERE c.catid = 1 AND e.active = 1\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: c
type: ref
possible_keys: combo,combo2
key: combo
key_len: 4
ref: const
rows: 71198
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: e
type: ref
possible_keys: combo2
key: combo2
key_len: 6
ref: const,test.c.itemid
rows: 1
Extra: Using where; Using index
rows
, сообщенный EXPLAIN, является важным индикатором того, сколько работы он предпримет для выполнения запроса. Обратите внимание, что rows
в приведенном выше EXPLAIN только 71k, намного меньше, чем 125k строк, которые вы получили при первом просмотре таблицы content_en.
- Выполняется через 0,44 секунды. Это неожиданно, потому что обычно запрос с использованием индекса покрытия является улучшением.
Преобразование таблиц в InnoDB
Я попробовал такое же решение индекса покрытия, как указано выше, но с InnoDB в качестве механизма хранения.
mysql> ALTER TABLE content_en ENGINE=InnoDB;
mysql> ALTER TABLE content_page_categories ENGINE=InnoDB;
У этого был тот же отчет EXPLAIN. Для нагрева буферного пула потребовалось 1 или 2 итерации, но затем производительность запроса утроилась.
-
Выполняется через 0,16 секунды.
-
Я также попытался удалить USE INDEX, а время немного увеличилось, до 0,17 секунды.
Решение @Matthew с STRAIGHT_JOIN
mysql> SELECT straight_join count(*)
FROM content_en
INNER JOIN content_page_categories use index (combo)
ON (id = itemid)
WHERE catid = 1 AND active = 1;
- Это выполняется за 0.20 - 0.22 секунды.
Решение @bobwienholt, денормализация
Я попробовал решение, предложенное @bobwienholt, используя денормализацию, чтобы скопировать атрибут active
в таблицу content_page_categories
.
mysql> ALTER TABLE content_page_categories ADD COLUMN active TINYINT(1);
mysql> UPDATE content_en JOIN content_page_categories ON id = itemid
SET content_page_categories.active = content_en.active;
mysql> ALTER TABLE content_page_categories ADD KEY combo3 (catid,active);
mysql> SELECT COUNT(*) FROM content_page_categories WHERE catid = 1 and active = 1;
Выполняется в 0.037 - 0.044 секунд. Так что это лучше, если вы можете поддерживать избыточный столбец active
в синхронизации со значением в таблице content_en
.
@Quassnoi решение, сводная таблица
Я попробовал решение, предложенное @Quassnoi, для поддержания таблицы с предварительно вычисленными значениями для каждого кота и активного. В таблице должно быть очень мало строк, и поиск нужных вам счетчиков - это поиск первичных ключей и не требует JOINs.
mysql> CREATE TABLE page_active_category (
active INT NOT NULL,
catid INT NOT NULL,
cnt BIGINT NOT NULL,
PRIMARY KEY (active, catid)
) ENGINE=InnoDB;
mysql> INSERT INTO page_active_category
SELECT e.active, c.catid, COUNT(*)
FROM content_en AS e
JOIN content_page_categories AS c ON c.itemid = e.id
GROUP BY e.active, c.catid
mysql> SELECT cnt FROM page_active_category WHERE active = 1 AND catid = 1
Выполняется в 0.0007 - 0.0017 секунд. Таким образом, это лучшее решение на порядок, если вы можете поддерживать таблицу с подсчетом агрегатов.
Вы можете видеть из этого, что различные типы денормализации (включая сводную таблицу) являются чрезвычайно мощным инструментом для производительности, хотя и имеют недостатки, поскольку сохранение избыточных данных может быть неудобным и делает ваше приложение более сложным.
Ответ 3
Проблема заключается в "активном" столбце в content_en. Очевидно, что если вам просто нужно знать, сколько записей контента было связано с определенной категорией (активной или нет), все, что вам нужно сделать, это:
SELECT count(1)
FROM content_page_categories
WHERE catid = 1;
Чтобы присоединиться к каждой записи content_en, просто прочитать "активный" флаг - это действительно то, что замедляет этот запрос.
Я рекомендую добавить "active" в content_page_categories и сделать его копией связанного значения в content_en... вы можете обновить этот столбец с помощью триггеров или кода. Затем вы можете изменить индекс комбо:
KEY combo (catid,active,itemid)
и перепишите свой запрос на:
SELECT count(1)
FROM content_page_categories USE INDEX (combo)
WHERE catid = 1 AND active = 1;
Кроме того, вам может быть намного лучше использовать таблицы InnoDB вместо MyISAM. Просто не забудьте настроить настройки InnoDB: http://www.mysqlperformanceblog.com/2007/11/01/innodb-performance-optimization-basics/
Ответ 4
Для меня с вашими данными в качестве настройки, я получал запрос на соединение в ~ 50 раз больше, чем просто выбор из content_page_categories.
Мне удалось добиться производительности примерно в 10 раз медленнее, чем просто выбрать из таблицы категорий, выполнив следующие данные:
Я использовал straight_join
SELECT straight_join count(*)
FROM content_en
INNER JOIN content_page_categories use index (combo)
ON (id = itemid)
WHERE catid = 1 AND active = 1 ;
и следующая структура таблицы (слегка измененная):
CREATE TABLE `content_en` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(100) DEFAULT NULL,
`uid` int(11) DEFAULT NULL,
`date_added` int(11) DEFAULT NULL,
`date_modified` int(11) DEFAULT NULL,
`active` tinyint(1) DEFAULT NULL,
`comment_count` int(6) DEFAULT NULL,
`orderby` tinyint(4) DEFAULT NULL,
`settings` text,
`permalink` varchar(255) DEFAULT NULL,
`code` varchar(3) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`),
KEY `test_con_1` (`active`) USING HASH,
KEY `combo` (`id`,`active`) USING HASH
ENGINE=MyISAM AUTO_INCREMENT=127126 DEFAULT CHARSET=utf8
и
CREATE TABLE `content_page_categories` (
`catid` int(11) unsigned NOT NULL,
`itemid` int(10) unsigned NOT NULL,
`main` tinyint(1) DEFAULT NULL,
KEY `itemid` (`itemid`),
KEY `catid` (`catid`),
KEY `test_cat_1` (`catid`) USING HASH,
KEY `test_cat_2` (`itemid`) USING HASH,
KEY `combo` (`itemid`,`catid`) USING HASH
ENGINE=MyISAM DEFAULT CHARSET=utf8
Чтобы достичь лучшего, я думаю, вам понадобится представление, сплющенная структура или другой тип поискового поля (как в триггере для заполнения строки в другой таблице, как обсуждается другим плакатом).
EDIT:
Я должен также указать на этот достойный пост о том, почему и когда нужно быть осторожным с Straight_Join
:
Когда использовать STRAIGHT_JOIN с MySQL
Если вы используете его, используйте его ответственно!
Ответ 5
чтобы ускорить подсчет соединений mysql, используйте подзапросы.
Например, получение городов с помощью placeCount
стол города
название титула......
место стол
id city_id title.....
SELECT city.title,subq.count as placeCount
FROM city
left join (
select city_id,count(*) as count from place
group by city_id
) subq
on city.id=subq.city_id