用OR语句驯服MySQL查询性能的问题

时间:2009-09-24 14:03:00

标签: sql mysql performance indexing

[警告:提前发帖!]

我现在已经敲了很长时间,但却无法找到共同点。我找到了一个解决方法,最后看到了,但我内心的禅还不满意。

我有一个包含论坛消息的主表(来自Phorum),简化后看起来像这样(暂时忽略anon_user_id,我会稍后再说):

CREATE TABLE `test_msg` (
  `message_id` int(10) unsigned NOT NULL auto_increment,
  `status` tinyint(4) NOT NULL default '2',
  `user_id` int(10) unsigned NOT NULL default '0',
  `datestamp` int(10) unsigned NOT NULL default '0',
  `anon_user_id` int(10) unsigned NOT NULL default '0',
  PRIMARY KEY  (`message_id`)
);

软件可以对消息进行匿名处理,在这种情况下,user_id设置为0。该软件还允许发布我们认可的完整匿名消息。在我们的例子中,我们仍然需要知道哪个用户发布了一条消息,所以通过Phorum提供的钩子系统,我们有第二个表格,我们会相应更新:

CREATE TABLE `test_anon` (
  `message_id` bigint(20) unsigned NOT NULL,
  `user_id` bigint(20) unsigned NOT NULL,
  KEY `fk_user_id` (`user_id`),
  KEY `fk_message_id` (`message_id`)
);

对于个人资料中的视图,我需要获取用户的消息列表,无论他们是否已经被他/她

用户本身始终有权查看他匿名撰写或稍后匿名撰写的邮件。

如果匿名化user_id设置为0,我们不能简单地使用WHERE;我们需要加入我们自己的第二张桌子。将上面的内容表达为SQL就像这样(status = 2是必需的,其他状态意味着帖子被隐藏,等待批准等):

SELECT * FROM  test_msg AS m
LEFT JOIN test_anon ON test_anon.message_id = m.message_id
WHERE (test_anon.user_id = 20 OR m.user_id = 20)
AND m.status = 2
ORDER BY m.datestamp DESC
LIMIT 0,10

每当查询缓存为空时,此查询本身需要几秒钟,当前为4秒。当多个用户发出查询并且查询缓存为空(这恰好发生;人们发布消息和缓存的查询无效)时,情况会变得更糟;我们在内部测试阶段遇到过,报告显示系统有时会变慢。由于并发性,我们已经看到查询需要30到60秒。我不想开始想象当我们扩展用户群时会发生什么......

现在不像我没有对瓶颈进行任何分析。

我尝试重写WHERE子句,添加indice并删除它们就像地狱一样。

当我发现当不使用任何索引时,查询会在某些条件下快速执行照明。不使用索引,查询如下所示:

SELECT * FROM  test_msg AS m USE INDEX()
LEFT JOIN test_anon ON test_anon.message_id = m.message_id
WHERE (test_anon.user_id = 20 OR m.user_id = 20)
AND m.status = 2
ORDER BY m.datestamp DESC
LIMIT 0,10

现在出现特定条件:LIMIT将结果限制为10行。假设我的完整结果为n = 26。使用LIMIT 0,10LIMIT 16,0需要零秒(某些值<0.01s):结果总是为10行。

LIMIT 17,10开始,结果将只有9行。从此时开始,查询将再次开始四秒。适用于结果集小于LIMIT限制的最大行数的所有结果。刺激性!

回到第一个CREATE TABLE语句,我也进行了没有LEFT JOIN的测试;我们假设匿名消息为user_id=0anon_user_id=<the previous user_id>,换句话说,完全绕过第二个表:

SELECT * FROM test_msg
WHERE status = 2 AND (user_id = 20 OR anon_user_id = 20)
ORDER BY m.datestamp DESC
LIMIT 20,10

结果:无关紧要。性能仍然在4或5秒内;强制不使用USE INDEX()的索引不会加速此查询。

这是我现在真的很困惑。索引将始终仅用于status列,OR阻止使用其他索引,这也是MySQL文档在这方面告诉我的内容。

我尝试过的另一种解决方案:不要使用test_anon表仅涉及匿名消息,而只涉及所有消息。这允许我写这样的查询:

SELECT * FROM test_msg AS m, test_anon AS t
WHERE m.message_id = t.message_id
AND t.user_id = 20
AND m.status = 2
ORDER BY m.datestamp DESC
LIMIT 20,10

此查询总是给我即时结果(== <0.01秒),无论LIMIT等等。

是的,我找到了解决方案。我还没有将整个应用程序改写为模型。

但是我想更好地理解我观察到的行为背后的理性(特别是强迫没有索引加速查询)。在纸面上,原始方法没有任何问题。

有些数字(无论如何都不是那么大):

  • 〜一百万条消息
  • 消息表数据大小约为600MB
  • 消息表索引大小约为350MB
  • test_anon中的匿名邮件数量&lt;所有消息的3%
  • 来自注册用户的消息数量&lt;所有消息的25%

所有表格都是MyISAM;我尝试使用InnnoDB,但性能要差得多。

2 个答案:

答案 0 :(得分:1)

问题是你正在为整个表做一个连接。您需要告诉优化器您只需要加入两个用户ID:零和您想要的用户ID。像这样:

SELECT * FROM test_msg AS m
LEFT JOIN test_anon ON test_anon.message_id = m.message_id
WHERE (m.user_id = 20 OR m.user_id = 0)
AND (test_anon.user_id = 20 OR test_anon.user_id IS NULL)
AND m.status = 2
ORDER BY m.datestamp DESC
LIMIT 0,10

这会更好吗?

答案 1 :(得分:1)

您实际上在这里有两个不同的查询,可以作为单独的查询进行更好的处理。

要改进LIMIT,您需要使用LIMIT on LIMIT技术:

SELECT  *
FROM    (
        SELECT  *
        FROM    test_msg AS m
        WHERE   m.user_id = 20
                AND m.status = 2
        ORDER BY
                m.datestamp DESC
        LIMIT 20
        ) q1
UNION ALL
SELECT  *
        (
        SELECT  m.*
        FROM    test_msg m
        JOIN    test_anon a
        ON      a.message_id = m.message_id
        WHERE   a.user_id = 20
                AND m.user_id = 0
                AND m.status = 2
        ORDER BY
                m.datestamp DESC
        LIMIT 20
        ) q2
ORDER BY
        datestamp DESC
LIMIT 20

有关此解决方案的更多详细信息,请参阅我的博客中的此条目:

您需要为此创建两个复合索引才能快速工作:

test_msg (status, user_id, datestamp)
test_msg (status, user_id, message_id, datestamp)

然后,您需要在第二个查询中选择索引的用途:排序或过滤。

在您的查询中,索引不能用于两者,因为您要对message_id上的范围进行过滤。

有关更多解释,请参阅此文章:

用几句话说:

  • 如果此用户有批次的匿名消息,i。即消息很可能在索引开头的某处找到,然后索引应该用于排序。使用第一个索引。
  • 如果此用户有很少个匿名消息,i。即在索引开头的某处找到消息的可能性很小,那么索引应该用于过滤。使用第二个索引。

如果有可能重新设计表格,只需将另一列is_anonymous添加到表格test_msg

它将解决许多问题。