【问题标题】:Mysql: inner join on primary key for 2 IDs gives "Range checked for each record"Mysql:2个ID的主键内部连接给出“检查每条记录的范围”
【发布时间】:2014-12-30 10:55:59
【问题描述】:

当对具有 2 个值的 PRIMARY 键(使用 IN 或 OR 构造)执行 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)

但如果我这样做,它工作正常,我只得到 1 行解析:

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;

【问题讨论】:

  • 内连接是正确的连接方法吗?您不应该也使用 FULL 吗?
  • 嗯,我看到您只有主索引设置了 ID 号。尝试为 sender_id 和 receiver_id 添加索引,这可能有助于配对。
  • 好吧,我做到了,它对 IN 结构没有帮助,但它适用于 OR 结构!我认为仅在正在搜索的列(user.id)上设置索引,而不是在(已经选择的)要测试的值来自(task.id_user)的列上。您能否发布一个实际答案(我将被允许接受)并向我(和其他人)解释为什么索引也应该设置在参考列上,而不仅仅是搜索的列?谢谢大卫!
  • 等等,我说得太快了。它仍然不起作用。它只修复了我做的一个单一的用例:u.id = t.id_user OR u.id = t.id_user(这毫无意义但仍然导致问题并使查询更加简单)。很抱歉,它不起作用。还有什么想法吗?我确定我不是唯一一个尝试使用 OR 将一列与两列进行比较的人。我很惊讶我没有早点遇到。
  • 嗨。你是什​​么意思,“它工作正常”?或“它不起作用”。此外,您实际上并没有说出您的期望。尽管你可能认为这很明显。请阅读minimal reproducible example 并采取行动。在这里,您没有示例输入、输出和所需的输出。 “我只解析了 1 行”是什么意思?并给出一个完整的例子(代码和数据)来展示你的问题。在给出您的规范时,还有什么其他 declarable 唯一和 FK 列集和非空列? PS“参考”和“搜索”列是什么意思?

标签: mysql sql indexing explain


【解决方案1】:

一般来说,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 之后。最终,这可能是对性能的洗礼。取决于需要检查多少行数据,以及生成多少行作为结果。

【讨论】:

  • 我无法检查但相信使用“UNION ALL”将有助于避免临时表(可能会导致重复,但我认为向自己发送消息的情况非常罕见)
  • @skyboyer:MySQL 5.7.3 及更高版本在使用UNION ALL 时可以消除临时表。请参阅dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-3.html 的具体说明。早期版本的 MySQL 没有这种优化。
【解决方案2】:

这个问题在其他(我认为是所有的)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" 

【讨论】:

  • 在表中添加了几十行之后,再次尝试EXPLAIN。您可能会发现它不是u 的全表扫描开始。但是,&lt;derived2&gt; 中仍然隐含了一个 tmp 表。此外,还对其进行全表扫描(好的,只有 2 行)以建立索引(&lt;auto_key0&gt;)。
  • @RickJames 我用 6 个用户和 50 条消息进行了测试,执行计划没有改变。此查询类似于@BillKarwin 之一,但它仅对 u 进行 1 次全表扫描,并且仅进行 1 次嵌套循环 (JOIN) 而不是 2 和 2。从 MySQL 5.7 开始,为派生表构建合并索引 &lt;auto_key0&gt; 应该有良好的表现。我的意思是,加入一个联合应该比两个联合的联合更好。
  • 哇!问题想要来自test_messagetest_user 的所有列;此处的查询仅提供来自test_user 的列。所以,我认为这不是一个有效的答案。
  • 问题是“在这种情况下,如何才能对每条记录进行'范围检查'?”我的回答给出了问题的描述和避免它的替代解决方案。如果你不喜欢它,别担心..我会活下来的..
  • '为每条记录检查的范围'太神秘了,我不知道它是什么意思。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-03-02
  • 1970-01-01
  • 2015-07-25
  • 1970-01-01
  • 2023-01-15
  • 2017-05-26
  • 2012-10-26
相关资源
最近更新 更多