在使用2个值(使用IN或OR结构)的PRIMARY键上进行INNER JOIN时,在EXPLAIN SELECT中得到“检查每个记录的范围(索引映射:0x1)”
以下是查询:
SELECT *
FROM message AS m
INNER JOIN user AS u
ON u.id = m.sender_id OR u.id = m.receiver_id
在做解释时,它给了我:
+----+-------------+-------+------+---------------+------+---------+------+-------+-----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+-------+-----------------------------------------------+
| 1 | SIMPLE | u | ALL | PRIMARY | null | null | null | 75000 | Range checked for each record (index map: 0x1)|
+----+-------------+-------+------+---------------+------+---------+------+-------+-----------------------------------------------+
不可能......
如果我尝试这个,我会得到相同的结果:
SELECT *
FROM message AS m
INNER JOIN user AS u
ON u.id IN(m.sender_id, m.receiver_id)
但是,如果我这样做,它工作正常,我只解析了一行:
SELECT *
FROM message AS m
INNER JOIN user AS u
ON u.id = m.sender_id
这怎么可能?我正在加入具有相同类型值的主键。 (实际的查询是“有点”更复杂,但没有什么花哨,2个内连接,最后一个左连接)
它应该是2行,句号。
感谢您对此提出任何意见(进行了一些研究,但除了“请添加索引”之外没有找到任何有价值的内容,这显然不适用于此处)
编辑:是的,我尝试了USE INDEX声明,但仍然没有运气
编辑:这是一个非常简单的模式来重现MySQL的这种奇怪的行为:
CREATE TABLE test_user (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(30),
PRIMARY KEY (id)
);
CREATE TABLE test_message (
id INT NOT NULL AUTO_INCREMENT,
sender_id INT NOT NULL,
receiver_id INT NOT NULL,
PRIMARY KEY (id),
INDEX idx_sender (sender_id),
INDEX idx_receiver (receiver_id)
);
EXPLAIN SELECT *
FROM test_message AS m
INNER JOIN test_user AS u
ON u.id = m.sender_id OR u.id = m.receiver_id;
答案 0 :(得分:5)
通常,MySQL在查询中每个表引用只能使用一个索引(有一个index-merge算法,但这并不像你想象的那样频繁。)
您的连接条件在两个与索引列的比较之间有OR
,并且优化器无法选择在逐行检查表中的数据之前使用哪个更好。
常见的解决方法是在更简单的查询之间执行UNION
,而不是OR
条件。
mysql> EXPLAIN
SELECT * FROM test_message AS m
INNER JOIN test_user AS u ON u.id = m.sender_id
UNION
SELECT * FROM test_message AS m
INNER JOIN test_user AS u ON u.id = m.receiver_id;
+----+--------------+------------+--------+---------------+---------+---------+--------------------+------+-----------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------+------------+--------+---------------+---------+---------+--------------------+------+-----------------+
| 1 | PRIMARY | m | ALL | idx_sender | NULL | NULL | NULL | 1 | NULL |
| 1 | PRIMARY | u | eq_ref | PRIMARY | PRIMARY | 4 | test.m.sender_id | 1 | NULL |
| 2 | UNION | m | ALL | idx_receiver | NULL | NULL | NULL | 1 | NULL |
| 2 | UNION | u | eq_ref | PRIMARY | PRIMARY | 4 | test.m.receiver_id | 1 | NULL |
| NULL | UNION RESULT | <union1,2> | ALL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+----+--------------+------------+--------+---------------+---------+---------+--------------------+------+-----------------+
这确实在两个子查询中都使用了正确的索引查找,但之后必须使用临时表来完成UNION
。最终,它可能是性能的洗涤。取决于需要检查多少行数据,以及生成多少行。
答案 1 :(得分:1)
这个问题在其他(我认为全部)RDBMS中也是众所周知的,优化器每个连接只使用一个规则。
如果连接条件很复杂或者无法识别已知模式来解决它,则不会应用任何优化,它将用于全表扫描。
在你的情况下,主连接中的OR
条件似乎很简单,但事实并非如此,因为你要求一次检查两个不同列(而不是常量值)的每个用户ID。
要解决此问题,您必须在更多子查询中拆分连接条件,以便优化器可以为每个子查询使用更好的规则。
@Bill Karwin提出了通用的解决方案,它有助于理解这个问题。
解决此问题的一种(稍微)更好的方法是将联合上移一级并加入派生表:
EXPLAIN
SELECT *
FROM test_user AS u
INNER JOIN (
select id, sender_id as msg_id
from test_message
union all
select id, receiver_id
from test_message
) AS m
ON u.id = m.msg_id;
它不会使用TEMPORARY
表,只会在test_users
而不是两个
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY u NULL ALL PRIMARY NULL NULL NULL 1 100.00 NULL
1 PRIMARY <derived2> NULL ref <auto_key0> <auto_key0> 4 test.u.id 2 100.00 NULL
2 DERIVED test_message NULL index NULL idx_sender 4 NULL 1 100.00 "Using index"
3 UNION test_message NULL index NULL idx_receiver 4 NULL 1 100.00 "Using index"