如何编写快速高效的SQL查询(一)——MySQL8.0优化器查询优化处理与样例

当希望MySQL能够以更高的性能运行查询时,最好的办法就是弄清楚MySQL是如何优化和执行查询的。一旦理解了这一点,很多查询优化工作实际上就是遵循一些原则让优化器能够按照预想的合理的方式运行。
MySQL是如何执行一个查询的过程的?根据图8-1可以看到,当向MySQL发送一个请求的时候,MySQL到底做了些什么:

  1. 客户端给服务器发送一条SQL查询语句。
  2. 服务器端进行SQL语句解析、预处理,再由优化器生成对应的执行计划。
  3. MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询。
  4. 将结果返回给客户端。
    在这里插入图片描述
    在这里我们重点关注查询优化器的工作,查询优化器是其中特别复杂也特别难理解的部分。还有很多的例外情况,例如,当查询使用绑定变量后,执行路径会有所不同。

查询优化处理

在用户通过通过协议向服务端发送一条SQL查询后,查询的生命周期的下一步是将一个SQL查询转换成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互。这包括多个子阶段:解析SQL、预处理、优化SQL执行计划。这个过程中产生的任何错误(例如,语法错误)都可能终止查询。这里不打算详细介绍MySQL的内部实现,而只是选择性地介绍其中几个独立的部分。在实际执行中,这几部分可能一起执行也可能单独执行。我们的目的是帮助大家理解MySQL如何执行查询,以便写出更优秀的查询。

语法解析器和预处理

首先,MySQL通过关键字将SQL语句进行解析,并生成一棵对应的“解析树”。MySQL解析器将使用MySQL语法规则验证和解析查询。例如,它将验证是否使用了错误的关键字,使用关键字的顺序是否正确,或者它还会验证引号是否能前后正确匹配。
然后,预处理器检查生成的解析树,以查找解析器无法解析的其他语义,例如,这里将检查数据表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义。
下一步预处理器会验证权限。这通常很快,除非服务器上有非常多的权限配置。

查询优化器

现在解析树被认为是合法的了,并且由优化器将其转化成查询执行计划。一条查询可以有很多种执行方式,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。
MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。最初,成本的最小单位是随机读取一个4KB数据页的成本,后来成本计算公式变得更加复杂,并且引入了一些“因子”来估算某些操作的代价,如执行一次WHERE条件比较的成本。可以通过查询当前会话的Last_query_cost的值来得知MySQL计算的当前查询的成本:
在这里插入图片描述

这个结果表示,MySQL的优化器认为大概需要做1040个数据页的随机查找才能完成上面的查询。这是根据一系列的统计信息计算得来的:每个表或者索引的页面个数、索引的基数(索引中不同值的数量)、索引和数据行的长度、索引分布情况。优化器在评估成本的时候并不考虑任何层面的缓存带来的影响,它假设读取任何数据都需要一次磁盘I/O。有很多种原因会导致MySQL优化器选择错误的执行计划,如下所示:

  • 统计信息不准确。MySQL服务器依赖存储引擎提供的统计信息来评估成本,但是有的存储引擎提供的信息是准确的,有的偏差可能非常大。例如,InnoDB因为其MVCC的架构,并不能维护一个数据表的行数的精确统计信息。
  • 成本指标并不完全等同于运行查询的实际成本,因此即使统计数据是准确的,查询的成本也可能超过或者低于MySQL估算的近似值。例如,有时候某个执行计划虽然需要读取更多的页面,但是它的成本却更低。因为如果这些页面都是顺序读或者这些页面都已经在内存中的话,那么它的访问成本将很低。MySQL并不知道哪些页面在内存中、哪些在磁盘中,所以查询在实际执行过程中到底需要多少次物理I/O是无法得知的。
  • MySQL的最优可能和你想的最优不一样。你可能希望执行时间尽可能短,但是MySQL只是基于其成本模型选择最优的执行计划,而有些时候这并不是最快的执行方式。所以,这里我们看到的根据执行成本来选择执行计划并不是完美的模型。
  • MySQL从不考虑其他并发执行的查询,这可能会影响到当前查询的速度。
  • MySQL也并不是任何时候都是基于成本的优化。它有时也会基于一些固定的规则,例如,如果存在全文搜索的MATCH()子句,则在存在FULLTEXT索引的时候就使用全文索引。即使有时候使用其他索引和WHERE条件可以远比这种方式要快,MySQL也仍然会使用对应的全文索引。
  • MySQL不会考虑不受其控制的操作的成本,例如,执行存储函数或者用户自定义函数的成本。
  • 后面我们还会看到,优化器有时候无法估算所有可能的执行计划,所以它可能错过实际上最优的执行计划。

