MySQL 不使用带有 WHERE IN 子句的索引?

Posted

技术标签:

【中文标题】MySQL 不使用带有 WHERE IN 子句的索引?【英文标题】:MySQL not using indexes with WHERE IN clause? 【发布时间】:2010-10-09 20:09:11 【问题描述】:

我正在尝试优化我的 Rails 应用程序中的一些数据库查询,但其中有几个让我很困惑。他们都在WHERE 子句中使用IN,并且都在进行全表扫描,即使似乎有适当的索引。

例如:

SELECT `user_metrics`.* FROM `user_metrics` WHERE (`user_metrics`.user_id IN (N,N,N,N,N,N,N,N,N,N,N,N))

执行全表扫描,EXPLAIN 说:

select_type: simple
type: all
extra: using where
possible_keys: index_user_metrics_on_user_id  (which is an index on the user_id column)
key: (none)
key_length: (none)
ref: (none)
rows: 208

在使用IN 语句时是否不使用索引,还是我需要做一些不同的事情?这里的查询是由 Rails 生成的,所以我可以重新审视我的关系是如何定义的,但我想我应该先从数据库级别的潜在修复开始。

【问题讨论】:

你在 user_metrics 表中的 user_id 上有索引吗? 它在问题中这样说:“possible_keys: index_user_metrics_on_user_id(这是 user_id 列上的索引)” 你的 N 是什么意思?它们是字面常量、列还是变量?这很重要。 抱歉,这些是从我的 query_reviewer 插件的输出中粘贴的。实际的查询在那里有整数 - 即。在(25、26、27) @blackant:你在你的表上运行分析了吗? 【参考方案1】:

见How mysql Uses Indexes。

在您向user_metrics 表添加额外的 2000 左右行后,还要验证 MySQL 是否仍然执行full table scan。在小表中,按索引访问实际上比表扫描更昂贵(I/O 方面),MySQL 的优化器可能会考虑到这一点。

与我之前的帖子相反,事实证明 MySQL 也是 using a cost-based optimizer,这是一个非常好的消息 - 也就是说,只要您在相信时至少运行一次 ANALYZE您数据库中的数据量代表未来的日常使用情况。

在处理基于成本的优化器(Oracle、Postgres 等)时,您需要确保定期在您的各种表上运行ANALYZE,因为它们的大小增加了 10-15% 以上。 (默认情况下,Postgres 会自动为您执行此操作,而其他 RDBMS 会将此责任留给 DBA,即您。)通过统计分析,ANALYZE 将帮助优化器更好地了解多少 I/O(以及当在各种候选执行计划之间进行选择时,将涉及其他相关资源,例如 CPU,例如排序所需的)。未能运行 ANALYZE 可能会导致非常糟糕的,有时甚至是灾难性的计划决策(例如,由于JOINs 上的糟糕的嵌套循环,需要毫秒查询,有时需要数小时。)

如果运行ANALYZE 后性能仍然不令人满意,那么您通常可以通过使用提示来解决该问题,例如FORCE INDEX,而在其他情况下,您可能会偶然发现 MySQL 错误(例如,这个 older one,如果您使用 Rails 的 nested_set,它可能会咬住您)。

现在,因为您在 Rails 应用程序中,发出带有提示的自定义查询而不是继续使用 ActiveRecord 会很麻烦(并且违背了ActiveRecord 的目的) - 生成的。

我曾提到,在我们的 Rails 应用程序中所有SELECT 切换到 Postgres 后查询下降到 100 毫秒以下,而 ActiveRecord 生成的一些复杂连接有时会花费 15 秒或MySQL 5.1 使用更多,因为带有内部表扫描的嵌套循环,即使索引可用。没有优化器是完美的,您应该了解这些选项。除了查询计划优化之外,需要注意的其他潜在性能问题是锁定。不过,这超出了您的问题范围。

【讨论】:

谢谢弗拉德。我希望通过我们当前的设置来解决这个问题,但我很高兴听到您使用 Postgres 取得的成功。 嗨,blackant,您在这个 MySQL 问题上取得了进展吗? 这是一个非常好的评论。一条评论回答了很多问题【参考方案2】:

尝试强制这个索引:

SELECT `user_metrics`.*
FROM `user_metrics` FORCE INDEX (index_user_metrics_on_user_id)
WHERE (`user_metrics`.user_id IN (N,N,N,N,N,N,N,N,N,N,N,N))

我刚刚检查过,它确实对完全相同的查询使用了索引:

