【问题标题】:What's the most efficient way to structure a 2-dimensional MySQL query?构造二维 MySQL 查询的最有效方法是什么?
【发布时间】:2013-02-08 19:40:50
【问题描述】:

我有一个包含以下表格和字段的 MySQL 数据库:

  • 学生(身份证)
  • 类(id)
  • 等级(id、student_id、class_id、等级)

学生和班级表在 id(主键)上建立索引。成绩表以 id(主键)和 student_id、class_id 和成绩为索引。

我需要构建一个查询,在给定班级 ID 的情况下,它会列出所有其他班级以及在该班级中得分更高的学生人数。

基本上,给定成绩表中的以下数据:

student_id | class_id | grade
--------------------------------------
1          | 1        | 87
1          | 2        | 91
1          | 3        | 75
2          | 1        | 68
2          | 2        | 95
2          | 3        | 84
3          | 1        | 76
3          | 2        | 88
3          | 3        | 71

使用类 ID 1 进行查询应该会产生:

class_id | total
-------------------
2        | 3
3        | 1

理想情况下,我希望它在几秒钟内执行,因为我希望它成为 Web 界面的一部分。

我遇到的问题是,在我的数据库中,我有超过 1300 个班级和 160,000 名学生。我的成绩表有近 1500 万行,因此查询需要很长时间才能执行。

这是我迄今为止尝试过的方法以及每个查询所用的时间:

-- I manually stopped execution after 2 hours
SELECT    c.id, COUNT(*) AS total
FROM      classes c
              INNER JOIN grades a ON a.class_id = c.id
              INNER JOIN grades b ON b.grade < a.grade AND
                  a.student_id = b.student_id AND
                  b.class_id = 1
WHERE     c.id != 1 AND
GROUP BY  c.id

-- I manually stopped execution after 20 minutes
SELECT    c.id,
          (
              SELECT    COUNT(*) 
              FROM      grades g 
              WHERE     g.class_id = c.id AND g.grade > (
                            SELECT   grade 
                            FROM     grades 
                            WHERE    student_id = g.student_id AND 
                                     class_id = 1
                        )
          ) AS total
FROM      classes c
WHERE     c.id != 1;

-- 1 min 12 sec
CREATE TEMPORARY TABLE temp_blah (student_id INT(11) PRIMARY KEY, grade INT);
INSERT INTO temp_blah SELECT student_id, grade FROM grades WHERE class_id = 1;
SELECT    o.id,
          ( 
              SELECT    COUNT(*)
              FROM      grades g
                            INNER JOIN temp_blah t ON g.student_id = t.student_id
              WHERE     g.class_id = c.id AND t.grade < g.grade
          ) AS total
FROM      classes c
WHERE     c.id != 1;

-- Same thing but with joins instead of a subquery - 1 min 54 sec
SELECT    c.id,
          COUNT(*) AS total
FROM      classes c
              INNER JOIN grades g ON c.id = p.class_id
              INNER JOIN temp_blah t ON g.student_id = t.student_id
WHERE     c.id != 1
GROUP BY  c.id;

我还考虑创建一个 2D 表,将学生作为行,将班级作为列,但是我可以看到两个问题:

  • MySQL 实现了最大列数 (4096) 和最大行大小(以字节为单位),此方法可能会超出此值
  • 我想不出查询该结构以获得所需结果的好方法

我还考虑将这些计算作为后台作业执行并将结果存储在某处,但为了使信息保持最新(必须),每次创建或更新学生、班级或成绩记录时都需要重新计算它们。

有谁知道构造这个查询的更有效方法?

编辑:创建表语句:

CREATE TABLE `classes` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1331 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci$$

CREATE TABLE `students` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=160803 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci$$