MySQL的查询优化器是一个非常复杂的软件,它使用了很多优化策略来生成一个最优的执行计划。优化策略可以简单地分为两种,一种是静态优化,一种是动态优化。静态优化可以直接对解析树进行分析,并完成优化。例如,优化器可以通过一些简单的代数变换将WHERE条件转换成另一种等价形式。静态优化不依赖于特别的数值,如WHERE条件中带入的一些常数等。静态优化在第一次完成后就一直有效,即使使用不同的参数重复执行查询也不会发生变化,可以认为这是一种“编译时优化”。
然而,动态优化则和查询的上下文有关,也可能和很多其他因素有关,例如WHERE条件中的取值、索引中条目对应的数据行数等。这需要在每次查询的时候都重新评估,可以认为这是“运行时优化”。

在执行绑定变量和存储过程的时候,动态优化和静态优化的区别非常重要。MySQL对查询的静态优化只需要做一次,但对查询的动态优化则在每次执行时都需要重新评估。有时候甚至在查询的执行过程中也会重新优化。1

MySQL能够处理的优化类型

重新定义联接表的顺序

数据表的联接并不总是按照在查询中指定的顺序进行。决定联接的顺序是优化器很重要的一个功能,本章后面将深入介绍这一点。将外联接转化成内联接并不是所有的OUTER JOIN语句都必须以外联接的方式执行。诸多因素,例如WHERE条件、库表结构都可能会让外联接等价于一个内联接。MySQL能够识别这一点并重写查询,让其可以调整联接顺序。

使用代数等价变换规则

MySQL可以使用一些代数等价变换规则来简化并规范表达式。它可以合并和减少一些比较,还可以移除一些恒成立和一些恒不成立的判断。例如,(5=5 AND a>5)将被改写为a>5。类似地,如果有(a<b AND b=c)AND a=5则会改写为b>5 AND b=c AND a=5。这些规则对于编写条件语句很有用。

优化COUNT()、MIN()和MAX()

索引和列是否可为空通常可以帮助MySQL优化这类表达式。例如,要找到某一列的最小值,只需要查询对应B-tree索引最左端的记录,MySQL可以直接获取索引的第一行记录。在优化器生成执行计划的时候就可以利用这一点,在B-tree索引中,优化器会将这个表达式作为一个常数对待。类似地,如果要查找一个最大值,也只需读取B-tree索引的最后一条记录。如果MySQL使用了这种类型的优化,那么在EXPLAIN中就可以看到“Select tables optimizedaway”。从字面意思可以看出,它表示优化器已经从执行计划中移除了该表,并以一个常数代替。

预估并转化为常数表达式

当MySQL检测到一个表达式可以转化为常数的时候,就会一直把该表达式作为常数进行优化处理。例如,一个用户自定义变量在查询中没有发生变化时就可以将其转换为一个常数。数学表达式则是另一种典型的例子。
让人惊讶的是,在优化阶段,有时候一个查询也能够转化为一个常数。一个例子是在索引列上执行MIN()函数。甚至是主键或者唯一键查找语句也可以被转换为常数表达式。如果WHERE子句中使用了该类索引的常数条件,MySQL可以在查询开始阶段就先查找到这些值,这样优化器就能够知道并将其转换为常数表达式。下面是一个例子:
在这里插入图片描述
MySQL分两步来执行这个查询,也就是上面执行计划的两行输出。第一步先从film表找到需要的行。因为在film_id列上有主键索引,所以MySQL优化器知道这只会返回一行数据,优化器在生成执行计划的时候,就已经通过索引信息知道将返回多少行数据了。因为查询优化器已经明确知道有多少个值(WHERE条件中的值)需要做索引查询,所以这里的表访问类型是const。在执行计划的第二步时,MySQL将第一步中返回的film_id列当作一个已知取值的列来处理。因为优化器清楚在第一步执行完成后,该值就会是明确的了。注意,正如在第一步中一样,使用film_actor字段对表的访问类型也是const。
另一种会看到常数条件的情况是通过等式将常数值从一个表传到另一个表,这可以通过WHERE、USING或者ON语句来限制某列取值为常数。在上面的例子中,因为使用了USING子句,所以优化器知道这会限制film_id在整个查询过程中始终都是一个常量——因为它必须等于WHERE子句中的那个取值。