EXPLAIN EXTENDED
SELECT * FROM tests WHERE (test IN ('test 1', 'test 2', 'test 3', 'test 4', 'test 5', 'test 6', 'test 7', 'test 8', 'test 9'))

1, 'SIMPLE', 'tests', 'range', 'ix_test', 'ix_test', '602', '', 9, 100.00, 'Using where'

【讨论】:

在 Rails 中实现特别痛苦 是的,这似乎迫使它使用索引。就像 Vlad 说的那样,在 Rails 中做起来很痛苦。 @blackant,您是否对表格进行了分析?仍然得到相同的解释计划? 由于 Rails 使用了 Arel,所以非常简单,例如: User.from("users IGNORE INDEX (index_users_on_status_id)").where(:status_id => 1).where("... ").全部【参考方案3】:

有时 MySQL 不使用索引,即使索引可用。发生这种情况的一种情况是优化器估计使用索引将需要 MySQL 访问表中很大比例的行。 (在这种情况下,表扫描可能会快得多,因为它需要的搜索次数更少。)

多少百分比的行匹配您的 IN 子句?

【讨论】:

我最初的测试是在一个只有约 200 行的表上进行的,因此百分比相对较高。但是,我添加了 5000 多行进行测试,它仍然执行全表扫描。现在的百分比将非常微不足道。 根据我的经验,截止值通常在 10% 到 30% 之间。【参考方案4】:

我知道我参加聚会要迟到了。但希望我能帮助其他有类似问题的人。

最近,我遇到了同样的问题。然后我决定使用 self-join-thing 来解决我的问题。 问题不在于 MySQL。问题是我们。子查询的返回类型与我们的表不同。所以我们必须将子查询的类型转换为选择列的类型。 下面是示例代码:

select `user_metrics`.* 
from `user_metrics` um 
join (select `user_metrics`.`user_id` in (N, N, N, N) ) as temp 
on um.`user_id` = temp.`user_id`

或者我自己的代码:

旧:(不使用索引:~4s)

SELECT 
    `jxm_character`.*
FROM
    jxm_character
WHERE
    information_date IN (SELECT DISTINCT
            (information_date)
        FROM
            jxm_character
        WHERE
            information_date >= DATE_SUB('2016-12-2', INTERVAL 7 DAY))
        AND `jxm_character`.`ranking_type` = 1
        AND `jxm_character`.`character_id` = 3146089;

新:(使用指数:~0.02s)

SELECT 
    *
FROM
    jxm_character jc
        JOIN
    (SELECT DISTINCT
        (information_date)
    FROM
        jxm_character
    WHERE
        information_date >= DATE_SUB('2016-12-2', INTERVAL 7 DAY)) AS temp 
        ON jc.information_date = STR_TO_DATE(temp.information_date, '%Y-%m-%d')
        AND jc.ranking_type = 1
        AND jc.character_id = 3146089;

jxm_character:

记录:~3.5M PK:jxm_character(information_date,ranking_type,character_id)

SHOW VARIABLES LIKE '%version%';

'protocol_version', '10'
'version', '5.1.69-log'
'version_comment', 'Source distribution'

最后一点:确保您了解 MySQL 索引最左规则。

P/s:对不起,我的英语不好。我发布我的代码(当然是生产)以清除我的解决方案:D。

【讨论】:

IN ( SELECT ... ) 因优化不佳而臭名昭著。你做了“正确的事”把它变成了JOIN 你真的拯救了我的一天 :D 。我的表大约有 100 万行,并且开始变得非常慢,这真的拯救了我的一天!【参考方案5】:

如果去掉 where 子句周围多余的括号,效果会更好吗?

虽然可能只是因为您只有 200 左右的行,但它决定表扫描会更快。尝试使用包含更多记录的表。

【讨论】:

额外的括号似乎无关紧要。此外,小数据集似乎无关紧要 - 我添加了额外的 5000 条记录,并且仍然扫描它们。

以上是关于MySQL 不使用带有 WHERE IN 子句的索引?的主要内容,如果未能解决你的问题,请参考以下文章

在 where 子句中使用解码,并带有“in”

强制 MySQL 从 WHERE IN 子句返回重复项而不使用 JOIN/UNION?

mysql IN子句不使用可能的键

SQL:如何在带有“NOT IN”条件的“Where”子句中使用“and”和“or”

带有 WHERE 子句和 INNER JOIN 的 MySQL 更新查询不起作用

MySQL Select 语句,WHERE 'IN' 子句