有什么方法可以优化这个MySQL查询? (资源紧张)

时间:2016-06-01 16:13:41

标签: mysql optimization query-optimization

我的应用需要经常运行此查询,这会获取要显示的应用的用户数据列表。问题是关于user_quiz的子查询资源很重,计算排名也非常强大。 基准:每次运行约0.5秒 什么时候运行:

  • 当用户想要查看他们的排名时
  • 当用户想要查看其他人的排名时
  • 获取用户朋友列表

.5秒,考虑到这个查询会经常运行很长时间。我可以做些什么来优化这个查询?

user表:

CREATE TABLE `user` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `firstname` varchar(100) DEFAULT NULL,
 `lastname` varchar(100) DEFAULT NULL,
 `password` varchar(20) NOT NULL,
 `email` varchar(300) NOT NULL,
 `verified` tinyint(10) DEFAULT NULL,
 `avatar` varchar(300) DEFAULT NULL,
 `points_total` int(11) unsigned NOT NULL DEFAULT '0',
 `points_today` int(11) unsigned NOT NULL DEFAULT '0',
 `number_correctanswer` int(11) unsigned NOT NULL DEFAULT '0',
 `number_watchedvideo` int(11) unsigned NOT NULL DEFAULT '0',
 `create_time` datetime NOT NULL,
 `type` tinyint(1) unsigned NOT NULL DEFAULT '1',
 `number_win` int(11) unsigned NOT NULL DEFAULT '0',
 `number_lost` int(11) unsigned NOT NULL DEFAULT '0',
 `number_tie` int(11) unsigned NOT NULL DEFAULT '0',
 `level` int(1) unsigned NOT NULL DEFAULT '0',
 `islogined` tinyint(1) unsigned NOT NULL DEFAULT '0',
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=230 DEFAULT CHARSET=utf8;

user_quiz表:

CREATE TABLE `user_quiz` (
 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
 `user_id` int(11) NOT NULL,
 `question_id` int(11) NOT NULL,
 `is_answercorrect` int(11) unsigned NOT NULL DEFAULT '0',
 `question_answer_datetime` datetime NOT NULL,
 `score` int(1) DEFAULT NULL,
 `quarter` int(1) DEFAULT NULL,
 `game_type` int(1) DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=9816 DEFAULT CHARSET=utf8;

user_starter表:

CREATE TABLE `user_starter` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `user_id` int(11) DEFAULT NULL,
 `result` int(1) DEFAULT NULL,
 `created_date` date DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=456 DEFAULT CHARSET=utf8mb4;

我的索引:

Table: user
Table    Non_unique    Key_name    Seq_in_index    Column_name    Collation    Cardinality    Sub_part    Packed    Null    Index_type    Comment    Index_comment
user    0    PRIMARY    1    id    A    32                BTREE

Table: user_quiz
Table    Non_unique    Key_name    Seq_in_index    Column_name    Collation    Cardinality    Sub_part    Packed    Null    Index_type    Comment    Index_comment
user_quiz    0    PRIMARY    1    id    A    9462                BTREE
user_quiz    1    user_id    1    user_id    A    270                BTREE

Table: user_starter
Table    Non_unique    Key_name    Seq_in_index    Column_name    Collation    Cardinality    Sub_part    Packed    Null    Index_type    Comment    Index_comment
user_starter    0    PRIMARY    1    id    A    454                BTREE
user_starter    1    user_id    1    user_id    A    227            YES    BTREE

查询:

SET @curRank = 0;
SET @lastPlayerPoints = 0;
SELECT
  sub.*,
  @curRank := IF(@lastPlayerPoints!=points_week, @curRank + 1, @curRank) AS rank,
    @lastPlayerPoints := points_week AS db_PPW
FROM (
  SELECT u.id,u.firstname,u.lastname,u.email,u.avatar,u.type,u.points_total,u.number_win,u.number_lost,u.number_tie,u.verified,
    COALESCE(SUM(uq.score),0) as points_week,
    COALESCE(us.number_lost,0) as number_week_lost,
    COALESCE(us.number_win,0) as number_week_win,
    (select MAX(question_answer_datetime) from user_quiz WHERE user_id = u.id and game_type = 1) as lastFrdFight,
    (select MAX(question_answer_datetime) from user_quiz WHERE user_id = u.id and game_type = 2) as lastBotFight
  FROM `user` u
  LEFT JOIN (SELECT user_id,
    count(case when result=1 then 1 else null end) as number_win,
    count(case when result=-1 then 1 else null end) as number_lost
    from user_starter where created_date BETWEEN '2016-01-11 00:00:00' AND '2016-05-12 05:10:27' ) us ON u.id = us.user_id
  LEFT JOIN (SELECT * FROM user_quiz WHERE question_answer_datetime BETWEEN '2016-01-11 00:00:00' AND '2016-05-12 00:00:00') uq on u.id = uq.user_id
  GROUP BY u.id ORDER BY points_week DESC, u.lastname ASC, u.firstname ASC
) as sub

说明:

id    select_type    table    type    possible_keys    key    key_len    ref    rows    filtered    Extra
1    PRIMARY    <derived2>    ALL                    3027    100    
2    DERIVED    u    ALL    PRIMARY                32    100    Using temporary; Using filesort
2    DERIVED    <derived5>    ALL                    1    100    Using where; Using join buffer (Block Nested Loop)
2    DERIVED    <derived6>    ref    <auto_key0>    <auto_key0>    4    fancard.u.id    94    100    
6    DERIVED    user_quiz    ALL                    9461    100    Using where
5    DERIVED    user_starter    ALL                    454    100    Using where
4    DEPENDENT SUBQUERY    user_quiz    ref    user_id    user_id    4    func    35    100    Using where
3    DEPENDENT SUBQUERY    user_quiz    ref    user_id    user_id    4    func    35    100    Using where

示例输出和预期输出: enter image description here

基准测试:大约0.5秒

1 个答案:

答案 0 :(得分:2)

以下索引应使子查询超快{。}}。