覆盖索引扫描

当索引中的列包含所有查询中需要使用的列的时候,MySQL就可以使用索引返回需要的数据,而无须查询对应的数据行。

子查询优化

MySQL在某些情况下可以将子查询转换为一种效率更高的形式,从而减少多个查询多次对数据进行访问。

提前终止查询

在发现已经满足查询需求的时候,MySQL总是能够立刻终止查询。一个典型的例子就是当使用了LIMIT子句的时候。除此之外,MySQL在其他几类情况下也会提前终止查询,例如发现了一个不成立的条件,这时MySQL可以立刻返回一个空结果。从下面的例子中可以看到这一点:
在这里插入图片描述
从这个例子中可看到,查询在优化阶段就已经终止。除此之外,MySQL在执行过程中,如果发现某些特殊的条件,则会提前终止查询。当查询执行引擎需要检索“不同取值”或者判断存在性的时候,MySQL都可以使用这类优化。例如,我们现在需要找到没有演员的所有电影:2

这个查询将会过滤掉所有有演员的电影。每一部电影可能都会有很多的演员,但是上面的查询一旦找到任何一个演员,就会停止并立刻判断下一部电影,因为只要有一名演员,那么WHERE条件就会过滤掉这部电影。类似这种“不同值/不存在”的优化一般可用于DISTINCT、NOT EXIST()或者LEFT JOIN类型的查询。

等值传播

如果两列的值可通过等式联接,那么MySQL能够把其中一列的WHERE条件传递到另一列上。例如,我们看下面的查询:
在这里插入图片描述
因为这里使用了film_id字段进行等值联接,MySQL知道这里的WHERE子句不仅适用于film表,而且对于film_actor表同样适用。如果使用的是其他的数据库管理系统,可能还需要手动通过一些条件来告知优化器这个WHERE条件适用于两个表,那么写法就会如下:
在这里插入图片描述

在MySQL中这是不必要的,这样写反而会让查询更难维护。

列表IN()的比较

在很多数据库服务器中,IN()完全等同于多个OR条件的子句,因为这两者是完全等价的。在MySQL中这点是不成立的,MySQL将IN()列表中的数据先进行排序,然后通过二分查找的方式来确定列表中的值是否满足条件,这是一个O(logn)复杂度的操作,等价地转换成OR查询的复杂度为O(n),对于IN()列表中有大量取值的时候,MySQL的处理速度将会更快。

上面列举的远不是MySQL优化器的全部,MySQL还会做大量其他的优化,即使本章全部用来描述这一点也会篇幅不足,但上面的这些例子已经足以让大家明白优化器的复杂性和智能性了。如果说从上面这段讨论中我们应该学到什么,那就是不要自以为比优化器更聪明。最终你可能会占一些便宜,但是有可能会使查询变得更加复杂而难以维护,而最终的收益为零。让优化器按照它的方式工作就可以了。
当然,虽然优化器已经很智能了,但是有时候也无法给出最优的结果。有时候你可能比优化器更了解数据,例如,由于应用逻辑使得某些条件总是成立;还有时候,优化器缺少某种功能特性,如哈希索引;再如前面提到的,从优化器的执行成本角度评估出来的最优执行计划,在实际运行中可能比其他的执行计划更慢。
如果能够确认优化器给出的不是最佳选择,并且清楚背后的原理,那么也可以帮助优化器做进一步的优化。例如,可以在查询中添加hint提示3,也可以重写查询,或者重新设计更优的库表结构,或者添加更合适的索引。

