优化 MySQL 查询以避免扫描大量行
Posted
技术标签:
【中文标题】优化 MySQL 查询以避免扫描大量行【英文标题】:Optimizing MySQL query to avoid scanning a lot of rows 【发布时间】:2011-08-15 19:30:52 【问题描述】:我正在运行一个使用类似于下表的应用程序。文章有一个表,标签有另一个表。我想通过文章 ID 获取特定标签订单的最新 30 篇文章。例如“acer”,下面的查询将完成这项工作,但它没有正确索引,因为如果有很多与特定标签相关的文章,它将扫描很多行。如何在不扫描大量行的情况下运行查询以获得相同的结果?
EXPLAIN SELECT title
FROM tag, article
WHERE tag = 'acer'
AND tag.article_id = article.id
ORDER BY tag.article_id DESC
LIMIT 0 , 30
输出
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE tag ref tag tag 92 const 220439 Using where; Using index
1 SIMPLE article eq_ref PRIMARY PRIMARY 4 testdb.tag.article_id 1
以下是表格和示例数据:
CREATE TABLE `article` (
`id` int(11) NOT NULL auto_increment,
`title` varchar(60) NOT NULL,
`time_stamp` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1000001 ;
--
-- Dumping data for table `article`
--
INSERT INTO `article` VALUES (1, 'Saudi Apple type D', 1313390211);
INSERT INTO `article` VALUES (2, 'Japan Apple type A', 1313420771);
INSERT INTO `article` VALUES (3, 'UAE Samsung type B', 1313423082);
INSERT INTO `article` VALUES (4, 'UAE Apple type H', 1313417337);
INSERT INTO `article` VALUES (5, 'Japan Samsung type D', 1313398875);
INSERT INTO `article` VALUES (6, 'UK Acer type B', 1313387888);
INSERT INTO `article` VALUES (7, 'Saudi Sony type D', 1313429416);
INSERT INTO `article` VALUES (8, 'UK Apple type B', 1313394549);
INSERT INTO `article` VALUES (9, 'Japan HP type A', 1313427730);
INSERT INTO `article` VALUES (10, 'Japan Acer type C', 1313400046);
CREATE TABLE `tag` (
`tag` varchar(30) NOT NULL,
`article_id` int(11) NOT NULL,
UNIQUE KEY `tag` (`tag`,`article_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
--
-- Dumping data for table `tag`
--
INSERT INTO `tag` VALUES ('Samsung', 1);
INSERT INTO `tag` VALUES ('Acer', 2);
INSERT INTO `tag` VALUES ('Sony', 3);
INSERT INTO `tag` VALUES ('Apple', 4);
INSERT INTO `tag` VALUES ('Acer', 5);
INSERT INTO `tag` VALUES ('HP', 6);
INSERT INTO `tag` VALUES ('Acer', 7);
INSERT INTO `tag` VALUES ('Sony', 7);
INSERT INTO `tag` VALUES ('Acer', 7);
INSERT INTO `tag` VALUES ('Samsung', 9);
【问题讨论】:
只是想知道,但是为什么您按 tag.article_id 而不是 article.id 排序? 您是否能够创建新表或修改架构? @Gerry 这不是必须的,它们都是一样的。也可以按time_stamp排序。 @Sean 是的,我可以更改架构。 @Gerry 实际上,查询已经没有扫描很多行,但是 EXPLAIN 忽略了 LIMIT ,因此它显示了很多行检查。请阅读下面的 Quassnoi 先生评论以获取更多信息。非常感谢 【参考方案1】:是什么让您认为查询会检查大量行?
查询将使用tag (tag, article_id)
上的UNIQUE
索引精确扫描30
记录,将文章连接到PRIMARY KEY
上的每条记录并停止。
这正是你的计划所说的。
我刚刚制作了这个测试脚本:
CREATE TABLE `article` (
`id` int(11) NOT NULL auto_increment,
`title` varchar(60) NOT NULL,
`time_stamp` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1000001 ;
CREATE TABLE `tag` (
`tag` varchar(30) NOT NULL,
`article_id` int(11) NOT NULL,
UNIQUE KEY `tag` (`tag`,`article_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT
INTO article
SELECT id, CONCAT('Article ', id), UNIX_TIMESTAMP('2011-08-17' - INTERVAL id SECOND)
FROM t_source;
INSERT
INTO tag
SELECT CASE fld WHEN 1 THEN CONCAT('tag', (id - 1) div 10 + 1) ELSE tag END AS tag, id
FROM (
SELECT tag,
id,
FIELD(tag, 'Other', 'Acer', 'Sony', 'HP', 'Dell') AS fld,
RAND(20110817) AS rnd
FROM (
SELECT 'Other' AS tag
UNION ALL
SELECT 'Acer' AS tag
UNION ALL
SELECT 'Sony' AS tag
UNION ALL
SELECT 'HP' AS tag
UNION ALL
SELECT 'Dell' AS tag
) t
JOIN t_source
) q
WHERE POWER(3, -fld) > rnd;
,其中t_source
是一个包含1M
记录的表,然后运行您的查询:
SELECT *
FROM tag t
JOIN article a
ON a.id = t.article_id
WHERE t.tag = 'acer'
ORDER BY
t.article_id DESC
LIMIT 30;
一瞬间。
【讨论】:
我认为它扫描了很多行,因为解释显示扫描了 220439 行。这是正确的吗? @usef_ksa:这是对满足WHERE
条件的行数的粗略估计。它不依赖于LIMIT
:只需尝试将不同的值放入LIMIT
子句中,EXPLAIN
中的值不会改变。实际上,查询将从tag
获取并检查30
记录,并将article
中的30
记录连接到它们。【参考方案2】:
尝试 ANSI 连接语法:
SELECT title
FROM tag t
INNER JOIN article a
ON t.article_id = a.id
WHERE
t.tag = 'acer'
ORDER BY
tag.article_id DESC
LIMIT 0 , 30
然后在 tag.tag 上放置一个索引。假设您对该表有足够的选择性,并且 article.id 是一个主键,那应该是相当活泼的。
【讨论】:
还是一样,还是一样的问题 标签中有“acer”标签的百分比是多少?如果超过 5% 左右,您将进行扫描。 --实际上,刚刚读取了您正在插入的数据,您将无法避免扫描。索引查找需要更高的选择性。【参考方案3】:我建议修改存储引擎和架构以使用外键。
CREATE TABLE `article` (
`id` int(11) NOT NULL auto_increment,
`title` varchar(60) NOT NULL,
`time_stamp` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1000001 ;
CREATE TABLE `tag` (
`id` int(11) NOT NULL auto_increment,
`tag` varchar(30) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `article_tag` (
`id` int(11) NOT NULL auto_increment,
`article_id` int(11) NOT NULL,
`tag_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`article_id`) REFERENCES article(id),
FOREIGN KEY (`tag_id`) REFERENCES tag(id)
) ENGINE=Innodb;
这会导致这样的查询:
EXPLAIN
SELECT * FROM article
JOIN article_tag ON article.id = article_tag.id
JOIN tag ON article_tag.tag_id = tag.id
WHERE tag.tag="Acer";
+----+-------------+-------------+--------+----------------+---------+---------+-------------------------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+--------+----------------+---------+---------+-------------------------+------+-------------+
| 1 | SIMPLE | article_tag | ALL | PRIMARY,tag_id | NULL | NULL | NULL | 1 | |
| 1 | SIMPLE | tag | eq_ref | PRIMARY | PRIMARY | 4 | temp.article_tag.tag_id | 1 | Using where |
| 1 | SIMPLE | article | eq_ref | PRIMARY | PRIMARY | 4 | temp.article_tag.id | 1 | |
+----+-------------+-------------+--------+----------------+---------+---------+-------------------------+------+-------------+
3 rows in set (0.00 sec)
【讨论】:
我可以更改架构,但无法将引擎更改为 Innodb。我必须使用 MyISAM。 因为目前我对 MyISAM 比较熟悉,而且它还支持全文搜索,我在我的应用程序中使用了这个功能。我相信有一种方法可以用 MyISAM 解决这个问题。我认为更改表引擎需要停机和大量更改。 您确实意识到 mysql 在 MySQL 5.5.5 之后已经切换到 InnoDB 作为默认存储引擎 - 这是将近一年前的事了,对吧? 对此评论 +1。是的,我知道,InnoDB 似乎是未来“也许是当前”的不错选择。将来我将使用 InnoDB 测试我的应用程序。现在我必须使用 MyISAM 解决这个问题,因为我的网站现在流量很大。【参考方案4】:编辑:添加此索引
UNIQUE KEY tag (article_id,tag)
【讨论】:
虽然我不这样做对 MYSQL 内部结构了解不够,无法确定这一点。 我添加了索引但仍然是同样的问题。我强迫它使用新索引,但它正在扫描更多行。 你试过UNIQUE KEY标签(article_id,tag)吗?它现在扫描了多少行,Extra 的输出是什么?还在哪里用吗? 为了以防万一,还要更改 orderby。 我测试过了。实际上它必须使用 where 因为 index 的第一部分将对 id 进行排序,然后按标签排序。有些 id 有多个标签。因此,如果从查询中删除 tag = 'acer' ,它将仅使用索引。以上是关于优化 MySQL 查询以避免扫描大量行的主要内容,如果未能解决你的问题,请参考以下文章