如何优化这个简单的 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_Views
或Sent_Winks
更改时,修改Score
列以匹配。这可能通过触发器来完成(我对触发器的经验有限),或者绝对可以在更新Sent_Views
和Sent_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 优化 SQL 查询的 ORDER BY 和 WHERE