表和索引的统计信息

MySQL架构由多个层次组成。在服务器层有查询优化器,却没有保存数据和索引的统计信息。统计信息由存储引擎实现,不同的存储引擎可能会存储不同的统计信息(也可以按照不同的方式存储统计信息)。
因为服务器没有存储任何统计信息,所以MySQL查询优化器在生成查询的执行计划时,需要向存储引擎获取相应的统计信息。存储引擎则给优化器提供对应的统计信息,包括:每个表或者索引有多少个页面、每个表的每个索引的基数是多少、数据行和索引的长度是多少、索引的分布信息等。优化器根据这些信息来选择一个最优的执行计划。

MySQL如何执行联接查询

MySQL中使用的术语“联接”(对应英文为Join)的范围可能比你熟悉的更广泛。总的来说,MySQL认为每一个查询都是联接——不仅是匹配两张表中对应行的查询,而是每一个查询、每一个片段(包括子查询,甚至基于单表的SELECT)都是联接。因此,理解MySQL如何执行联接查询是非常重要的。
所以,理解MySQL如何执行UNION查询至关重要。我们先来看一个UNION查询的例子。对于UNION查询,MySQL先将一系列的单个查询结果放到一个临时表中,然后再重新读出临时表中的数据来完成UNION查询。在MySQL的概念中,每个查询都是一次联接,所以读取临时表的结果也是一次联接。
当前MySQL的联接执行策略很简单:MySQL对任何联接都执行嵌套循环联接操作,即MySQL先在一个表中循环取出单条数据,然后再嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为止。最后根据各个表匹配的行,返回查询中需要的各列。MySQL会尝试在最后一个联接表中找到所有匹配的行,如果最后一个联接表无法找到更多的行,MySQL返回到上一层次的联接表,看是否能够找到更多的匹配记录,依此类推,迭代执行。4
在MySQL 8.0.20版本之后,已经不再使用基于块的嵌套循环联接操作,取而代之的是哈希联接。这让联接操作性能变得更好,特别是当数据集可以全部存储在内存时。

执行计划

和很多其他关系数据库不同,MySQL并不会生成查询字节码来执行查询。MySQL生成查询的一棵指令树5,然后通过查询执行引擎执行完成这棵指令树并返回结果。最终的执行计划包含了重构查询的全部信息。如果你对某个查询执行EXPLAIN EXTENDED后,再执行SHOW WARNINGS,就可以看到重构出的查询。6
任何多表查询都可以使用一棵树来表示,例如,可以按照图8-2执行一个四表的联接操作。
在这里插入图片描述
在计算机科学中,这被称为一棵平衡树。但是,这并不是MySQL执行查询的方式。正如我们在前面章节中介绍的,MySQL总是从一个表开始,一直嵌套循环、回溯完成所有表联接。所以,MySQL的执行计划总是如图8-3所示,是一棵左侧深度优先的树。
在这里插入图片描述

联接查询优化器

MySQL查询优化器最重要的一部分就是联接查询优化器,它决定了多个表联接时的顺序。通常多表联接的时候,可以有多种不同的联接顺序来获得相同的执行结果。联接查询优化器通过评估不同顺序时的成本来选择一个成本最低的联接顺序。
下面的查询可以通过不同顺序的联接最后获得相同的结果:
在这里插入图片描述