user_quiz

请为所有表提供ALTER TABLE user_quiz ADD INDEX (`user_id`,`game_type`,`question_answer_datetime`) 语句,因为这有助于进行其他优化。

更新#1

好吧,我有时间仔细研究一下,幸运的是,在优化方面似乎有很多相对较低的成果。

以下是要添加的所有索引:

SHOW CREATE TABLE tablename

请注意,名称(例如ALTER TABLE user_quiz ADD INDEX `userGametypeAnswerDatetimes` (`user_id`,`game_type`,`question_answer_datetime`) ALTER TABLE user_quiz ADD INDEX `userAnswerScores` (`user_id`,`question_answer_datetime`,`score`) ALTER TABLE user_starter ADD INDEX `userResultDates` (`user_id`,`result`,`created_date`) )是可选的,您可以将它们命名为对您最有意义的名称。但是,一般来说,最好在自定义索引上放置特定的名称(仅用于组织目的。)

现在,您的查询应该适用于这些新索引:

userGametypeAnswerDatetimes

注意:这不一定是最好的结果。这取决于你的数据集,这是否一定是最好的,有时你需要做一些试验和错误。

我们如何查询SET @curRank = 0; SET @lastPlayerPoints = 0; SELECT sub.*, @curRank := IF(@lastPlayerPoints!=points_week, @curRank + 1, @curRank) AS rank, @lastPlayerPoints := points_week AS db_PPW FROM ( SELECT u.id, u.firstname, u.lastname, u.email, u.avatar, u.type, u.points_total, u.number_win, u.number_lost, u.number_tie, u.verified, COALESCE(user_scores.score,0) as points_week, COALESCE(user_losses.number_lost,0) as number_week_lost, COALESCE(user_wins.number_win,0) as number_week_win, ( select MAX(question_answer_datetime) from user_quiz WHERE user_id = u.id and game_type = 1 ) as lastFrdFight, ( select MAX(question_answer_datetime) from user_quiz WHERE user_id = u.id and game_type = 2 ) as lastBotFight FROM `user` u LEFT OUTER JOIN ( SELECT user_id, COUNT(*) AS number_won from user_starter WHERE created_date BETWEEN '2016-01-11 00:00:00' AND '2016-05-12 05:10:27' AND result = 1 GROUP BY user_id ) user_wins ON user_wins.user_id = u.user_id LEFT OUTER JOIN ( SELECT user_id, COUNT(*) AS number_lost from user_starter WHERE created_date BETWEEN '2016-01-11 00:00:00' AND '2016-05-12 05:10:27' AND result = -1 GROUP BY user_id ) user_losses ON user_losses.user_id = u.user_id LEFT OUTER JOIN ( SELECT SUM(score) FROM user_quiz WHERE question_answer_datetime BETWEEN '2016-01-11 00:00:00' AND '2016-05-12 00:00:00' GROUP BY user_id ) user_scores ON u.id = user_scores.user_id ORDER BY points_week DESC, u.lastname ASC, u.firstname ASC ) as sub lastFrdFight经文如何查询lastBotFightpoints_week,我们如何使用试验和错误的提示, number_week_lost。所有这些都可以在select语句中完成(就像我的查询中的前两个一样)或者可以通过加入子查询结果来完成(就像在我的查询中最后三个一样)。

