MySQL 不使用带有 JOIN、WHERE 和 ORDER 的索引
Posted
技术标签:
【中文标题】MySQL 不使用带有 JOIN、WHERE 和 ORDER 的索引【英文标题】:MySQL not using index with JOIN, WHERE and ORDER 【发布时间】:2010-11-16 16:30:35 【问题描述】:我们有两个类似于简单标签记录结构的表,如下所示(实际上它要复杂得多,但这就是问题的本质):
tag (A.a) | recordId (A.b)
1 | 1
2 | 1
2 | 2
3 | 2
....
和
recordId (B.b) | recordData (B.c)
1 | 123
2 | 666
3 | 1246
问题是获取带有特定标签的有序记录。显而易见的方法是在 (PK)(A.a, A.b), (A.b), (PK)(B.b), (B.b,B.c) 上进行简单的连接和索引:
select A.a, A.b, B.c from A join B on A.b = B.b where a = 44 order by c;
但是,这会给文件排序带来令人不快的结果:
+----+-------------+-------+------+---------------+---------+---------+-----------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+---------+---------+-----------+------+----------------------------------------------+
| 1 | SIMPLE | A | ref | PRIMARY,b | PRIMARY | 4 | const | 94 | Using index; Using temporary; Using filesort |
| 1 | SIMPLE | B | ref | PRIMARY,b | b | 4 | booli.A.b | 1 | Using index |
+----+-------------+-------+------+---------------+---------+---------+-----------+------+----------------------------------------------+
使用巨大且极其冗余的“物化视图”,我们可以获得相当不错的性能,但这会以使业务逻辑复杂化为代价,这是我们希望避免的,特别是因为 A 和 B 表已经是 MV:s (并且对于其他查询是必需的,并且实际上使用 UNION 进行相同的查询)。
create temporary table C engine=innodb as (select A.a, A.b, B.c from A join B on A.b = B.b);
explain select a, b, c from C where a = 44 order by c;
使情况更加复杂的是我们在 B 表上有条件,例如范围过滤器。
select A.a, A.b, B.c from A join B on A.b = B.b where a = 44 AND B.c > 678 order by c;
但是如果文件排序问题消失,我们有信心可以处理这个问题。
有谁知道为什么上面代码块 3 中的简单连接不会使用索引进行排序,以及我们是否可以在不创建新 MV 的情况下以某种方式解决问题?
以下是我们用于测试的完整 SQL 列表。
DROP TABLE IF EXISTS A;
DROP TABLE IF EXISTS B;
DROP TABLE IF EXISTS C;
CREATE TEMPORARY TABLE A (a INT NOT NULL, b INT NOT NULL, PRIMARY KEY(a, b), INDEX idx_A_b (b)) ENGINE=INNODB;
CREATE TEMPORARY TABLE B (b INT NOT NULL, c INT NOT NULL, d VARCHAR(5000) NOT NULL DEFAULT '', PRIMARY KEY(b), INDEX idx_B_c (c), INDEX idx_B_b (b, c)) ENGINE=INNODB;
DELIMITER $$
CREATE PROCEDURE prc_filler(cnt INT)
BEGIN
DECLARE _cnt INT;
SET _cnt = 1;
WHILE _cnt <= cnt DO
INSERT IGNORE INTO A SELECT RAND()*100, RAND()*10000;
INSERT IGNORE INTO B SELECT RAND()*10000, RAND()*1000, '';
SET _cnt = _cnt + 1;
END WHILE;
END
$$
DELIMITER ;
START TRANSACTION;
CALL prc_filler(100000);
COMMIT;
DROP PROCEDURE prc_filler;
CREATE TEMPORARY TABLE C ENGINE=INNODB AS (SELECT A.a, A.b, B.c FROM A JOIN B ON A.b = B.b);
ALTER TABLE C ADD (PRIMARY KEY(a, b), INDEX idx_C_a_c (a, c));
EXPLAIN EXTENDED SELECT A.a, A.b, B.c FROM A JOIN B ON A.b = B.b WHERE A.a = 44;
EXPLAIN EXTENDED SELECT A.a, A.b, B.c FROM A JOIN B ON A.b = B.b WHERE 1 ORDER BY B.c;
EXPLAIN EXTENDED SELECT A.a, A.b, B.c FROM A JOIN B ON A.b = B.b where A.a = 44 ORDER BY B.c;
EXPLAIN EXTENDED SELECT a, b, c FROM C WHERE a = 44 ORDER BY c;
-- Added after Quassnois comments
EXPLAIN EXTENDED SELECT A.a, A.b, B.c FROM B FORCE INDEX (idx_B_c) JOIN A ON A.b = B.b WHERE A.a = 44 ORDER BY B.c;
EXPLAIN EXTENDED SELECT A.a, A.b, B.c FROM A JOIN B ON A.b = B.b WHERE A.a = 44 ORDER BY B.c LIMIT 10;
EXPLAIN EXTENDED SELECT A.a, A.b, B.c FROM B FORCE INDEX (idx_B_c) JOIN A ON A.b = B.b WHERE A.a = 44 ORDER BY B.c LIMIT 10;
【问题讨论】:
文件排序发生在您的 ORDER BY 子句中。B.c
是如何编入索引的?
@jason:我更新了帖子中的 SQL,使其更具可读性。现在应该清楚索引了。
【参考方案1】:
当我尝试使用您的脚本重现此查询时:
SELECT A.a, A.b, B.c
FROM A
JOIN B
ON A.b = B.b
WHERE a = 44
ORDER BY
c
,它在0.0043 seconds
中完成(立即),返回930
行并产生这个计划:
1, 'SIMPLE', 'A', 'ref', 'PRIMARY', 'PRIMARY', '4', 'const', 1610, 'Using index; Using temporary; Using filesort'
1, 'SIMPLE', 'B', 'eq_ref', 'PRIMARY', 'PRIMARY', '4', 'test.A.b', 1, ''
这样的查询效率很高。
对于这样的查询,您不能同时使用单个索引进行过滤和排序。
查看我博客中的这篇文章以获得更详细的解释:
Choosing index如果您希望您的查询返回很少的记录,您应该使用A
上的索引进行过滤,然后使用文件排序进行排序(就像上面的查询一样)。
如果你期望它返回很多记录(和LIMIT
它们),你需要使用索引进行排序然后过滤:
CREATE INDEX ix_a_b ON a (b);
CREATE INDEX ix_b_c ON b (c)
SELECT *
FROM B FORCE INDEX (ix_b_c)
JOIN A
ON A.b = B.b
ORDER BY
b.c
LIMIT 10;
1, 'SIMPLE', 'B', 'index', '', 'ix_b_c', '4', '', 2, 'Using index'
1, 'SIMPLE', 'A', 'ref', 'ix_a_b', 'ix_a_b', '4', 'test.B.b', 4, 'Using index'
【讨论】:
对于真实数据,记录表非常大(无论是宽度还是行数,都有很多 VARCHAR(255):s),因此临时表的成本更高,因为有很多更多数据要复制。在我们的生产数据库(8 核至强,所有内容都在内存中)上,查询大约需要 0.05-0.1 秒,而 MV 测试显示低于 0.01 秒。 我没有得到与您在上面打印的相同查询相同的查询计划。无论如何, ORDER 的变化并没有真正帮助我,确保它删除了文件排序,但我得到的结果顺序错误!此外,只需将原始查询中的 ORDER 更改为“B.b,B.c”即可删除文件排序,这表明(嗯,对我来说;))无需临时表/文件排序就可以做到这一点。 (有趣的是,我实际上是从你的博客中借来插入的SP) @Paso:抱歉,没有很好地理解你的任务。仅在b.c
上创建索引并更改 ORDER BY
条件。我现在会在帖子中更新它。
@Paso:我注意到 :) 很高兴听到您阅读我的博客。一点小提示:在过程中填充InnoDB
表时,始终在事务中进行,这样会更快。
@Paso:能否请您发布运行查询时的计划?【参考方案2】:
select A.a, A.b, B.c from A join B on A.b = B.b where a = 44 order by c;
如果为列设置别名,是否有帮助?示例:
SELECT
T1.a AS colA,
T2.b AS colB,
T2.c AS colC
FROM A AS T1
JOIN B AS T2
ON (T1.b = T2.b)
WHERE
T1.a = 44
ORDER BY colC;
我所做的唯一更改是:
我将连接条件放在括号中 连接条件和 where 条件基于表列 ORDER BY 条件基于结果表列 我为结果表列和查询表设置了别名,以(希望)在我使用其中一个或另一个时更清楚(并且对服务器更清楚。您忽略了在原始文件中的两个位置引用您的列查询)。我知道您的真实数据更复杂,但我假设您提供了一个简单版本的查询,因为问题就在那个简单的级别。
【讨论】:
恐怕不行,你的查询给出了完全相同的 EXPLAIN 结果。 您真的要加入这两个表吗?我的意思是,这两个表是否链接在一起,其中每一行都是基于查询的完整结果,或者更像是每一行都有两个表所需的数据?我问是因为如果这两个表实际上没有以需要连接的方式绑定在一起,那么您可能会考虑使用 UNION。使用 UNION,查询是完全独立的,因此不需要发生子查询或临时表或其他任何繁重的事情。 我不太明白。这些表是通过 A.b = B.b 连接的,我需要来自 B 的每个匹配条件的 A 的数据,UNION 在这里有什么帮助?为了完整性;不,我不需要所有数据,只需要来自 B 的数据。请参阅问题顶部的标签示例,它应该尽可能准确地解释所有内容。以上是关于MySQL 不使用带有 JOIN、WHERE 和 ORDER 的索引的主要内容,如果未能解决你的问题,请参考以下文章
mysql的inner join,left jion,right join,cross join以及on和where的区别
强制 MySQL 从 WHERE IN 子句返回重复项而不使用 JOIN/UNION?