很容易看出,可以通过一些不同的执行计划来完成上面的查询。例如,MySQL可以从film表开始,使用film_actor表的索引film_id来查找对应的actor_id值,然后再根据actor表的主键找到对应的记录。Oracle用户会用下面的术语描述:“film表作为驱动表先查找file_actor表,然后以此结果为驱动表再查找actor表”。这样做效率应该很高,我们再使用EXPLAIN看看MySQL将如何执行这个查询:
在这里插入图片描述
在这里插入图片描述
这和我们前面给出的执行计划完全不同。MySQL从actor表开始(从上面的EXPLAIN结果的第一行输出可以看出这一点),然后与我们前面的计划按照相反的顺序进行联接。这样是否效率更高呢?我们来看看。
我们先使用STRAIGHT_JOIN关键字,按照之前的顺序执行,下面是对应的EXPLAIN输出结果:
在这里插入图片描述
在这里插入图片描述
这说明了MySQL为什么要反转联接顺序:反转后可以使查询在第一个表中检查更少的行。在这两种情况下,都能够在第二个和第三个表中执行快速索引查找,不同的是,需要执行的索引查找次数不一样。将film表作为第一个表需要检查大约1000行记录(参见rows字段),每一行都是一个探针,用于针对film_actor和actor表进行索引查找。如果MySQL先扫描actor表,则只需要对后面的表进行200次索引查找。换句话说,反转联接顺序会让查询进行更少的回溯和重读操作。
这个简单的例子主要想说明MySQL是如何选择合适的联接顺序来让查询执行的成本尽可能低的。重新定义联接的顺序是优化器非常重要的一项功能。不过有的时候,优化器给出的并不是最优的联接顺序。这时可以使用STRAIGHT_JOIN关键字重写查询,让优化器按照你认为的最优的联接顺序执行——不过老实说,人的判断很难那么精准。绝大多数时候,优化器做出的选择都比普通人的判断要更准确。
联接优化器会尝试在所有的联接顺序中选择一个成本最低的来生成执行计划树。如果可能,优化器会遍历每一个表,然后逐个做嵌套循环,计算执行每一棵可能的计划树的成本,最后返回一个最优的执行计划。
不过,糟糕的是,n个表的联接可能有n的阶乘种联接顺序,我们称之为所有可能的查询计划的“搜索空间”。搜索空间的增长速度非常块,例如,若是10个表的联接,那么共有3628800种不同的联接顺序!当搜索空间非常大的时候,优化器不可能逐一评估每一种联接顺序的成本。这时,优化器选择使用“贪婪”搜索的方式查找“最优”的联接顺序。实际上,当需要联接的表超过optimizer_search_depth的限制的时候,就会选择“贪婪”搜索模式了(optimizer_search_depth参数可以根据需要指定大小)。
在MySQL这些年的发展过程中,优化器积累了很多“启发式”的优化策略来加速执行计划的生成。在绝大多数情况下这都是有效的,但因为不会去计算每一种联接顺序的成本,所以偶尔也会选择不是最优的执行计划。
有时查询不能重新排序,联接优化器可以利用这一点通过消除选择来减小搜索空间。左联接(LEFT JOIN)和相关子查询都是很好的例子(稍后将详细介绍子查询)。这是因为,一个表的结果依赖于另外一个表中检索的数据,这种依赖关系通常可以帮助联接优化器通过消除选择来减少搜索空间。

排序优化

无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者尽可能避免对大量数据进行排序。当不能使用索引生成排序结果的时候,MySQL需要自己进行排序,如果数据量小则在内存中进行,如果数据量大则需要使用磁盘,不过MySQL将这个过程统一称为文件排序(filesort),即使完全是在内存中排序不需要任何磁盘文件时也是如此。
如果需要排序的数据量小于“排序缓冲区”,MySQL使用内存进行快速排序操作。如果内存不够排序,那么MySQL会先将数据分块,对每个独立的块使用“快速排序”进行排序,并将各个块的排序结果存放在磁盘上,然后将各个排好序的块进行合并(merge),最后返回排序结果。

MySQL两种排序算法

两次传输排序(旧版本使用)

读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行。
这需要进行两次数据传输,即需要从数据表中读取两次数据,第二次读取数据的时候,因为是读取排序列进行排序后的所有记录,这会产生大量的随机I/O,所以两次传输排序的成本非常高。

单次传输排序(新版本使用)

先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果。
因为不再需要从数据表中读取两次数据,对于I/O密集型的应用来说,这样做的效率高了很多。另外,相比两次传输排序,这个算法只需要一次顺序I/O就可读取所有的数据,而无须任何的随机I/O。
然而,这种方式可能占用更多空间,因为会保存查询中每一行所需要的列,而不仅仅是进行排序操作所需要的列。这意味着更少的元组可以放入排序缓冲区,使得文件排序(filesort)操作必须执行更多的排序合并过程。

