有什么方法优化这个MySQL查询吗?(资源密集型)

有什么方法优化这个MySQL查询吗?(资源密集型),mysql,optimization,query-optimization,Mysql,Optimization,Query Optimization,我的应用程序需要经常运行此查询,以获取应用程序要显示的用户数据列表。问题是关于用户测验的子查询是资源密集型的,并且计算排名也非常耗费CPU。 基准测试:每次运行约5秒 它将在何时运行: 当用户想要查看他们的排名时 当用户想要查看其他人的排名时 获取用户的好友列表 .5秒考虑到此查询将经常运行,这是一个非常长的时间。我可以做些什么来优化这个查询吗 用户的表: CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `first

我的应用程序需要经常运行此查询,以获取应用程序要显示的用户数据列表。问题是关于
用户测验的子查询是资源密集型的,并且计算排名也非常耗费CPU。
基准测试:每次运行约5秒
它将在何时运行:

  • 当用户想要查看他们的排名时
  • 当用户想要查看其他人的排名时
  • 获取用户的好友列表
.5秒考虑到此查询将经常运行,这是一个非常长的时间。我可以做些什么来优化这个查询吗

用户的表

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;
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;
用户测验表

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;
用户启动程序的表

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;
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
id选择类型表类型可能的键参考行过滤额外
1小学全部3027100
2.所有主要32 100使用临时;使用文件排序
2使用where推导出所有1100;使用联接缓冲区(块嵌套循环)
2导出参考


基准点:大约0.5秒

以下索引应使子查询到
用户测验
极快

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

更新#1 好的,我已经花了一些时间来研究了,幸运的是,在优化方面,似乎有很多相对容易实现的结果

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

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
lastBotFight
的结构,以及我们如何查询
points\u week
number\u week\u lost
number\u week\u win
。所有这些都可以在select语句中完成(如前两个在我的查询中),也可以通过连接子查询结果来完成(如后三个在我的查询中完成)

混合搭配,看看什么效果最好。一般来说,我发现当外部查询中有大量行时(在本例中,查询
user
表),连接到子查询的速度最快。这是因为它只需要获得一次结果,然后就可以逐个用户地匹配结果。其他时候,最好只在SELECT子句中进行查询-这将运行得更快,因为有更多常量(用户id已经知道),但必须针对每一行运行。因此,这是一种权衡,也是为什么有时需要使用试错法

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

如果您的用户表上有一个由
phonebookIndex
lastname
firstname
email
)组成的复合索引(这里的示例!您实际上不需要添加该索引!),您将得到与电话簿提供的结果类似的结果。(使用电子邮件而不是电话号码。)

每个索引都是整个表中数据的内部副本。通过这个
phonebookIndex
可以在内部存储一个所有用户的列表,其中包括他们的姓、名和电子邮件,并且每个用户都可以像电话簿一样进行排序

这为什么有用?当你知道某人的姓和名时,考虑一下。你可以快速翻到他们的姓氏,然后快速浏览每个人的姓氏列表,找到你想要的名字,从而获得电子邮件

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

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

(
    select MAX(question_answer_datetime) 
    from user_quiz 
    WHERE user_id = u.id and game_type = 1
) as lastFrdFight
请注意,我们如何将用户id(来自外部查询)和游戏类型作为常量。这与我们前面的示例完全相同,有名字和姓氏,并且想要查找电子邮件/电话号码。在本例中,我们将查找索引中第三个值的最大值。仍然很简单:所有的值都是有序的,所以如果这个索引在我们前面,我们可以直接切换到特定的用户id,然后查看所有
game\u type=1
的部分,然后只选择最后一个值来找到最大值。非常非常快。数据库也是如此。它可以非常快地找到这个值,这就是为什么您的总查询时间减少了80%以上

这就是索引的工作原理,也是我选择这些索引的原因

请注意,索引越多,在执行插入和更新时,速度越慢。但是,如果你从表格中读的比写的多得多,这通常是一个不可接受的权衡

因此,请尝试一下这些更改,并让我知道它的性能。如果您需要进一步的优化帮助,请提供新的解释计划。此外,这应该给你很多工具来使用试错法,看看什么是有效的,什么是无效的。我所做的所有更改都是相互独立的,因此您可以将它们与原始查询片段进行交换,以查看每个查询片段是如何运行的