如何优化这个简单的 JOIN+ORDER BY 查询?

Posted

技术标签:

【中文标题】如何优化这个简单的 JOIN+ORDER BY 查询?【英文标题】:How to optimize this simple JOIN+ORDER BY query? 【发布时间】:2011-10-23 12:41:05 【问题描述】:

我有两个mysql表:

/* Table users */
CREATE TABLE IF NOT EXISTS `users` (
  `Id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `DateRegistered` datetime NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/* Table statistics_user */
CREATE TABLE IF NOT EXISTS `statistics_user` (
  `UserId` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `Sent_Views` int(10) unsigned NOT NULL DEFAULT '0',
  `Sent_Winks` int(10) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`UserId`),
  CONSTRAINT `statistics_user_ibfk_1` FOREIGN KEY (`UserId`) REFERENCES `users` (`Id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

两个表都填充了 10.000 个随机行,用于使用以下过程进行测试:

DELIMITER //
CREATE DEFINER=`root`@`localhost` PROCEDURE `FillUsersStatistics`(IN `cnt` INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE dt DATE;
DECLARE Winks INT DEFAULT 1;
DECLARE Views INT DEFAULT 1;

WHILE (i<=cnt) DO
        SET dt = str_to_date(concat(floor(1 + rand() * (9-1)),'-',floor(1 + rand() * (28 -1)),'-','2011'),'%m-%d-%Y');

        INSERT INTO users (Id, DateRegistered) VALUES(i, dt);

        SET Winks = floor(1 + rand() * (30-1));
        SET Views = floor(1 + rand() * (30-1));
        INSERT INTO statistics_user (UserId, Sent_Winks, Sent_Views) VALUES (i, Winks, Views);

     SET i=i+1;
END WHILE;

END//
DELIMITER ;
CALL `FillUsersStatistics`(10000);

问题:

当我为此查询运行 EXPLAIN 时:

SELECT
t1.Id, (Sent_Views + Sent_Winks) / DATEDIFF(NOW(), t1.DateRegistered) as Score
FROM users t1
JOIN  statistics_user t2 ON t2.UserId = t1.Id
ORDER BY Score DESC

.. 我明白了:

Id  select_type table   type    possible_keys   key     key_len     ref             rows    extra
1   SIMPLE      t1      ALL     PRIMARY         (NULL)  (NULL)      (NULL)          10037   Using temporary; Using filesort
1   SIMPLE      t2      eq_ref  PRIMARY         PRIMARY 4           test2.t2.UserId 1   

当两个表的行数都超过 500K 时,上述查询会变得非常慢。我想这是因为'使用临时;在查询的解释中使用 filesort'。

如何优化上述查询,使其运行得更快?

【问题讨论】:

您正在根据无法索引的动态属性 (now())) 对整个结果进行排序。如果您可以在统计数据更新时计算分数并维护分数索引,那么您将有更好的机会。 只是一个想法:如果你不是 now(),而是使用一个非常长的未来时间(好像你会计算这个结果,比如说,在 2500 年),绝对分数会不同,但将保持相对顺序。因此,您可以维护一个反映您想要的排序的分数索引,并可能重新计算排序结果的真实分数。 真正的问题是:为什么要维护 两个 表,它们之间(有效地)1::1 关系? 【参考方案1】:

我很确定 ORDER BY 是要害你的,因为它无法正确编入索引。这是一个可行的解决方案,如果不是特别漂亮的话。

首先,假设您有一个名为Score 的列,用于存储用户的当前分数。每次用户的Sent_ViewsSent_Winks 更改时,修改Score 列以匹配。这可能通过触发器来完成(我对触发器的经验有限),或者绝对可以在更新Sent_ViewsSent_Winks 字段的相同代码中完成。此更改不需要知道 DATEDIFF 部分,因为它可以只除以 Sent_Views + Sent_Winks 的旧总和并乘以新总和。

现在您只需每天更改一次Score 列(如果您对用户注册的确切小时数不挑剔的话)。这可以通过 cron 作业运行的脚本来完成。

然后,只需索引 Score 列并选择离开!

注意:已编辑以删除不正确的第一次尝试。

【讨论】:

但是使用 to_days() 并没有给出正确的排序顺序。如果我们都有 1000 次眨眼和观看,但我昨天注册而你 100 天前注册,你会得到更高的分数,但应该相反 感谢您指出这一点......显然我的大脑没有正确连接。编辑了可能的修复。 我尝试了 Chris 的建议,但并没有解决我的问题。还有其他建议吗? 克里斯,感谢您的编辑,但您能否详细说明一下:“此更改不需要知道 DATEDIFF 部分,因为它可以除以 Sent_Views + Sent_Winks 和乘以新的。”。也许我错过了一些东西,但是如果不知道 DATEDIFF(NOW(), DateRegistered),如何更新分数?也许您的意思是稍后将使用 cron 作业重新计算分数,这将考虑 DATEDIFF(NOW(), DateRegistered)?? @user1009456 我的意思是,每当用户的 Views 或 Winks 发生变化时,您只需将现有 Score 乘以 (new_sum / old_sum)。分数的 DATEDIFF 部分仅每 24 小时计算一次。如果您需要更多说明,请告诉我。【参考方案2】:

我提供我的评论作为答案:

确定一个未来的日期,不要影响你的申请,比如 5000 年。在你的分数计算中用这个未来的日期替换当前日期。分数计算现在对于所有意图和目的都是绝对的,并且可以在更新眨眼和视图时计算(通过存储的过程或触发(mysql 有触发器吗?))。

score 列添加到您的statistics_user 表以存储计算的分数并在其上定义索引。

您的 SQL 可以重写为:

SELECT
   UserId, score  
FROM
  statistics_user 
ORDER BY score DESC

如果你需要真正的分数,它很容易用一个常数乘法计算,如果它干扰了 mysql 索引选择,可以在之后完成。

【讨论】:

【参考方案3】:

您不应该在用户中为 DateRegistered 编制索引吗?

【讨论】:

【参考方案4】:

您应该尝试内连接,而不是笛卡尔积,接下来您可以做的就是根据 date_registered 进行分区。

【讨论】:

以上是关于如何优化这个简单的 JOIN+ORDER BY 查询?的主要内容,如果未能解决你的问题,请参考以下文章

记一次join + order by 的sql优化

记一次join + order by 的sql优化

记一次join + order by 的sql优化

使用 JOIN 优化 SQL 查询的 ORDER BY 和 WHERE

使用 ORDER BY 和 INNER JOIN 优化 MySQL 查询(选择用户关注的位置)

使用 LEFT JOIN + ORDER BY 时如何避免 FileSort?