MySQL在进行文件排序时需要使用的临时存储空间可能会比想象的要大得多。原因在于MySQL在排序时,对每一个排序记录都会分配一个足够长的定长空间来存放。这个定长空间必须足够长才能容纳其中最长的字符串,例如,如果是VARCHAR列,则需要分配其完整长度;如果使用utf8mb4字符集,那么MySQL将会为每个字符预留4字节。我们曾经在一个库表结构设计不合理的案例中看到,排序消耗的临时空间比磁盘上的原表要大很多倍。
在联接查询的时候如果需要排序,MySQL会分两种情况来处理这样的文件排序。如果ORDER BY子句中的所有列都来自联接的第一个表,那么MySQL在联接处理第一个表的时候就进行文件排序。如果是这样,那么在MySQL的EXPLAIN结果中可以看到Extra字段会有“Using filesort”字样。除此之外的所有情况,MySQL都会先将联接的结果存放到一个临时表中,然后在所有的联接都结束后,再进行文件排序。在这种情况下,在MySQL的EXPLAIN结果的Extra字段可以看到“Using temporary;Using filesort”字样。如果查询中有LIMIT的话,LIMIT也会在文件排序之后应用,所以即使需要返回较少的数据,临时表和需要排序的数据量仍然会非常大。

MySQL查询优化器的局限性

MySQL所实现的查询执行方式并不是对每种查询都是最优的。不过还好,MySQL查询优化器只对少部分查询不适用,而且我们往往可以通过改写查询让MySQL高效地完成工作。

UNION的限制

有时,MySQL无法将限制条件从UNION的外层“下推”到内层,这使得原本能够限制部分返回结果的条件无法应用到内层查询的优化上。如果希望UNION的各个子句能够根据LIMIT只取部分结果集,或者希望能够先排好序再合并结果集的话,就需要在UNION的各个子句中分别使用这些子句。例如,想将两个子查询结果联合起来,然后再取前20条记录,那么MySQL会将两个表存放到同一个临时表中,然后再取出前20行记录:
在这里插入图片描述

这条查询将会把actor表中的200条记录和customer表中的599条记录存放在一个临时表中,然后再从临时表中取出前20条。可以通过在UNION的两个子查询中分别加上一个LIMIT 20来减少临时表中的数据:
在这里插入图片描述
现在临时表只包含40条记录了,除了考虑性能之外,在这里还需要注意一点:从临时表中取出数据的顺序并不是一定的,所以如果想获得正确的顺序,还需要在最后的LIMIT操作前加上一个全局的ORDER BY操作。

等值传递

某些时候,等值传递会带来一些意想不到的额外消耗。例如,考虑一列上的巨大IN()列表,优化器知道它将等于其他表中的一些列,这是由于WHERE、ON或USING子句使列彼此相等。
优化器通过将列表复制到所有相关表中的相应列来“共享”列表。通常,因为各个表新增了过滤条件,所以优化器可以更高效地从存储引擎过滤记录。但是如果这个列表非常大,则会导致优化和执行都会变慢。除了修改MySQL源代码,目前还没有什么办法能够绕过该问题(不过这个问题很少会碰到)。

并行执行

MySQL无法利用多核特性来并行执行查询。很多其他的关系数据库能够提供这个特性,但是MySQL做不到。这里特别指出是想告诉读者不要花时间去尝试寻找并行执行查询的方法。

在同一个表中查询和更新

