【问题标题】:MySQL not using index when using IN with OR将 IN 与 OR 一起使用时 MySQL 不使用索引
【发布时间】:2021-09-04 06:08:07
【问题描述】:

背景

  • users 表有 2k 行
  • relationships 表有 150 万行
  • posts 表有 200 万行
  • 使用 mysql 版本 5.7.34

users 的结构:

CREATE TABLE `users` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `email` varchar(255) NOT NULL DEFAULT '',
  `first_name` varchar(255) NOT NULL DEFAULT '',
  `last_name` varchar(255) NOT NULL DEFAULT '',
  `password` varchar(255) NOT NULL DEFAULT '',
  `active` tinyint(1) NOT NULL,
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=3263 DEFAULT CHARSET=utf8

relationships 的结构:

CREATE TABLE `relationships` (
  `user_id` int(11) unsigned NOT NULL,
  `is_following_user_id` int(11) unsigned NOT NULL,
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY `user_id` (`user_id`,`is_following_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

posts 的结构:

CREATE TABLE `posts` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int(11) unsigned NOT NULL,
  `parent_post_id` int(11) DEFAULT NULL,
  `content` varchar(255) DEFAULT '',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  CONSTRAINT `users` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2412061 DEFAULT CHARSET=utf8

注意:用户 922 没有任何关系或帖子,因此查询会根据需要执行完整的索引和/或表扫描。


这个查询耗时 0.5ms:

# 0.5ms
select * from posts where user_id in (
    select id from users inner join relationships
        on users.id = relationships.is_following_user_id
        where relationships.user_id = 922
);

解释上述快速查询的输出:

这个查询需要 500 毫秒:

# 500ms
select * from posts where user_id in (
    select id from users inner join relationships
        on users.id = relationships.is_following_user_id
        where relationships.user_id = 922
    )
or user_id = 922;

解释上述慢查询的输出:

很明显,对于第二个查询,它识别出与第一个查询(users.user_id) 相同的索引,但在第二个查询中,根据解释输出,它特别避免使用它(key = NULL)

这个查询需要 2.3 秒:

# 2.3 seconds
select * from posts where user_id in (
    select id from users inner join relationships
        on users.id = relationships.is_following_user_id
        where relationships.user_id = 922
    union all
    select 922
);

解释上述超慢查询的输出:

问题:

  1. 为什么查询 #2 不像查询 #1 那样使用 users.user_id 索引?
  2. 为什么查询 #3 这么慢,而且还没有使用 users.user_id 索引?

【问题讨论】:

  • 为了完整起见,我建议尝试第四个查询,合并两个 select * from posts 的结果:一个带有 where 子查询检查,另一个用于 where user_id = 922 检查。
  • 除了你的问题,你为什么不把你的第一个查询简化为SELECT * FROM posts INNER JOIN relationships ON posts.user_id = relationships.is_following_user_id WHERE relationships.user_id = 922
  • 顺便说一句,(user_id,is_following_user_id) 是主要的
  • 底线是 MySQL 的优化器在处理 IN ( SELECT ... ) 方面做得很糟糕;躲开它!几乎总是将重新表述为JOIN 有帮助。 (有时EXISTS( SELECT ... ) 可以很好地工作。)

标签: mysql sql performance indexing subquery


【解决方案1】:

一般来说,您的问题的答案是查询优化器会尽力而为,但只寻找有限数量的特殊情况,这些情况通常不包括合并不同来源的键值,而且通常 确实包括尝试将子查询的一部分转换为连接,有时会损害效率。

你可以强迫它做你想做的事:

select straight_join p.*
from (
    select id from users inner join relationships on users.id = relationships.is_following_user_id where relationships.user_id = 922
    union all
    select 922
) ids
join posts p on p.user_id=ids.id

【讨论】:

  • You can likely force it ...但通常强制使用索引并不是最佳做法(在大多数情况下)。
  • @TimBiegeleisen 带着这种情绪,你可能会更好地使用 postgres。当我编写 sql 时,我总是有一个查询计划,平衡我所知道的可能的最佳和最坏情况,并尽我所能强制执行。是的,有时需要在版本升级时重复此操作
  • @ysth 谢谢,您的查询具有预期的性能。我感到震惊的是,在我看来,在where 子句中非常明确使用索引会导致查询优化器强行避免使用该索引——甚至force index (user_id) 很多人会立即仔细审查并不足以改变主意。
【解决方案2】:

ORIN ( SELECT ... ) 很少得到很好的优化。

扁平化结构(SELECTs 的嵌套更少)似乎有所帮助。

( SELECT p.*
    FROM relationships AS r
    JOIN users AS u  ON u.id = r.is_following_user_id
    JOIN posts AS p  ON p.user_id = u.id
    WHERE r.user_id = 922
) UNION ALL   -- see note
( SELECT *
    FROM posts
    WHERE user_id = 922
)

注意:UNION ALL 可能比UNION DISTINCT 快。假设没有用户关注自己,使用ALL 是“正确的”。

(我已经按照优化器决定使用的顺序列出了JOIN 中的表格。按照ysth,我喜欢“像优化器一样思考”。)

剖析查询,着眼于 MySQL 的优化器...

  • relationships 将首先被查看——因为它是在 WHERE 中找到的唯一东西。 INDEX(user_id, ...) 让它运作良好。
  • 另一个提升是INDEX(user_id, is_following_user_id) 正在“覆盖”。
  • 下一个表将是usersPRIMARY KEY(id)
  • 最后(对于第一个SELECT),posts 通过INDEX(user_id)
  • 另一个选择也使用INDEX(user_id)。 (但是,在许多UNIONs 中,可能会使用不同的索引。这就是为什么UNION 通常是对OR 的显着优化。)

【讨论】:

    【解决方案3】:

    我什至会将预查询(来自查询)更进一步。完全删除用户加入。没必要。关系表有一个 ID,表示正在关注的人以及执行以下操作的用户。这可以简化为

    select straight_join 
          p.*
       from 
          posts p 
             JOIN ( select r.is_following_user_id
                       from relationships r
                       where r.user_id = 922
                    union all
                    select 922 ) ids
             on p.user_id = ids.is_following_user_id
    

    由于您正在过滤关系 USER_ID = 922 的内部查询,因此“is_following_user_id”是您想要的另一个人。无需加入用户表即可获取“id”列。它仍然是 ID 列,只是名称更长。工会按预期拉动 922。所以现在你所有的 ID 都被简化了,没有 JOIN。这些 ID 的外部结果拉取。

    【讨论】:

      猜你喜欢
      • 2018-04-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-28
      • 1970-01-01
      • 1970-01-01
      • 2010-09-07
      • 2020-06-22
      相关资源
      最近更新 更多