混合搭配,看看哪种效果最好。一般情况下,当外部查询中有大量行时(在这种情况下,查询number_week_win表),我发现加入子查询是最快的。这是因为它只需要获取结果一次,然后可以在用户的​​基础上匹配它们。其他时候,最好只在SELECT子句中进行查询 - 这将更快地运行,因为有更多常量(user_id已知),但必须为每一行运行。所以这是一个权衡,为什么你有时需要使用反复试验。

为什么索引有效?

所以,你可能想知道我为什么像我那样制作索引。如果你熟悉电话簿(在这个智能手机时代,这不再是我能做出的有效假设),那么我们可以将其作为一个类比:

如果您的用户表上有一个userphonebookIndexlastnamefirstname)的综合索引(例如此处!您实际上不需要添加该索引索引!)你会得到一个类似于电话簿提供的结果。 (使用电子邮件而不是电话号码。)

每个索引都是整个表中数据的内部副本。使用此email,内部将存储所有用户的列表,其中包含姓氏,然后是他们的名字,然后是他们的电子邮件,并且每个用户都会被订购,就像电话簿一样。

为什么这有用?考虑一下你知道某人的名字和姓氏。您可以快速翻到他们姓氏的位置,然后快速浏览所有姓氏的名单,找到您想要的名字,以便获取电子邮件。

就数据库如何看待它们而言,索引的工作方式完全相同。

考虑我上面定义的phonebookIndex索引,以及我们如何在userGametypeAnswerDatetimes SELECT子查询中查询该索引。

lastFrdFight

注意我们如何将user_id(来自外部查询)和game_type都作为常量。这与我们之前的示例完全相同,具有名字和姓氏,并且想要查找电子邮件/电话号码。在这种情况下,我们正在寻找索引中第3个值的MAX。仍然很简单:所有的值都是有序的,所以如果这个索引位于我们面前,我们可以翻到特定的user_id,然后查看所有( select MAX(question_answer_datetime) from user_quiz WHERE user_id = u.id and game_type = 1 ) as lastFrdFight 的部分,然后选择最后一个值来查找最大值。非常快。对于数据库也是如此。它可以非常快速地找到这个值,这就是为什么你看到整个查询时间减少80%以上的原因。

所以,这就是索引的工作原理,以及为什么我像我一样选择这些索引。

请注意,您拥有的索引越多,在执行插入和更新时您就会看到越慢的速度。但是,如果你从表中阅读的内容比你写的要多得多,这通常是一种可接受的权衡。

所以,给这些更改一个镜头,让我知道它是如何执行的。如果您需要进一步的优化帮助,请提供新的EXPLAIN计划。此外,这应该为您提供相当多的工具来使用试验和错误来查看什么不起作用。我的所有更改都相互独立,因此您可以将它们与原始查询部分进行交换,以查看每个部分的工作方式。