MySQL不允许对一张表同时进行查询和更新。这其实并不是优化器的限制,如果你清楚MySQL是如何执行查询的,就可以避免这种情况。下面是一段无法运行的SQL语句,尽管这是一段符合标准的SQL语句。
这个查询会将表中每一行的c字段值更新为和该行的type字段值相同的行数量:
在这里插入图片描述
可以使用生成表的形式来绕过上面的限制,因为MySQL只会把这个表当作一个临时表来处理。实际上,这执行了两个查询:一个是子查询中的SELECT语句,另一个是多表UPDATE查询,其中包含原表和子查询的联接结果。子查询会在UPDATE语句打开表之前就完成,所以下面的查询将会正常执行:
在这里插入图片描述


  1. 例如,在联接操作中,范围检查的执行计划会针对每一行重新评估进行索引。可以通过EXPLAIN执行计划中的Extra列是否有“range checked for each record”来确认这一点。该执行计划还会增select_full_range_join这个服务器变量的值。 ↩︎

  2. 一部电影没有演员,是有点儿奇怪。不过在示例数据库Sakila中,影片Slacker Liaisons中就没有任何演员,它的描述是“鲨鱼和见过中国古代鳄鱼的学生的简短传说”。 ↩︎

  3. 可以通过查看MySQL手册中的“索引提示”“优化器提示”等章节来确认在某个具体的版本中支持哪些提示,以及如何使用这些提示。 ↩︎

  4. 如后文所述,MySQL的执行计划并不是这里描述的这么简单,过程中有非常多的细节优化,使得整个过程是一个非常复杂的过程。 ↩︎

  5. 可以通过在EXPLAIN语句中新增FORMAT=TREE关键字来查看树形结构。 ↩︎

  6. MySQL根据执行计划生成输出。这和原查询有完全相同的语义,但是查询语句可能并不完全相同。 ↩︎

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,这个任务我可以尝试帮你完成。不过在开始之前,我需要确认一下你的数据库已经创建好了,里面包含三张表:学生表、课程表、选课表。每张表的字段和数据类型也应该已经确定好了。 如果你的数据库还没有创建好,请先创建好数据库和表,并确保表中已经添加了一些示例数据。当然,为了保护你的数据安全,我不会在这里给出具体的数据库连接信息和密码,你需要自行修改为你的信息。 现在,让我们开始建造班级管理系统。 首先,我们需要创建一个 index.php 文件作为入口文件。在这个文件中,我们需要连接数据库,并建立三个类对应三张表:Student、Course、Selection。每个类中需要包含增删改查的方法,以及其他需要的方法。下面是示例代码: ```php <?php // 连接数据库 $servername = "localhost"; $username = "your_username"; $password = "your_password"; $database = "your_database"; $conn = new mysqli($servername, $username, $password, $database); if ($conn->connect_error) { die("Connection failed: " . $conn->connect_error); } // 学生类 class Student { private $id; private $name; private $age; private $gender; // 构造函数 function __construct($id, $name, $age, $gender) { $this->id = $id; $this->name = $name; $this->age = $age; $this->gender = $gender; } // 添加学生 function add() { global $conn; $sql = "INSERT INTO students (id, name, age, gender) VALUES ('$this->id', '$this->name', '$this->age', '$this->gender')"; if ($conn->query($sql) === TRUE) { echo "New student added successfully"; } else { echo "Error: " . $sql . "<br>" . $conn->error; } } // 修改学生信息 function update() { global $conn; $sql = "UPDATE students SET name='$this->name', age='$this->age', gender='$this->gender' WHERE id='$this->id'"; if ($conn->query($sql) === TRUE) { echo "Student updated successfully"; } else { echo "Error: " . $sql . "<br>" . $conn->error; } } // 删除学生 function delete() { global $conn; $sql = "DELETE FROM students WHERE id='$this->id'"; if ($conn->query($sql) === TRUE) { echo "Student deleted successfully"; } else { echo "Error: " . $sql . "<br>" . $conn->error; } } // 查询学生 function select() { global $conn; $sql = "SELECT * FROM students WHERE id='$this->id'"; $result = $conn->query($sql); if ($result->num_rows > 0) { $row = $result->fetch_assoc(); echo "id: " . $row["id"] . " - Name: " . $row["name"] . " - Age: " . $row["age"] . " - Gender: " . $row["gender"]; } else { echo "0 results"; } } } // 课程类 class Course { private $id; private $name; private $teacher; private $credit; // 构造函数 function __construct($id, $name, $teacher, $credit) { $this->id = $id; $this->name = $name; $this->teacher = $teacher; $this->credit = $credit; } // 添加课程 function add() { global $conn; $sql = "INSERT INTO courses (id, name, teacher, credit) VALUES ('$this->id', '$this->name', '$this->teacher', '$this->credit')"; if ($conn->query($sql) === TRUE) { echo "New course added successfully"; } else { echo "Error: " . $sql . "<br>" . $conn->error; } } // 修改课程信息 function update() { global $conn; $sql = "UPDATE courses SET name='$this->name', teacher='$this->teacher', credit='$this->credit' WHERE id='$this->id'"; if ($conn->query($sql) === TRUE) { echo "Course updated successfully"; } else { echo "Error: " . $sql . "<br>" . $conn->error; } } // 删除课程 function delete() { global $conn; $sql = "DELETE FROM courses WHERE id='$this->id'"; if ($conn->query($sql) === TRUE) { echo "Course deleted successfully"; } else { echo "Error: " . $sql . "<br>" . $conn->error; } } // 查询课程 function select() { global $conn; $sql = "SELECT * FROM courses WHERE id='$this->id'"; $result = $conn->query($sql); if ($result->num_rows > 0) { $row = $result->fetch_assoc(); echo "id: " . $row["id"] . " - Name: " . $row["name"] . " - Teacher: " . $row["teacher"] . " - Credit: " . $row["credit"]; } else { echo "0 results"; } } } // 选课类 class Selection { private $student_id; private $course_id; // 构造函数 function __construct($student_id, $course_id) { $this->student_id = $student_id; $this->course_id = $course_id; } // 选课 function add() { global $conn; $sql = "INSERT INTO selections (student_id, course_id) VALUES ('$this->student_id', '$this->course_id')"; if ($conn->query($sql) === TRUE) { echo "Selection added successfully"; } else { echo "Error: " . $sql . "<br>" . $conn->error; } } // 退课 function delete() { global $conn; $sql = "DELETE FROM selections WHERE student_id='$this->student_id' AND course_id='$this->course_id'"; if ($conn->query($sql) === TRUE) { echo "Selection deleted successfully"; } else { echo "Error: " . $sql . "<br>" . $conn->error; } } // 查询选课信息 function select() { global $conn; $sql = "SELECT * FROM selections WHERE student_id='$this->student_id'"; $result = $conn->query($sql); if ($result->num_rows > 0) { while ($row = $result->fetch_assoc()) { echo "Student id: " . $row["student_id"] . " - Course id: " . $row["course_id"] . "<br>"; } } else { echo "0 results"; } } } ?> ``` 上面的代码中,我们使用了 mysqli 连接数据库,并定义了三个类:Student、Course、Selection。每个类中都有一个构造函数,用于初始化类的属性;以及增删改查等方法。这些方法可以通过创建类的实例来使用。 接下来,我们可以在 index.php 中添加一些简单的测试代码,来测试我们的班级管理系统是否正常工作。以下是示例代码: ```php <?php // 添加学生 $student = new Student("1001", "张三", 18, "男"); $student->add(); // 修改学生信息 $student = new Student("1001", "张三", 20, "男"); $student->update(); // 删除学生 $student = new Student("1001", "", 0, ""); $student->delete(); // 查询学生 $student = new Student("1002", "", 0, ""); $student->select(); // 添加课程 $course = new Course("C001", "PHP开发", "张老师", 3); $course->add(); // 修改课程信息 $course = new Course("C001", "PHP开发", "李老师", 4); $course->update(); // 删除课程 $course = new Course("C001", "", "", 0); $course->delete(); // 查询课程 $course = new Course("C002", "", "", 0); $course->select(); // 选课 $selection = new Selection("1002", "C002"); $selection->add(); // 退课 $selection = new Selection("1002", "C002"); $selection->delete(); // 查询选课信息 $selection = new Selection("1002", ""); $selection->select(); ?> ``` 上面的代码中,我们分别测试了添加学生、修改学生信息、删除学生、查询学生、添加课程、修改课程信息、删除课程、查询课程、选课、退课、查询选课信息等操作。 当然,上面的代码只是一个示例,实际使用时还需要进行优化和改进。比如,可以将增删改查等方法封装成一个公共类,然后在每个类中继承这个公共类,这样可以避免重复代码。另外,还需要考虑数据安全、用户权限、界面设计等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值