CREATE TABLE `grades` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `student_id` int(11) DEFAULT NULL,
  `class_id` int(11) DEFAULT NULL,
  `grade` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_grades_on_student_id` (`student_id`),
  KEY `index_grades_on_class_id` (`class_id`),
  KEY `index_grades_on_grade` (`grade`)
) ENGINE=InnoDB AUTO_INCREMENT=15507698 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci$$

最有效查询的解释输出(1 分 12 秒):

id | select_type        | table | type   | possible_keys                                                             | key                      | key_len | ref               | rows   | extra 
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1  | PRIMARY            | c     | range  | PRIMARY                                                                   | PRIMARY                  | 4       |                   | 683    | Using where; Using index
2  | DEPENDENT SUBQUERY | g     | ref    | index_grades_on_student_id,index_grades_on_class_id,index_grades_on_grade | index_grades_on_class_id | 5       | mydb.c.id         | 830393 | Using where
2  | DEPENDENT SUBQUERY | t     | eq_ref | PRIMARY                                                                   | PRIMARY                  | 4       | mydb.g.student_id | 1      | Using where

另一个编辑 - 解释 sgeddes 建议的输出:

+----+-------------+------------+--------+---------------+------+---------+------+----------+----------------------------------------------+
| id | select_type | table      | type   | possible_keys | key  | key_len | ref  | rows     | Extra                                        |
+----+-------------+------------+--------+---------------+------+---------+------+----------+----------------------------------------------+
|  1 | PRIMARY     | <derived2> | ALL    | NULL          | NULL | NULL    | NULL | 14953992 | Using where; Using temporary; Using filesort |
|  2 | DERIVED     | <derived3> | system | NULL          | NULL | NULL    | NULL |        1 | Using filesort                               |
|  2 | DERIVED     | G          | ALL    | NULL          | NULL | NULL    | NULL | 15115388 |                                              |
|  3 | DERIVED     | NULL       | NULL   | NULL          | NULL | NULL    | NULL |     NULL | No tables used                               |
+----+-------------+------------+--------+---------------+------+---------+------+----------+----------------------------------------------+

【问题讨论】:

  • 您提供的示例数据是否与您的表格一样?如果是这样,你能把它标准化吗?
  • 你应该认真规范你的数据库,重复列是一种反模式。
  • “我有超过 1300 个班级和 160,000 名学生”这就是数据库的用途,也是您使用 MySQL 而不是 Excel 的原因。 ;-) 如果您的查询速度很慢,请检查您的索引。
  • 真的吗?那么Class 1Class 2Class 3 是什么?这是重复。
  • 你能用一些示例数据制作一个 sqlfiddle 吗?

标签: mysql sql database performance


【解决方案1】:

我认为这应该适合您使用 SUMCASE

SELECT C.Id,
  SUM(
    CASE 
    WHEN G.Grade > C2.Grade THEN 1 ELSE 0 
    END
  ) 
FROM Class C
  INNER JOIN Grade G ON C.Id = G.Class_Id
  LEFT JOIN (
      SELECT Grade, Student_Id, Class_Id
      FROM Class
        JOIN Grade ON Class.Id = Grade.Class_Id
      WHERE Class.Id = 1
    ) C2 ON G.Student_Id = C2.Student_Id
WHERE C.Id <> 1
GROUP BY C.Id

Sample Fiddle Demo

--编辑--

针对您的评论,这是另一个应该更快的尝试:

SELECT 
  Class_Id, 
  SUM(CASE WHEN Grade > minGrade THEN 1 ELSE 0 END)
FROM 
(
  SELECT 
    Student_Id,
    @classToCheck:=
      IF(G.Class_Id = 1, Grade, @classToCheck) minGrade ,
    Class_Id,
    Grade
  FROM Grade G
    JOIN (SELECT @classToCheck:= 0) t
  ORDER BY Student_Id, IF(Class_Id = 1, 0, 1)
  ) t
WHERE  Class_Id <> 1
GROUP BY Class_ID

还有more sample fiddle

【讨论】:

  • 嗨@sgeddes,感谢您的回复。不幸的是,这不起作用:我在 5 分钟后停止了执行。总的来说,我认为任何试图加入成绩表的事情都会花费很长时间。
  • @pricj004 -- 没问题。我已经编辑了我的答案,以包括我认为应该是一个更快的解决方案。让我知道——祝你好运!
  • 快得多! 43 秒。肯定会到达某个地方。我已编辑问题以显示您查询的解释输出。
  • @pricj004 -- 很高兴我能帮上忙。不确定我能想到任何更快的解决方案:D
【解决方案2】:

您也可以尝试一下原始数据吗?它只是一个连接:)

select
  final.class_id, count(*) as total
from
  (
    select * from   
      (select student_id as p_student_id, grade as p_grade from table1 where class_id = 1) as partial
    inner join table1 on table1.student_id = partial.p_student_id
    where table1.class_id <> 1 and table1.grade > partial.p_grade    
  ) as final
 group by
  final.class_id;

sqlfiddle link

【讨论】:

  • @pricj004 试一试。它可能也很慢,想不出更好的方法。 :)
  • 嗨@jurgenreza,感谢您的建议。这还不错 - 在我的机器上用了 1 分 46 秒,如果我用 temp_blah(我创建的临时索引表)替换部分表,速度会更快一些(1 分 37 秒)。
  • @pricj004 很酷,希望对您有所帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-03-22
  • 2012-10-30
  • 2018-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-19
相关资源
最近更新 更多