MySQL入门

本章主要介绍MySQL关系数据库管理系统(Relational Database Management System,RDBMS)和MySQL所使用的结构化查询语言(Structured Query Language,SQL)。其中,列出了应该掌握的基本术语和概念,描述了示例所要用到的样本数据库sampdb,并且提供了一个用于展示如何使用`MySQL创建数据库并与之进行交互操作的教程。

如果你对数据库系统还不甚了解,或者还不是很肯定自己是否需要学习它,甚至还不肯定是否需要使用它,那么请从本章开始。如果你对MySQL和SQL还一无所知,并且需要一个入门指南,那么也请从本章开始。对MySQL或其他数据库系统已有一定经验的读者,则可以略读一下本章的内容。不过,为了你能对全书所用那个数据库sampdb的目的和内容更熟悉,希望可以阅读一下1.2节。

1.1 MySQL的用途

本节描述的是MySQL数据库系统的用途,具体描述了“MySQL可以干什么”,以及“它如何才能给你提供帮助”。如果你已经明白数据库的用途(也许你正在思考某个问题,到这里来只是想要找出“如何使用MySQL来解决它”的答案),那么可以直接翻阅到1.2节。

数据库系统在本质上是一种高效的管理大量列表信息的办法。这些信息的来源可能多种多样。它可能是研究数据、业务记录、客户需求、体育统计、销售报告、个人信息、人事档案、bug报告或者学生成绩。发挥数据库系统强大作用的时机在于:想要组织和管理的信息非常庞大或复杂,以至于所有记录采用手工处理会变得异常繁重。对于每天处理上百万条事务的大公司来说,数据库是必不可少的。不过,即使只有一个人的小公司,也可能会维护很多的信息,甚至多到需要用一个数据库来管理它。假设有下面这些情况。

  • 你在牙科诊所工作,在那里需要管理好患者的跟踪记录:何人何时到访、做了些什么、下次预约信息、保险信息等。
  • 你收集了多年的研究数据,为了发表而需要对它们进行分析。你要从大量原始数据里提炼出摘要性的信息,并取出选中的观察子集进行详细的统计分析。
  • 你是一名教师,需要跟踪学生的成绩和考勤。每次考试结束,你都需要记录每一个学生的分数。虽然将成绩记录到成绩册上很简单,但之后的成绩分析却很繁琐。你很想避免为了确定分数曲线,而对每次考试的分数进行排序;也真的很不愿意在期末时为了确定最终成绩,而把每个学生的成绩都加起来。统计每个学生的考勤也很无趣。
  • 你在某个组织机构(可能是一个专业团体、一个俱乐部、一个交响乐团或者一个健身俱乐部)担任秘书一职,具体负责维护机构成员名录的工作。你每年都要为所有成员生成一份打印名录,名录是用文字处理软件管理的,每当成员资料有变化时你都得编辑更新。你非常厌倦这种维护名录的方式,因为它限制了你的发挥,主要表现在:很难对名录条目按不同方式进行排序;无法轻松地选到每一条目的指定部分,如列出人名和电话号码;更不能轻松地找出一组的成员,如需要尽快更新成员资格的成员。如果有办法,就可以省却每月通过翻阅这些条目来找出那些需要更新成员资格的成员的工作。你听说过“无纸化办公”,知道它是电子化记录发展的结果,但你还未见过它所带来的任何好处。虽然成员资格记录是电子化的,但具有讽刺意味的是,它们记录的形式除了能将名录打印成纸质的以外,很难用作他途!

上面这些场景所涉及的信息量有大有小。但它们都有一个共同的特点,即这些工作都可以通过手工来完成,但使用数据库系统来管理会更高效。

在使用像MySQL这样的数据库系统时,你希望从中获得哪些具体好处呢?这取决于你的特殊需要和需求,并且,如同上面示例所示,具体的好处都各不相同。不过,在一般情况下,适合于用数据库管理系统来处理任务的人群是那些不使用数据库管理系统就要使用文件柜的人。事实上,数据库系统就像一个内置了复杂文件系统的巨大文件柜。与手工管理记录的方式相比,以电子化方式来管理记录存在有许多优势。下面来看看前面描述过的那个牙科诊所的场景。在用于管理患者记录的文档系统能力方面,MySQL可以为你带来下面这些帮助。

(1)缩短记录归档时间。你不用在文件柜里挨个拉抽屉找存放新记录的地方。你只需将它提交给MySQL,MySQL会为你找到正确的存放这条记录地方。

(2)缩短记录检索时间。当进行记录查找时,不必为了想要找到的记录而亲自动手去逐个搜索。为了给那些最近一段时间没来做检查的患者发个提醒信息,你可以让MySQL来帮你找出这些记录。当然,这与让你告诉另一个人“请帮忙确认一下最近6个月哪些患者没来参加检查”的情况有所不同。事实上,你“念出的”是一段奇怪的“咒语”:

SELECT last_name, first_name, last_visit FROM patient  
WHERE last_visit < DATE_SUB(CURDATE(), INTERVAL 6 MONTH);

如果你以前从没见过类似的内容,那么它初看起来可能会相当吓人。不过,它的效果却是相当吸引人:你不用再花费一小时来翻看你的记录,只需一两秒即可得到想要的结果。不管怎样,无需多久你就会习惯这种奇形怪状的表达。等阅读完本章,你便会明白它真正的含义。

(3)灵活的检索排序。不用严格按照记录存储的顺序(如按患者的名字排序)来检索它们。MySQL可以按任何你想要的顺序来提取记录,如按名字、保险公司名字、上次就诊时间等。

(4)灵活的输出格式。在找到感兴趣的记录之后,你无需手动复制这些信息,MySQL会为你生成一个列表。有时,你可能只是想打印这些信息;有时,你可能会想在另一个程序里使用它们。例如,在生成了最近逾期未能复诊的患者名单后,你便可以把这些资料输送到某个文字处理软件,让它打印出你想要发送给那些患者的通知单。或许,你只是对如同“选中记录统计”这样的汇总信息比较感兴趣。你不用亲自来统计记录,MySQL会替你生成汇总信息。

(5)多用户同时访问记录。对于纸质记录,如果有两个人同时想要查看某个记录,那么其中一个人就必须等待另一个人将这个记录归还之后才能查看。而MySQL则可以允许这两个人同时访问这个记录。

(6)记录的远程访问与电子传输。想用纸质记录,就得亲自跑到存放它们的地方,或者让人将它们复印之后再发送给你。电子记录则为远程访问记录或者电子传输记录提供了可能。如果你的牙科集团设有许多分支机构,那么这些机构里的人员便可以在当地访问到你的资料。你完全不用再通过快递来传送这些副本。如果有人需要记录,但却又没有与你一样的数据库软件,那么你可以选择那些所需的记录,将其中的内容通过网络发送给他。

如果你曾经用过数据库管理系统,那么肯定对刚才描述的种种好处深有体会,而且可能也在思索如何才能超越常规“代替文件柜”应用程序的限制。有许多组织机构将数据库与网站结合在一起使用,这就是一种很好的方式。假设贵公司有一个商品库存数据库,每当顾客打电话过来询问仓库里是否有货、其价格是多少时,服务台的员工便会用到它。这是一种比较传统的数据库使用方式。不过,如果贵公司搭建一个供顾客访问的网站,那么就可以提供一项新的服务:创建一个搜索页面,让顾客可以查看条目,从而确定价格、可用性以及商品的库存情况。如果支持在线订购,那么顾客甚至不用离开家门便能购买到你的产品。这让顾客获得了他们想要的信息,而数据库却是根据提问自动搜索商品库存信息来提供这些内容的。顾客立即获得了所关心的信息,不用一边听着恼人的录音,一边傻等着;也不用受限于服务台的上下班时间。并且,每当有一位顾客使用贵公司网站,就意味着会少一个电话,而这是需要由一个在服务台拿工资的人来处理的事情。如此看来,该网站或许可以为自己买单。

不过,你还可以更进一步地发挥数据库的作用。基于Web的库存搜索请求,不仅可以把信息提供给顾客,也能提供给贵公司。这些查询可以让你了解到顾客正在查找哪些商品,而查询结果则会让你知道是否能够满足他们的需求。在某种程度上,如果你没有顾客想要的东西,那么你可能会错失这笔生意。因此,记录下库存搜索信息非常有意义,通过它可以了解到:顾客正在找寻什么商品,而你是否还有存货。接着,就可以根据这些信息来调整库存,并向顾客提供更好的服务。

说了半天,那么MySQL是如何工作的呢?找到答案的最好方式就是自己动手体验一下。为此,我们需要有一个可以操作的数据库。


1.2 示例数据库

本节将描述本书所使用的示例数据库。在你学习使用MySQL的过程中,可以用这个数据库提供的示例进行尝试,该数据库是针对之前描述过的两种情况设计的。

  • “机构秘书”场景。该机构有一些特点:其成员对美国历史很感兴趣(因没有一个更好的名字,所以姑且称其为“美史联盟”)。所有成员都要定期缴纳一定的费用以维持其成员资格。缴纳的费用会用于一些正常开支,如出版通讯——《Chronicles of U.S. Past 》(美国编年史)。该联盟运营着一个小型网站,但还未被充分开发出来,而你很想要扭转这一局面。
  • “成绩考评”场景。你是一名教师,要负责考评期间的各种考试与测验,记录分数和打分。之后,你要确定出最终成绩,并把它们随同考勤情况一同上交到学校办公室。

下面,我们进一步分析一下这两个场景的需求。

  • 你必须决定数据库里的哪些内容是你所想要的——即那些你要实现的目标。
  • 你必须决定需要将哪些内容放入数据库——即你想要跟踪的数据是什么。

在考虑“将什么放入数据库”之前考虑“要从数据库里获得什么样的内容”,这似乎有些本末倒置。毕竟,大家都是这么认为的:要先输入数据,然后才能检索它。不过,你使用数据库的方式取决于你的目标是什么。而且,与想要放入数据库的内容相比,想要从数据库检索的内容则与那些目标的关系更加密切。只有在计划今后使用这些信息之后,你才会想要花费时间和精力把它们放入数据库。

1.2.1 美史联盟项目

该项目的场景是:你是联盟秘书,眼下正使用某个文字处理文档维护那份成员资格名单。对于像“生成打印名录”这样的事情,处理起来当然没什么问题。但是,当你想要获得其他更多信息时,就会受到限制。你还有几个目标要实现。

  • 你想要以不同格式输出名录,同时按照不同的应用程序来定制信息。有一个目标是每年生成打印好的名录——这是联盟一直以来的一个需求,要继续实施下去。你可能还会想到名录信息的其他用途,例如向出席联盟年会的人员提供一份打印好的最新成员名单。这两个应用程序涉及的信息是不同的。打印名录程序需要用到每个成员条目的所有内容。而年会程序则只需要提取出成员的姓名即可(使用文字处理软件无法轻松完成这项工作)。
  • 你想要在名录里搜索出满足不同条件成员。例如,你想要知道近期有哪些成员需要更新成员资格。你还需要另一个搜索相关的应用程序,用来维护每个成员各自的关键字列表。这些关键字描述了所有成员都特别感兴趣的一些美国历史时期,如南北战争(Civil War)、经济大萧条(Depression)、民权法案(civil right)、托马斯·杰斐逊(Thomas Jefferson)总统的生平事迹。有时,有些成员会要求你为他们提供一份与其志趣相投的其他成员名单,而你也很愿意满足这些需求。
  • 你想要将名录发布到联盟网站上去。这样做能让你和所有成员都受益。如果你能通过某些自动化的过程将名录转换为Web页面,那么名录的在线版本将总能保持最新,而这是纸质形式无法做到的。如果在线名录能支持搜索功能,那么成员们便能轻松地自行查找信息。例如,如果某成员想要知道其他还有谁对“南北战争”感兴趣,那么他便可以自行查找,完全用不着等你帮他搜索,而你也不用抽时间去处理这件事情。

数据库不是世界上最令人兴奋的工具,因此我不会鼓吹像“使用数据库能激发人的创造性思维”这样不切实际的话。不过,如果你不再把信息看成是一种累赘(如同你在使用文字处理文档时所想的那样),而是把它想成是可相对轻松处理的事情(如同你希望使用MySQL来处理一样),那么你自然会释放出自己的潜能,找到更多新的使用这些信息的方法。

  • 如果数据库中的信息能够以在线名录的形式放到网站上去,那么你也可以让这些信息以其他方式流转。例如,让成员能够在线修改他们自己的资料,并更新到数据库。这样,你就不用自己负责所有的编辑工作,而且这也能让名录里的信息更加准确。
  • 如果你把电子邮件地址也存储到数据库里,那么你就可以利用它们来给成员发送电子邮件,提醒他们及时更新自己的资料。邮件内容可以显示出成员的当前资料,请成员们进行检查,并提示他们如何使用网站提供的功能完成必要的修改。
  • 数据库还可以在很多方面拓展联盟网站的用途,而并不仅限于成员资格列表。联盟通讯《Chronicles of U.S. Past》有一个儿童专栏,其中每一期都会包含一个历史知识测验。最近几期的重点是美国总统的传记。联盟网站上也可以设置一个儿童专区,把那些测验题目放在上面。或许这个专区还可以弄成互动的,比如将做过的测验信息放入数据库,让Web服务器在数据库里查询问题的答案,然后呈现给访客。

好了!此时此刻,你所想到的数据库应用数量可能让你有些忘乎所以了。在稍息片刻之后,重新回到现实,你开始问一些比较实际的问题,例如:

  • 是不是想太多了?实现起来工作量会很大吧?

当然,所有事情只想不做都会变得很容易,而这些想法对于具体实现根本不重要。不过,在读完本书之后,你就能实现我们刚才罗列出来的所有需求。但是请记住,没有必要一下子完成所有需求。要将整个工作分解成若干需求,一次只解决一个需求。

  • MySQL能实现所有这些目标吗?

不,它不能,至少单靠它是不行。例如,MySQL没有内置Web程序开发工具。不过,你可以将它与其他工具相结合,借助它们来补充和扩展其功能。

我们将使用脚本语言Perl和它的数据库接口(Database Interface,DBI)模块来编写访问MySQL数据库的脚本程序。Perl语言有着强大的文本处理能力,它能以极其灵活的方式对数据库的查询结果进行处理,并产生各式各样的输出。例如,我们可以用Perl语言生成一份富文本格式(Rich Text Format,RTF)格式的成员名录,该格式可供各种文字处理软件读取;另外,还可以生成适用于Web浏览器的HTML格式的名录。

我们还要用到另一种脚本语言PHP。PHP语言特别适用于编写Web应用程序,并且它也易与数据库进行交互。有了它,你就能够通过Web页面来启动MySQL查询,然后生成新的包含数据库查询结果的页面。有很多支持PHP语言的Web服务器(其中包括世界上最流行的Web服务器Apache),因此像“呈现一个搜索表单,并显示搜索结果”这样的事情,对它来说就是“小菜一碟”。

MySQL与这些工具可以很好地集成在一起,因此你可以自由地选择组合方式,以便达到你心中设定的目标。别太相信那些一体化的套装组件,它们通常都大肆鼓吹自己具有“集成”功能,但是它们只有彼此配合才能更好地发挥作用。

  • 最后,还有一个很重要的问题:总计要花费多少钱?毕竟联盟的预算是有限的。

答案可能令人难以置信,但实际上它可能不会有任何成本。如果你对数据库系统有所了解,那么你应该知道它们通常都很昂贵。相比之下,MySQL通常是可免费使用的。即便是在需要有技术支持和维护承诺保证的企业环境里,将MySQL作为数据库系统,其成本也是很低的。(想了解更多详情,请访问www.mysql.com。)我们将用到的其他工具(其中包括Perl、DBI、PHP和Apache)都是免费的。因此,综合考虑一下,你完全能够以相当低的成本组建一个有用的系统。

用于开发数据库的操作系统则由你来选择。几乎所有我们将讨论的软件都能在UNIX(它指代了BSD UNIX、Linux、Mac OS X等)和Windows上运行。极少数的例外情况一般是UNIX或Windows特有的shell脚本或批处理脚本。

1.2.2 成绩考评项目

现在一起来看另一个要使用示例数据库的情况。该项目的场景是:你是一名负责成绩考评的教师。你想将成绩处理工作,从使用成绩册的手工操作方式转换成使用MySQL的电子表示方式。在这种情况下,从数据库获取信息的方式隐含在你目前使用成绩册的方式中。

  • 对于每一次测验或考试,你都要记录分数。如果是考试,你还需要对分数进行排序,以便查看它们并确定每一个字母成绩(包括A、B、C、D和F)所代表的界线。
  • 在期末,你需要把计算出每一个学生的总分数,对这些总分数进行排序,并以此为基础确定出成绩。总分数可能需要加权计算,因为你可能需要让考试比测验的权重更大。
  • 在期末,你还要向学校办公室提供学生的考勤情况。

最终目标是要避免手动排序、避免手动汇总分数和考勤情况。换句话说,你希望MySQL可以对分数排序,并在期末完成所有与每个学生的总分和缺席次数相关的计算。为实现这些目标,你需要班里学生的名单、每次考试和测验的分数,以及全部学生的缺勤日期。

1.2.3 如何运用示例数据库

如果你对这里的“美史联盟”和“成绩考评”两个项目都不感兴趣,那么你可能会想“还有什么场景一定适合你呢”。答案就是“无穷无尽”。其实,这两个项目可以说明你用MySQL和与之相关的工具可以做什么事情。稍微想象一下,你便能看到示例数据库查询是如何应用于

你所要解决的那些特定问题的。假设你正好就在我前面提到的那个牙科诊所里工作。虽然在这本书里不会看到很多与牙科学有关的查询,但是会看到这里所发现的很多查询都可以应用到患者记录维护、办公记载等那些工作中。例如,“确定联盟里哪些成员需要更新他们的成员资格”与“确定最近哪些患者未来复诊”这两个任务便非常相似。它们都是基于日期的查询,因此只要你学会了“成员资格更新”的查询,那么便可以将该技巧用于编写“久未复诊患者”的查询,从而带来更多的收获。


1.3 基本数据库术语

你可能已经注意到了,尽管这是一本关于数据库的书籍,但到目前为止,你还未遇到过多少晦涩难懂的专业技术术语。事实上,尽管我们对如何使用示例数据库有过大致描述,但关于数据库到底是什么样子,我却只字未提。可是,既然我们要设计数据库,并实现它,那么我们就不能再避而不谈有关的术语了。这正是本节的主要内容。本节所描述的术语都是本书要用到的,因此希望大家能够熟悉它们。庆幸的是,关系数据库的许多概念都很简单。人们之所以喜欢关系数据库,很大程度上便是因为其基本概念都简明易懂。

1.3.1 结构术语

在数据库领域,MySQL被划分为关系数据库管理系统(RDBMS)。我们下面来拆解一下。

  • 数据库(DataBase,即RDBMS里的DB)是一个用来存储信息的仓库,它的结构简单、规则。

    • 数据库里的数据集都被组织成表(table)。

    • 每个表由多个行(row)和列(column)组成。

    • 表中的每一行称为一条记录(record)。

    • 记录可以包含多项信息;表里的每一列对应于其中的一项。

  • 管理系统(Management System,即RDBMS里的MS)是一个软件,我们可以通过它来插入(insert)、检索(retrieve)、修改(modify)或删除(delete)记录。

  • 关系(Relational,即RDBMS里的R)一词表示这是一种特殊的DBMS,其长处在于通过查找两个表里的共同元素,将分别存放于两个表里的信息联系(即匹配)起来。RDBMS的强大之处在于:它能方便地将这些表里的数据提取出来,并把相关表里的信息结合起来生成答案,回答那些只靠单个表无法回答的问题。(事实上,“关系”的正式定义与我在本书中用它的方式有所不同。为此,我先向那些纯粹主义者道歉。不过,我的定义更有助于表达出RDBMS的用途。)

关系数据库是如何把数据组织到表里的呢?又是如何把不同表的信息关联在一起的呢?下面来看一个例子。假设你在经营一个网站,它有一项横幅广告服务。你与多家想要刊登广告的公司签订了合同,满足它们想要在人们访问你网站页面时显示其广告的需要。每当有访客点击其中的某个页面时,你就提供一个嵌有广告的页面发送给访客浏览器,这样你就能从刊登这条广告的公司那里获得一点费用。这就是所谓的广告“点击”。为了表示这些信息,你需要用到3个表(见图1-1)。第1个company表由这样几列构成:公司名称、编号、地址和电话号码。第2个ad表的构成列有:广告编号、拥有该广告的那家公司的编号,以及每点击一次的收费数目。第3个hit表`需要记录广告点击量和广告点击日期。

图1-1 横幅广告的表

有些问题只用一个表便能回答。例如,想要知道与你签订了合同的公司有多少家,那么你只需统计一下company表共有多少行即可。同样地,想要了解在某段指定的时间里有多少点击量,则只需检查表hit即可。其他有些问题则可能比较复杂,需要查询多个表才能得到答案。例如,想要知道在7月14日那天Pickles公司(Pickles,Inc.)的每一条广告分别被点击了多少次,那么就需要像下面那样使用所有的3个表。

(1)在company表里查找公司名称(Pickles,Inc.),从而查出该公司的编号(14)。

(2)在ad表里,利用这个公司编号找出与之匹配的行,从而可以确定出所有相关广告的编号。最后找到两条广告,即编号48和101。

(3)对于在ad表里匹配到的每一行,使用该行里的广告编号在hit表里找出日期介于给定日期范围内的所有匹配行;然后,再统计这些匹配行的数量。最后查询出的结果是:48广告有3个匹配;101广告有2个匹配。

这些听起来好像很复杂!但这正是关系数据库系统最擅长做的事情。这种复杂性只是表面现象,因为刚才描述的每一步骤都是一个简单的匹配操作:将一个表的行值与另一个表的行值,通过匹配关联起来。这种简单的操作可以有多种变化,用于回答各式各样的问题,如各家公司分别投放了多少个不同的广告?哪家公司的广告最受欢迎?每个广告带来的收益是多少?在当前结算期内,各家公司应该支付你多少广告费?

有了之前讲述的这些关系数据库理论,你就能读懂本书后续的内容了;我们不必了解“第三范式”(Third Normal Form)、“实体联系图”(Entity-Relationship Diagram)等这些枯燥乏味的内容。(如果想了解它们,请阅读C. J. Date或E. F. Codd的著作。)

1.3.2 查询语言术语

为了与MySQL交互,需要使用一种名为SQL的语言。所有主流数据库系统都支持SQL,但各个服务商的实现都各不相同。SQL支持许多不同的语句,可以让你用非常有趣和实用的方式与数据库进行交互。

与其他计算机语言一样,初次接触SQL的人往往会觉得它很奇怪。例如,在创建表时,你必须告诉MySQL该表的结构是什么样子的。很多人都会想到把表与图表或图片联系起来。但是MySQL不是这样子的,因此在创建表时,你必须告知MySQL一些类似下面那样的内容:

CREATE TABLE company  
(  
  company_name VARCHAR(30),  
  company_num  INT,  
  address      VARCHAR(30),  
  phone        VARCHAR(12)  
);

如果对SQL还不太熟悉,那么可能会对这样的语句心生畏惧。不过请放心,你不用成为程序员就能学会如何熟练地使用SQL。随着对SQL的深入了解,你对CREATE TABLE的看法也会悄然地发生变化——它不再是一组怪诞的表达,而是一种有助于信息描述的强大工具。

1.3.3 MySQL的体系结构术语

当使用MySQL的时候,你实际上至少会用到两个程序,因为MySQL采用的是“客户端/服务器”体系结构。有一个程序是MySQL服务器,即mysqld。它运行在存放数据库的那台机器上,主要负责监听网络上的客户端请求,并根据这些请求去访问数据库内容,然后向客户端提供它们所查询的信息。另一个程序是MySQL客户端,它主要负责连接到数据库服务器,并发起查询,以便将自己想要的信息告知服务器。

MySQL的大部分发行版本都包括数据库服务器程序和客户端程序。(在Linux下使用RPM包时,会有一个单独的服务器RPM包和客户端RPM包,因此应该安装两种包。)请根据自己的实际情况使用合适的客户端程序。最常用的客户端程序是mysql,它是一个交互式客户端程序,你可以用它来发起查询并查看结果。另外还有两个主要用于管理的客户端程序,即mysqldump和mysqladmin。前者主要用来把表的内容导出到文件里;后者主要用来检查数据库服务器的工作状态,以及执行一些数据库管理相关的任务,如通知服务器关闭。MySQL发行版本里还包括一些其他的客户端程序。此外,MySQL还提供了一个客户端开发库,如果它自带的标准客户端程序无法满足你的应用需求,你可以利用这个库来编写自己的程序。这个开发库可以被C语言程序直接使用。如果你不习惯C语言,那么也可以选择适用于其他语言(如Perl、PHP、Python、Java和Ruby)的编程接口。

本书讨论的客户端程序都是基于命令行的。你也可以试试MySQL工作台(MySQL Workbench),这是一个基于图形用户界面(Graphical User Interface,GUI)的工具,提供了即点即击(point-
and-click)功能。关于此工具的更多信息,请访问http://www.mysql.com/products/
tools/。

MySQL的“客户端/服务器”体系结构有以下几个好处。

  • 服务器强制执行并发控制,可以防止两个用户同时修改同一条记录。所有客户端请求都要经过服务器,因此服务器会负责安排处理它们的先后顺序。即使出现多个客户端同时访问同一个表的情况,它们也不用先找到对方进行协商。它们只需把自己的请求发往服务器,然后由服务器来决定执行这些请求的顺序。
  • 不是只有在数据库所在的那台机器上才能登录。MySQL工作在网络环境里,因此你可以在任意地方运行MySQL客户端程序,它都能够通过网络连接到服务器。距离不是问题!你可以在世界的任何地方访问服务器。例如,你的服务器位于澳大利亚,那么就算你带着笔记本电脑旅行到了冰岛,你也仍然可以访问自己的数据库。这是否意味着任何人都能够通过互联网看到你的数据呢?答案是“不能”。MySQL有一套灵活的安全机制,你可以设定只有得到授权的人才能访问。此外,你还可以进一步限制这些人的操作。例如,财务部的Sally应该有查看和更新(修改)记录的权限;而服务台的Phil却只应该有查看记录的权限。总之,你可以把这种访问权限控制细化到每一个人。如果你只想运行一个自属的系统,那么你完全可以把访问权限设置为只允许服务器上的客户端程序进行连接。

MySQL与mysql之间的差异

为避免混淆,我们使用MySQL指代完整的MySQL RDBMS,而mysql指代特定客户端程序的名字。虽然它们的发音相同,但我们通过字母大小写和字体差异对它们进行了区分。

说到发音,MySQL被读作“my-ess-queue-ell”。具体细节可以在MySQL参考手册(MySQL Reference Manual)里查阅到。此外,SQL的读法有“sequel”和“ess-queue-ell”两种,具体是哪种由读它的人决定。本书假定SQL的发音为“ess-queue-ell”。


1.4 MySQL教程

现在你已具备了所有的预备知识。下面来实际操作一下MySQL。

本节提供了一个带有多个示例的教程,其目的在于帮助你熟悉MySQL。主要内容包括创建一个示例数据库和多个表,通过对这些表里的信息执行插入、检索、删除和修改操作,练习与示例数据库的交互。在此过程中,你将学会以下内容。

  • 了解MySQL所能理解的SQL基础知识。(MySQL与其他RDBMS所使用的SQL有所不同,因此你最好也能快速浏览一下本节的内容,从而确认一下MySQL的SQL实现与你熟悉的版本是否存在差异。)
  • 了解MySQL自带的标准客户端程序是如何与MySQL服务器进行通信的。前一节讲过,MySQL采用的是“客户端/服务器”体系结构。其中,服务器运行在数据库所在的机器上;而客户端则是通过网络连接到服务器。本教程主要依赖于客户端程序mysql,它首先读取你输入的SQL查询语句,把它们发送到服务器执行,然后把执行结果显示在你面前。客户端mysql可以在MySQL所支持的所有平台上运行,并且提供了与服务器进行交互的最直接的方式。不过根据需要,有些示例会使用mysqlimport或者mysqlshow来代替。

本书将示例数据库命名为sampdb。如果在你的系统上已经有人占用了这个名字,或者你的MySQL管理员为你分配了另一个名字,那么你需要换一个名字。不管是何种情况,你都需要把本书示例中的sampdb替换为你实际使用的数据库名称。

即使你的系统里有多个用户,且他们都各自拥有自己的示例数据库,本节所有示例里的表名也都可以直接使用。在MySQL里,只要每个人都用自己的数据库,那么大家完全可以使用相同的表名,不会有任何问题。MySQL将表限制在各自的数据库里,防止了相互干扰。

1.4.1 获取示例数据库发行包

本教程在好几个地方都要用到“示例数据库发行包”( 也称作“sampdb数据库的发行版”)。该发行包里包含了许多用来安装示例数据库的查询语句和数据。发行包的获取办法和安装步骤可以在附录A里查到。在发行包解压之后,它会自动创建一个名为sampdb的子目录,其中包含了你所需要的许多文件。顺便提个建议:每次你操作数据库里的示例时,最好都能切换到一个新的目录。

如果想要在任何目录里都可以方便地运行MySQL程序,那么你最好能把位于MySQL目录下包含着这些程序的bin目录,添加到命令解释器的搜索路径里。具体操作方法是,参考本书A.3.3节,把该目录的路径名称添加到环境变量PATH里

1.4.2 基本配置要求

为练习本教程里的各个示例,除获得示例数据库发行包外,还必须满足以下几项基本要求。

  • 必须安装MySQL软件。
  • 要有一个能够连接数据库服务器的MySQL账号。
  • 要有一个用来操作的数据库。

所需的软件包括MySQL客户端和MySQL服务器。客户端程序必须安装在你自己要用的机器里。服务器可以选择安装在你的机器上,这点不强求。只要你有权限连接它,那么服务器可以位于任何地方。如果你的因特网服务提供商(Internet Service Provider,ISP)提供了MySQL服务,那么也可以申请使用它。想要自己获得和安装MySQL,请参考附录A。

除了MySQL软件,还必须要有一个MySQL账户,这样才能连接到服务器,并创建示例数据库及其表。(如果你已有MySQL账户,则可以直接用它。不过,建议你另外建立一个专用于本书学习的账户。)

此时此刻,我们遇到了一个“是先有鸡,还是先有蛋”的难题:为了建立一个用于连接服务器的MySQL账户,你必须要先连接到该服务器。通常情况下,你需要在运行MySQL服务器的主机上,以root用户身份登录,然后执行CREATE USER语句GRANT语句来创建新的MySQL账户,并为其分配数据库权限。如果你是在自己的机器上安装了MySQL服务器,并且正在运行,那么你可以像下面那样用root身份连接服务器,然后创建一个新的示例数据库管理员账户,其用户名为sampadm,密码为secret。你也可以把其中的名字和密码更改成自己想用的,但在本书后面用到它们的地方也要做相应的修改。

% **mysql -p -u root  
**Enter password: ******  
mysql> **CREATE USER 'sampadm'@'localhost' IDENTIFIED BY 'secret';  
**Query OK, 0 rows affected (0.04 sec)  
mysql> **GRANT ALL ON sampdb.* TO 'sampadm'@'localhost';  
**Query OK, 0 rows affected (0.01 sec)

命令mysql有一个选项-p,它能让mysql提示输入root用户的MySQL密码。如上例所示,输入的密码会被显示为一串星号,即**。这里假设你已经为MySQL的root用户设置了密码。如果你还未设置,则请在Enter Password:提示出现后直接按回车(Enter)键。不过,如果root用户无密码,那么这是个很大的安全漏洞,你应该尽快给它设置一个。想要了解更多关于CREATE USER语句、GRANT语句、设置MySQL用户账户以及修改密码的信息,请参考第13章。

在创建sampadm账户之后,请输入quit,并按回车键,然后退出mysql``程序``。

刚才展示那些语句适合于在运行服务器的那台机器上连接MySQL。它们让你可以通过用户名sampadm和密码secret来连接服务器,并让你拥有完全访问sampdb数据库的权限。不过,GRANT语句并不会创建数据库(你可以在创建数据库之前为它分配权限)。我们稍后会讨论与数据库创建相关的操作。

如果你打算通过网络从另一台主机连接到MySQL服务器,那么需要把示例中的localhost更改为你正使用的那台主机的名字。例如,你想要从主机boa.example.com连接到MySQL服务器,那么相应的语句则应类似这样:

mysql> **CREATE USER 'sampadm'@'boa.example.com' IDENTIFIED BY 'secret';  
**mysql> **GRANT ALL ON sampdb.* TO 'sampadm'@'boa.example.com';**

如果你对服务器没有控制权限,无法创建用户,那么请求助MySQL管理员,让他为你建立一个账户。然后,将本书各示例里的samp admsecretsampdb分别替换为管理员分配给你的用户名、密码和数据库名。

1.4.3 建立和断开MySQL服务器连接

为连接到MySQL服务器,请在命令提示符(即Unix系统的shell提示符,或者Windows下的控制台窗口提示符)里调用mysql程序。命令如下:

% **mysql**options

本书使用%来表示命令提示符。它是Unix系统的其中一个标准提示符;另一个是$。在Windows系统里,你所看到的提示符有点像C:\&gt;。当输入这些示例里所示的命令时,请不要输入提示符本身。

mysql命令行里的options部分可以为空。但下面这种命令形式更为常见:

% **mysql -h**host_name**-p -u**user_name

在执行mysql时,不用提供所有的选项,但通常至少要指定用户名和密码。下面是全部选项的具体含义。

  • -h host_name(另一种形式是:--host=host_name

指定运行MySQL服务器的那台主机。如果它与你运行mysql程序的那台机器相同,就可以省略此选项。

  • -u user_name(另一种形式是:--user=user_name

指定MySQL用户名。如果你使用的是Unix系统,并且你的MySQL用户名与登录名完全一样,就可以省略此选项。mysql将自动将你的登录名当作MySQL用户名。

在Windows系统上,默认用户名为ODBC,它有可能无法使用。你可以在命令行通过-u选项来指定,也可以通过设置环境变量USER来添加一个默认用户名。例如,你可以用下面的set命令来指定一个用户名sampadm

C:\>** set USER=sampadm**

如果你通过控制面板(Control Panel)里的系统(System)项目设置环境变量USER,那么该设置对每一个控制台窗口都会起作用,你就不必在命令提示符里执行这条命令了。

  • -p(另一种形式是:--password

此选项会让mysql显示Enter password: 提示符,并要求你输入MySQL密码。例如:

% **mysql -h **host_name**-p -u**user_name
Enter password:

当你看到Enter password: 提示符时,请输入你的密码。(输入的密码不会显示到屏幕上,以免被人偷看到。)请注意:MySQL密码并不一定与登录Unix或Windows系统的密码相同。

如果你省略选项-p,那么mysql将认为你不需要密码,因此不会提示你输入它。

在命令行上直接提供密码的另一种方式是输入-pyour_pass选项(另一种形式是:--password=your_pass,其中的your_pass即为你的密码)。不过,出于安全的考虑,最好别这样做。因为在输入时,你身边的其他人能看到屏幕上的密码。另外,在Unix系统里,其他用户可能也能够使用系统工具查看到命令行。

如果你确实想要在命令行直接输入密码,那么请注意:在-p选项和后面的密码值之间没有空格。-p选项的这种行为经常会造成混乱,因为它与选项-h-u的习惯要求有所不同:它们都是与跟在后面的单词相关联的,不管其间是否有空格。

假设MySQL的用户名和密码分别是sampadmsecret。如果运行MySQL服务器的那台主机与你运行mysql程序的主机相同,那么你可以省略选项-h``, 此时mysql命令会像下面那样连接服务器:

% **mysql -p -u sampadm  
**Enter password: ******

在输入完这条命令之后,mysql会显示Enter password:``,提示你输入密码,然后你便可以输入它(输入的secret会在屏幕上显示为6个星号******)。

如果一切顺利,mysql将会显示出一条欢迎消息和一个mysql&gt;提示符,此时表明它在等你发起查询命令。完整的启动过程近似如下:

%** mysql -p -u sampadm  
**Enter password: ******  
Welcome to the MySQL monitor.  Commands end with ; or \g.  
Your MySQL connection id is 13762  
Server version: 5.5.30-log  

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.  

mysql>

如果想要连接运行于另一台机器上的MySQL服务器,那么必须使用-h选项来指定其主机名。假设该主机名为cobra.example.com,那么相应的命令如下所示:

% **mysql -h cobra.example.com -p -u sampadm**

为简洁起见,在后面大部分显示mysql命令行的示例里,选项-h-u-p都会被省略,并且假定你会提供所有必要的选项。在运行其他MySQL程序(如mysqlshow)时,你也会用到这些选项。

在连接到MySQL服务器之后,你可以随时输入quit命令来终止会话:

mysql> **quit**  
Bye

也可以输入exit\q来退出。在Unix系统里,可以按组合键Ctrl+D来退出。

在刚开始学习MySQL时,很多人都可能会对它的安全系统感到烦恼,因为它很难操作。(必须要有创建和访问数据库的权限;还有,不管何时连接服务器,都必须指定正确的用户名和密码。)不过,在练习完本书提供的示例数据库,并开始输入和使用自己的数据记录之后,你的看法便会迅速发生转变。到那时,你就会欣赏MySQL的这种方式,因为它可以防止他人窥视(或者更为糟糕的情况,破坏)你的信息。

如何设置工作环境,才能不用在每次运行mysql时都需要在命令行指定连接参数?关于这个问题请参考1.5节。简化服务器连接过程的最常见办法是,将连接参数存放到一个选项文件里。如果想现在就了解如何建立这样的文件,那么请直接跳转到1.5节。

1.4.4 执行SQL语句

在连接上服务器以后,你便可以发起SQL语句,让服务器执行它。本节将介绍一些与mysql``进行交互的一般原则。

想要在mysql里输入语句,只需要直接输入即可。在语句的结尾,请输入一个分号(;),并按下回车键。该分号会告知mysql——语句到此结束。在输入完一条语句之后,mysql会将它发送到服务器执行。服务器接着处理它,并把结果发送回mysql,然后mysql会显示结果。

下面这个示例展示了一条用于查询当前日期和时间的简单语句:

mysql> **SELECT NOW();**  
+---------------------+  
| NOW()               |  
+---------------------+  
| 2013-01-08 17:42:33 |  
+---------------------+  
1 row in set (0.00 sec)

除使用分号外,还有另一种终止语句的方法,即使用\g(意思是go):

mysql> **SELECT NOW()\g  
**+---------------------+  
| NOW()               |  
+---------------------+  
| 2013-01-08 17:42:40 |  
+---------------------+  
1 row in set (0.00 sec)

也可以使用\G。它会以“垂直”方式显示结果,每行显示一个值:

mysql> **SELECT NOW(), USER(), VERSION()\G  
***************************** 1. row ***************************  
    NOW(): 2013-01-08 17:54:24  
   USER(): sampadm@localhost  
VERSION(): 5.5.30-log  
1 row in set (0.00 sec)

如果语句的输出行比较短,那么\G就没什么用处;但如果输出行很长,而且会在屏幕上回绕显示,那么使用\G便能让输出内容更易于阅读。

如上所示,mysql会显示语句结果和一行统计信息,其中包括该结果所包含的行数,以及语句执行所花费的时间。在后面的示例里,一般情况下将不再显示统计信息行。

因为mysql会等待语句结束符,所以你不用在单独一行里输入一条语句的全部内容。你可根据需要,将它分隔成多行进行输入:

mysql> **SELECT NOW(),********  
**    -> **USER(),  
**    -> **VERSION()  
**    -> **;  
**+---------------------+-------------------+------------+  
| NOW()               | USER()            | VERSION()  |  
+---------------------+-------------------+------------+  
| 2013-01-08 17:54:56 | sampadm@localhost | 5.5.30-log |  
+---------------------+-------------------+------------+

请注意,在输入该语句的第一行时,提示符从mysql&gt;变为了-&gt;。这是在提醒你:mysql认为你还要继续输入语句内容。这是个很重要的反馈。如果你忘了在语句末尾加上分号,那么这个变化的提示符会提醒你注意:mysql仍在耐心等待你继续输入语句内容。否则,你会很不耐烦地等在一边,心里疑惑:为何MySQL执行你的语句要花这么长的时间。命令mysql还有另外几种提示符,附录F对它们进行了详细介绍。

如果你已经输入了一条多行语句,但突然决定不执行它了,那么可以输入\c来清除(即取消)它:

mysql>** SELECT NOW(),  
**    ->** VERSION(),  
**    ->** \c  
**mysql>

请注意,提示符是如何变回mysql&gt;的。这种变化表明mysql已准备接收一条新的语句。

与将一条语句分成多行输入相反的操作是,在单独一行里输入多条语句,两条语句中间用终止符隔开:

mysql> **SELECT NOW();SELECT USER();SELECT VERSION();  
**+---------------------+  
| NOW()               |  
+---------------------+  
| 2013-01-08 17:55:20 |  
+---------------------+  
+-------------------+  
| USER()            |  
+-------------------+  
| sampadm@localhost |  
+-------------------+  
+------------+  
| VERSION()  |  
+------------+  
| 5.5.30-log |  
+------------+

大多数情况下,输入语句使用大写、小写或者大小写混用都可以。例如,下面几条语句检索到的是相同的信息(虽然显示结果里列标题的大小写有所不同):

SELECT USER();  
select user();  
SeLeCt UsEr();

本书的所有示例都将用大写字母来表示SQL关键字和函数名,用小写字母来表示数据库、表和列的名字。

当在语句里调用函数时,请不要在函数名和后面的括号之间加入空格。如果有空格,则会导致语法错误。

将多条语句存储在一个文件里可以创建一个SQL脚本,然后让mysql从该文件(而不从键盘)读取语句。请使用shell的输入重定向功能来实现这种操作。例如,假设语句都存储在一个名为myfile.sql的文件里,那么我们可以使用下面这样的命令来执行其中的语句(请记得指定所有必需的连接参数选项):

% **mysql < myscript.sql**

你可以为该文件随便取一个名。我给它加上了一个“.sql”后缀,以表明里面存放的是SQL语句。

像这种调用mysql来执行文件里的语句的做法,会在1.4.7节再次用到,到时,我们会用这种办法来往sampdb数据库里输入数据。与逐条手工输入相比,让mysql从某个文件里读取多条INSERT语句来加载表要方便很多。

本教程的其余部分展示了很多SQL语句,你可以自行练习。在这些语句前面都有提示符mysql&gt;作为标志,并且这些示例都提供了语句输出结果。如果你输入与示例显示一样的语句,那得到的输出结果也应该是相同的。前面未加提示符的语句主要是用来做说明用的,你不需要执行它们。当然,如果愿意,执行一下也无妨。提醒一下,请记得在每条语句的末尾加上一个分号作为结束符。

1.4.5 创建数据库

数据库的使用涉及以下几个步骤。

(1)创建一个数据库。

(2)在该数据库里创建多个表。

(3)对表里的数据执行插入、检索、修改或删除操作。

想要创建新的数据库,请先使用mysql连接到服务器。然后执行CREATE DATABASE语句,并指定新的数据库名字:

mysql> **CREATE DATABASE sampdb;**

在创建可进入或对其内容进行操作的表之前,必须先创建sampdb数据库。

你可能会希望在创建某个数据库的同时,让它成为默认(或当前)数据库。但这是行不通的。看看下面这条检查默认数据库的语句,你就会明白这一点:

mysql> **SELECT DATABASE();  
**+------------+  
| DATABASE() |  
+------------+  
| NULL       |  
+------------+

NULL表示“未选择到数据库”。如果想要把sampdb设置为默认选择数据库,那么还需要执行一条USE语句:

mysql> **USE sampdb;  
**mysql> **SELECT DATABASE();  
**+------------+  
| DATABASE() |  
+------------+  
| sampdb     |  
+------------+

另一种选择默认数据库的办法是,调用mysql的时侯在命令行上给该数据库取一个名字:

% **mysql sampdb**

事实上,这就是在选择数据库时用得最多的办法。如果还需要使用连接参数,那么可以在命令行指定它们。例如,下面的命令可以让用户sampadm连接到本地主机(如果未指定主机名字,则默认是它)上的sampdb数据库:

%** mysql -p -u sampadm sampdb**

如果需要连接到运行于远程主机上的MySQL服务器,那么需要在命令行指定该主机:

% **mysql -h cobra.example.com -p -u sampadm sampdb**

如果无特别说明,后面所有的示例都将假定:当调用mysql时,命令行里指定的默认数据库就是sampdb。如果在调用mysql时忘记在命令行指定数据库,那么请在mysql&gt;提示符处输入一条USE sampdb语句。

1.4.6 创建表

本节将创建示例数据库sampdb所需要的那些表。首先,创建“美史联盟”场景所需要的表,然后,再为“成绩考评”项目创建所需的表。有些数据库书讲到在此处便会开始讨论“数据库的分析与设计”、“实体联系图”、“规范化过程”(Normalization Procedure)等内容。有很多书专门讲解这些内容,所以本书在这里只想说明我们的数据库应该是个什么样子,具体来讲涉及这样几个方面:它应该包含哪些表、每个表都应包含什么内容,以及在决定如何表示这些数据时需要考虑哪些问题。

这里所选择的表示方式并不是绝对的。在其他场合,你可以选用不同的方式来表示相似的数据。到底选择哪一种,需要由应用程序和数据的具体用途来决定。

1.4.6.1 美史联盟表

美史联盟的表相当简单,包括以下两个表。

  • president表。其中包含美国历任总统的描述性记录。我们需要用它来实现联盟网站上的在线小测验(对联盟通讯儿童专栏里出现的小测验进行交互式模拟)。
  • member表。用于保存联盟每位成员的最新个人资料。我们可以用它来创建成员名录的印刷版本和在线版本,用它来向到期成员自动发送提醒通知,还可以用它做很多其他事情。
1. president

president表包含一些与美国历任总统生平相关的基本信息。

  • 姓名。在表里,表示姓名的方式有好几种,如使用单列包含整个名字,或用不同的列分别表示姓(last name)和名(first name)。使用单列来表示当然更简单一些,但这种做法不够灵活,存在一些限制。

如果优先输入名,就无法按姓排序。

如果优先输入姓,就无法按名在前、姓在后的顺序显示它们。

难以查找姓名。例如,想要查找某人的姓,那么必须先使用一种模式,然后去查找与之匹配的姓名。与直接查找姓的做法相比,这种做法的效率很低,速度很慢。

为避免出现这些限制, president表将使用两列分别表示姓和名。

那个“名”列还要存放中间名或其缩写。这种做法不会影响到以后的姓名排序操作,因为我们不太可能对中间名进行排序(也不太可能对名进行排序)。另外,姓名也可以正常显示,因为无论是以“Bush, George W.”或“George W. Bush”中的哪一种格式来输出,姓名里的中间名总是排在名字的后面。

还有一点需要注意。如果一位总统(如Jimmy Carter)的姓名后面带有一个“Jr.”,那么这个“Jr.”应该放在哪里呢?根据英文姓名输出格式习惯,这位总统的名字可以显示为“James E. Carter, Jr.”,或者“Carter, James E., Jr.”。这里的“Jr.”与名和姓没有联系,因此我们还需要另外创建一个列,用它来保存姓名后缀。这种情况表明:当你在选择数据的表示形式时,即使只是一个单值,也可能会引发某些问题。它同时还表明:在把数据值放入数据库之前,最好尽可能多地对数据值进行深入的了解。如果对数据值的真正含义了解不够,那么在表启用之后,可能还要被迫更改它的结构。虽然这种事不能算作是一场灾难,但总的说来还是尽量避免为妙。

  • 出生地(城市和州)。与姓名的情况类似,它既可以用一列来表示,也可以用多列表示。采用单列来表示的做法显得更简单些;但与姓名的情况一样,使用多列能实现某些更复杂的操作。例如,若把州名与市名分开表示,那么像“找出出生在某个州的总统共有多少位”这种类似的操作便能轻易地实现。我们将使用两个单独的列来分别存放州名与市名。
  • 出生日期和逝世日期。这里唯一需要特殊处理的事情是:因为有些总统依然健在,所以我们不能要求必须填上逝世日期。特殊值NULL的意思即表示“无值”,因此我们可以在逝世日期列里用它来表示该位总统“依然健在”。
2. member

从每条记录都保存着单独某个人的个人资料这一角度来看,存放“美史联盟”成员列表的member表与刚才介绍的president表很相似。只是member表的每一行还包含了以下这些列。

  • 姓名。我们将沿用与president表相同的3列表示法:姓、名和姓名后缀。
  • ID编号。这是一个唯一值,为每个首次加入的成员分配一个。联盟此前从未对成员编过号,但现在需要所有的记录都更系统化,因此这里需要这个值。(希望你能不断发现MySQL的好处,并找到更多将编号应用于联盟记录的方式。当想要将member表里的行,与你所创建的成员相关的其他表建立关联时,使用编号则会比使用姓名更容易实现。)
  • 有效期。所有成员必须定期更新其成员资格,以避免过期。对于某些应用程序,可能还需要把最近一次资格更新后的起始日期存储起来,但“美史联盟”不需要这样做。成员资格的有效期是一个变值(通常有1年、2年、3年或者5年之分),而最近一次的资格更新日期也并不能说明该成员下一次的资格更新日期一定是在什么时候。因此,我们需要把成员资格的截止日期存储起来。此外,联盟还提供了终身成员资格。虽然我们可以用一个很遥远的日期来表示这种情况,但使用NULL会更合适,因为“无值”在逻辑上正好对应于“永不失效”。
  • 电子邮件地址。公开电子邮件地址可以使兴趣相投的成员交流起来更方便。作为联盟的秘书,在有了这些地址之后,就可以用电子邮件来向成员发送成员资格更新通知,而不用邮寄信件了。与到邮局寄信相比,这种做法既方便又省钱。你还可以利用电子邮件把每位成员的个人最新资料发送给他们,让他们在必要时更新信息。
  • 通信地址。当与那些没有电子邮件(或者长期没有回复你邮件)的成员进行联系时,你会需要这条信息。我们将使用多个列来分别存储街道地址、城市名、州名和邮政编码。

我们假设所有联盟成员都居住在美国。当然,对于那些成员遍布于世界各地的组织机构来说,这个假设可能显得有些简单。如果想要处理涉及多个国家的地址,那么你将遭遇到一些棘手的问题,必须要解决好不同国家所使用的地址格式不同的问题。例如,邮政编码就不是一项国际标准;另外,有很多国家都只有“省”的概念,而无“州”的说法。

  • 电话号码。与地址列相似,主要用于联系成员。
  • 特殊兴趣关键字。联盟的每位成员对美国历史肯定都很感兴趣,但他们的兴趣却可能集中于某些特定的历史时期。此列便是用于记录这些兴趣。每位成员都可以利用这些信息来寻找与自己兴趣相投的其他成员。(严格来讲,建立一个独立的表可能会更好些,表中的每一行都由一个关键字和相关成员的ID组成。这点有些复杂,我们暂不作处理。)
3. 创建美史联盟表

现在,我们准备创建美史联盟表。为此,我们需要使用CREATE TABLE语句,该语句具有以下格式:

CREATE TABLE tbl_name (column_specs);

其中,tbl_name指的是你要给表指定的名字;column_specs指的是该表的各列。该语句还会包括各种索引的定义,如果有的话。索引能够加快查找速度,关于这一点请参考第5章的介绍。

下面是针对president表的CREATE TABLE语句:

CREATE TABLE president  
(  
  last_name  VARCHAR(15) NOT NULL,  
  first_name VARCHAR(15) NOT NULL,  
  suffix     VARCHAR(5) NULL,  
  city       VARCHAR(20) NOT NULL,  
  state      VARCHAR(2) NOT NULL,  
  birth      DATE NOT NULL,  
  death      DATE NULL  
);

执行此语句的方法有两种:可以自己手动输入;也可以使用sampdb发行包中create_president.sql文件里包含的预先编写好的语句。

如果想要自已输入这条语句,那么请先调用mysql,同时将sampdb设置为默认数据库:

%** mysql sampdb**

接着,输入上面的CREATE TABLE语句。请记得在语句末尾输入分号,以便将这条语句的结束位置告知mysql程序。没有缩进格式也没有关系,你不用像上面那样换行。你完全可以在同一行输入整条语句。

如果想用预先编写好的描述来创建president表,那么可以使用sampdb发行包里的create_president.sql文件。当你解压发行包时,解压程序会自动创建一个sampdb目录,并将那个文件放在这个目录里。先进入该目录,然后执行下面这条命令:

% **mysql sampdb < create_president.sql**

无论你采用何种方式调用mysql,都请记得在命令行的命令名后面指定你可能需要的连接参数(其中包括主机名、用户名和密码)。

现在让我们来仔细看一下CREATE TABLE语句。该语句会为每一列指定列名、数据类型(列要存放的值的类型),以及可能有的某些列属性。

president表用到了两种数据类型:VARCHARDATEVARCHAR(n)表示的是:该列可以存放长度可变的字符型值,且最大长度为n个字符。也就是说,它们包含了长度不定的字符串,但其长度存在上限。那个n的值即表明了你所期望的数据长度。例如,将state定义为VARCHAR(2)类型,即表示我们需要以两个字母的缩写形式输入州名。而其他值为字符串类型列,其数据长度必须要再长一点才能容纳下更长的值。

我们用到的另一个数据类型是DATE。很显然,此类型表示该列用于保存日期值。不过,日期的表示格式可能会让你感到意外。MySQL要求将日期表示为'CCYY-MM-DD'格式,其中的CCYYMMDD分别代表了世纪、世界的年份、月和日。这也是SQL标准规定的日期表示格式(也叫做ISO 8601格式)。例如,想要在MySQL里指定“2013年7月18日”这样一个日期,则需要使用'2013-07-18'而不能用'07-18-2013''18-07-2013'`。

president表里,我们用到的列属性只有NULL(可以没有值)和NOT NULL(必须要有值)。大部分数据列的属性都为NOT NULL,因为我们要求它们必须要有值。可以有NULL属性的两列是:suffix(大部分总统的姓名都无后缀)和death(如果总统还健在,则无逝世日期)。

对于member表,其CREATE TABLE语句如下:

CREATE TABLE member  
(  
  member_id  INT UNSIGNED NOT NULL AUTO_INCREMENT,  
  PRIMARY KEY (member_id),  
  last_name  VARCHAR(20) NOT NULL,  
  first_name VARCHAR(20) NOT NULL,  
  suffix     VARCHAR(5) NULL,  
  expiration DATE NULL,  
  email      VARCHAR(100) NULL,  
  street     VARCHAR(50) NULL,  
  city       VARCHAR(50) NULL,  
  state      VARCHAR(2) NULL,  
  zip        VARCHAR(10) NULL,  
  phone      VARCHAR(20) NULL,  
  interests  VARCHAR(255) NULL  
);

与之前一样,你可以手动将这些语句输入mysql中,也可以利用预先编写好的文件。发行包sampdb中的creat_member.sql文件包含了member表的CREATE TABLE语句。要使用它,需要执行以下命令:

% **mysql sampdb < create_member.sql**

就数据类型而言,member表里的大部分列(有两个例外)的类型都很普通,都是可变长度的字符串类型。列member_idexpiration是例外,它们分别保存的是序号和日期。

使用成员资格编号列member_id的主要考虑是:为了避免成员之间产生混淆,其中每一个值都应该是唯一的。此时,AUTO_INCREMENT列正好可以派上用场,因为当我们添加新成员时,MySQL便能为我们自动生成一个唯一编号。虽然member_id列只是包含数字,但其定义却包含有好几个部分。

  • INT。它表示该列用于存放整数(无小数部分的数值)。
  • UNSIGNED。它表示该值不能为负数。
  • NOT NULL。它表示该列必须要填值,这可以防止创建的成员没有ID号。
  • AUTO_INCREMENT。它是MySQL的一个特殊属性,表示该列存放的是序号。AUTO_INCREMENT的工作原理为:当往member表里添加新记录时,如果没有为member_id列提供值,那么MySQL将自动生成下一个编号,并将它赋给该列。如果你显式地将NULL赋给该列,结果也是一样的。AUTO_INCREMENT的这种特性使得为每一位成员分配一个唯一的ID变得很简单,因为MySQL会替我们生成这些值。

PRIMARY KEY子句表示为member_id列创建索引,这样能加快查找速度。同时,它还设置了约束:要求该列里的所有值都必须唯一。后面这个属性正好符合对成员ID值的要求,因为它可以防止我们两次误用相同的ID。此外,MySQL要求AUTO_INCREMENT列必须有某种索引,如果没有索引,那么表的定义就是不合法的。(任何一个PRIMARY KEY列也必须是NOT NULL的,因此就算我们在member_id定义里省略了NOT NULL,MySQL也会自动加上。)

如果你还不太明白AUTO_INCREMENTPRIMARY KEY是怎么回事,那么可以把它们想象成一种能生成索引ID号的神奇魔法。这些值到底是什么并不重要,只要这些ID号对每位成员来说都是唯一的就可以了。(关于AUTO_INCREMENT列的更多信息请参考第3章。)

expiration列的数据类型是DATE。它允许值为NULL,且默认也为NULL,即表示可以不输入日期。正如前面所提到的,我们将使用这样一个约定:当expiration``值NULL时,表明该成员拥有终身成员资格。

到目前为止,你已经让MySQL创建了两个表,现在让我们来确认一下结果是否正确。在mysql里,调用下面这条命令可以查看president表的结构:

mysql> **DESCRIBE president;  
**+------------+-------------+------+-----+---------+-------+  
| Field      | Type        | Null | Key | Default | Extra |  
+------------+-------------+------+-----+---------+-------+  
| last_name  | varchar(15) | NO   |     | NULL    |       |  
| first_name | varchar(15) | NO   |     | NULL    |       |  
| suffix     | varchar(5)  | YES  |     | NULL    |       |  
| city       | varchar(20) | NO   |     | NULL    |       |  
| state      | varchar(2)  | NO   |     | NULL    |       |  
| birth      | date        | NO   |     | NULL    |       |  
| death      | date        | YES  |     | NULL    |       |  
+------------+-------------+------+-----+---------+-------+

调用DESCRIBE member语句,则可以看到有关member表的类似信息。(如果你想知道为什么未定义为允许NULLDefault列会显示NULL,那么我在这里告诉你,那是因为NULL也可用于表明该列没有显式的DEFAULT子句。)

如果你不记得一个表中的列名、想知道其数据类型,或者想了解其宽度是多少,那么可以使用DESCRIBE。你还可以利用它来查看MySQL在表行里存储各列的先后顺序。当你执行INSERTLOAD DATA语句时,这个顺序很重要,因为这些语句要求各列的值是以它们的默认列顺序列出的。

你也可以通过其他方式来获得DESCRIBE生成的信息。既可以是简写的DESC,也可以是EXPLAIN``语句SHOW语句。下面这些语句具有相同的作用:

DESCRIBE president;  
DESC president;  
EXPLAIN president;  
SHOW COLUMNS FROM president;  
SHOW FIELDS FROM president;

这些语句还允许对输出列加以限制。例如,可以在SHOW语句的末尾加上一个LIKE子句,这样便只能看到与给定模式相匹配的那些列的信息:

mysql> **SHOW COLUMNS FROM president LIKE '%name';  
**+------------+-------------+------+-----+---------+-------+  
| Field      | Type        | Null | Key | Default | Extra |  
+------------+-------------+------+-----+---------+-------+  
| last_name  | varchar(15) | NO   |     |         |       |  
| first_name | varchar(15) | NO   |     |         |       |  
+------------+-------------+------+-----+---------+-------+

使用``DESCRIBE president`` '%name'也可以得到同样的结果。这里用到的百分号(%)是一个特殊的通配符,我们将在后面的1.4.9 .7节中介绍它。

SHOW FULL COLUMNSSHOW COLUMNS很像,不同之处在于它会显示附加的列信息。你可以现在试一下,看看结果。

SHOW语句还有其他几种形式,可用于从MySQL获得不同类型的信息。SHOW TABLES语句会列出默认数据库里的表。到目前为止,我们已在数据库sampdb里创建了两个表,因此执行该语句将得到如下输出:

mysql> **SHOW TABLES;  
**+------------------+  
| Tables_in_sampdb |  
+------------------+  
| member           |  
| president        |  
+------------------+

SHOW DATABASES语句会列出当前连接到的服务器上的所有数据库:

mysql> **SHOW DATABASES;  
**+--------------------+  
| Database           |  
+--------------------+  
| information_schema |  
| mysql              |  
| sampdb             |  
| test               |  
+--------------------+

这个列表可能会因服务器不同而有所差异,但是应该能看到information_schemasampdb。其中,information_schema是一个事先存在的特殊数据库;而sampdb则是刚才创建的。另外还有一个数据库test,它是MySQL在安装过程中创建的。如果你的访问权限足够高,你可能还会看到名为mysql``的数据库,它是一个权限分配表。

客户端程序mysqlshow提供了一个命令行接口,通过它所获得的信息和使用SHOW语句所显示的一样。请记住:当你运行mysqlshow时,还需要提供正确的命令行选项,其中包括用户名、密码和主机名。这些选项与你在运行mysql时所用的一样。

当不带参数时,mysqlshow会显示出一个数据库列表:

% **mysqlshow**  
+--------------------+  
|     Databases      |  
+--------------------+  
| information_schema |  
| mysql              |  
| sampdb             |  
| test               |  
+--------------------+

在带上数据库名之后,mysqlshow会列出给定数据库里的所有表:

% **mysqlshow sampdb  
**Database: sampdb  
+-----------+  
|  Tables   |  
+-----------+  
| member    |  
| president |  
+-----------+

如果同时带上数据库名和表名,那么mysqlshow会显示出该表里各列的信息,这时等同于SHOW ``FULL ``COLUMNS语句。

1.4.6.2 成绩考评项目表

要想确定成绩考评项目需要用到哪些表,得先弄清在用纸质成绩册来记录考生成绩时可能会怎么做。图1-2展示了成绩册中一页的内容。该页的主体部分是用于记录分数的表格。该页里面还包含了其他一些让分数有意义的信息。学生的姓名和ID号列在表格的左侧。(为了简洁,这里只列出了4位学生。)考试或测验的举行日期则列在表格的顶部。从该表格可以看出:在9月的3、6、16、23日有测验;在9月9日和10月1日有考试。

图1-2 成绩册示例

为使用数据库持续跟踪这些信息,我们需要一个score``(分数)表。那么,在这个表里应该包含哪些行呢?此问题不难回答。在每一行里,需要列出学生姓名、考试或测验日期,以及学生的考试分数。图1-3展示了成绩册里的部分分数在表里的表现形式。(其中,日期是按MySQL的方式来表现的,即'CCYY-MM-DD'格式。)

图1-3 最初的score

可惜的是,采用这种方式得到的表遗漏了一些信息。例如,请仔细看看图1-3中的各行,它还未清楚地表明该成绩是考试分数还是测验分数。一般来说,在评定的期末总成绩时,考试分数与测验分数的权重是有明显区别的,因此有必要将考分类别标识出来。当然,我们也可以根据某给定日期的分数范围(在数值上,测验分数通常要比考试分数低很多)来推测其类型,但这种不采用具体数据明确标识的做法会带来问题。

可以在每行记录里将各类考分区别开来,具体方法就是:给score表增加一列,用TQ分别表示考试(test)或测验(quiz),如图1-4所示。这种做法的优点是考分类别能直接体现在数据上;缺点是显得有些冗余。看看那些日期相同的行就能发现,考分类别栏里的值都是相同的。所有9月23日的考分都属于Q类,所有10月1日的考分都属于T类。没人会喜欢这个样子。如果按这种办法来记录学生们的考试分数,那么我们不仅要反复输入相同的日期,而且还要多次输入一个相同的考分类别。天啊!谁会愿意输入这么多的冗余信息呢?

图1-4 修改后包含分数类型的score

我们再来试试另一种表示方式。这次不把考分类别放到score表里,而是将它与考试日期对应起来。我们可以把考试日期列出来,然后用它来跟踪在各个日期发生过什么样的“考试事件”(包括测验和考试)。这样,我们就可以根据score表里的日期在grade_``event表里查出当天的考试事件类型,从而得知某个分数是来自测验,还是来自考试。将score表里的日期与grade_``event表进行匹配,便能获得考试的类别。图1-5列出了这个表的结构,并展示了它与score表之间是如何与9月23日那天关联起来的。通过将该行与grade_``event表里的相应行相匹配,我们便能看出该分数是一次测验成绩。

图1-5 score表与grade_event表,通过日期链接

与通过推测来判断考分类别的做法相比,这种方法更好一些。因为现在能够从记录在数据库里的数据直接得出考试分数的类型。与将考分类别直接记录在score表里的做法相比,这种方法也更易让人接受。现在,我们只需记录一次考分类别,再也不用为每个考分都记录一次了。

不过,我们现在需要把多个表的信息进行组合。如果你是我,那么在第一次听说这种事的时候,或许会想:“嘿,这个主意真不错。但是,有这么多的表,想查什么东西会不会太费事?这会不会把事情搞得更复杂了?”

在某种程度上,这种担心是有道理的,它需要做更多的工作。记录两个表肯定会比记录一个表复杂。但是,请再仔细看看当初的成绩册(如图1-2所示),你不是已经在记录两组信息了吗?请看下面两个事实。

  • 你需要把考试成绩记录在表格的每一个格里,这些格都要按学生姓名和考试日期进行排列(姓名由上往下排列,考试日期则由左往右排列)。这正是我刚才讲过的两组信息当中的一组,而它正好对应于score表里的内容。
  • 你要如何才能知道各日期所对应的考试类型呢?你或许会这样做:在日期上方写一个TQ。于是,你又在表格的顶部把考试日期与考试类型关联起来了。这正是我刚才讲过的两组信息当中的第二组,而它也正好对应于grade_``event表里的内容。

换句话说,尽管你可能还未意识到这一点,但你在成绩册里所做的事,与我把信息放到两个表里的做法并无多大差异。唯一的区别在于,纸质成绩册里的两类信息没有明确地分离开来。

成绩册里的页的概念体现了我们对信息的思维方式,同时这也表明:把信息妥善地放到数据库里并不是一件简单的事情。在日常生活中,人们习惯于把不同信息综合起来,然后把它们当作一个整体来考虑。但数据库无法那样工作,这也正是它们看起来不太自然的原因。我们习惯把信息统一起来,这也使得我们有时很难清楚地分辨出自己所拥有的信息是有多种类型,还是只有一种。正因如此,“以数据库系统的方式进行思考”,考虑如何表现数据才具有挑战性。

图1-5里的grade_``event表还隐含了这样一个要求:所有的日期必须唯一,因为每个日期要用于链接scoregrade_``event两个表里的各个行。换句话说,不能在同一天进行两场测验,或者一次测验加一次考试。如果这样做的话,那么对于同一个日期,在score表里将会出现两组记录,而在grade_``event表里也会有两条记录,而且你也无法说清如何将score``中的这些行与grade_``event``中的那两行进行匹配。

假如你每天最多只进行一场考试,那么这个问题就绝不会出现。但是,可否假设这一情况永远不会发生呢?似乎可行。毕竟,心地善良的你应该不会对学生过于苛刻,要对他们每天进行两场考试。不过,我还是会经常听到有人声称:对于他们的数据,“这种奇怪的情况永远不会发生”。然而,事实证明,这种奇怪的情况偶尔也会出现;而这时为了弥补这一奇怪情况所引发的各种问题,你便不得不重新设计相关表。

最好能防患于未然,事先想好如何处理这些问题。因此,我们现在假设:你有时也会需要在同一天记录两组分数。这一问题该如何解决呢?事实证明,这个问题并不是那么难以解决。只需对有关数据的布局结构作一点小小的改动,就能处理在同一天存在多个事件的情况了,如下所示。

(1)在grade_``event表里增加一列,用它来为该表里的每一行分配一个唯一的编号。这样,每次事件就都拥有自己的ID编号了,我们可以将这一列命名为event_id。这种做法看起来有点奇怪,不过在图1-2的成绩册里其实已经隐式地用到了这个属性:这里的事件ID与成绩册分数表格里的列序号是相当的。虽说这个列序号未被显式地写出来,并标注为“event ID”,但它实际就是列序号。

(2)当把各个分数放到score表中的时候,记录事件ID,不记录日期。

完成上述改动之后,我们会得到图1-6所示的结果。现在要用event_id来链接score表和grade_``event表,不再使用date``了。使用grade_``event表不仅能查出每个考分的类别,还能查出它具体发生的日期。还有,在grade_``event表里,具有唯一性的不再是日期,而是事件ID。这意味着,在同一天可以进行多次考试和测验(在听到这个消息之后,你的学生肯定会欣喜若狂),而且你也能把它们直接记录下来。

图1-6  score``表grade_``event 表,通过事件ID链接

必须承认,图1-6里的表结构不如前面的那几个看起来顺眼。score表变得更加抽象,因为它包含的列越来越让人看不懂。请看图1-4里的score表,里面既有考试日期又有考分类别,让人一眼即能看明白。但在图1-6所示的score表里,那两列都不见了,我们看到的是一个高度抽象化的信息表示形式。谁愿意看一个只包含“事件ID”的score表呢?它毫无意义。

此时此刻,我们到了一个十字路口。大家之前还对电子化的成绩考评系统充满希望,觉得很快就能从繁琐的评分工作中解脱出来。可是,在了解到“在数据库里分数信息实际上是如何表示的”之后,你却因这些信息表示起来相当抽象和分散而开始踌躇不前。

这自然会引出一个问题:“也许MySQL不适合我。不用数据库会不会好些?”想必大家已猜到了,我对此持反对意见。原因很简单,你看本书的厚度就知道了。但是,当你在考虑如何开始某项工作之前,多考虑几种情况,以及想一下“如果使用像MySQL这样的数据库系统,或者使用其他像电子表格程序那样的工具,事情是否会变得更好”,这些都是不错的做法。对比情况如下。

  • 成绩册由行和列构成,电子表格也是如此。这使得它们在概念和外观上都很相似。
  • 电子表格程序能够执行计算,因此可以使用计算字段来统计每个学生的分数。将测验分数和考试分数按不同权重来统计可能会有点棘手,但相信你能办到。

另一方面,如果你想只对一部分数据进行操作(如只统计测验分数,或者只统计考试分数),进行对比分析(如男生与女生的对比),或者想以灵活方式显示统计信息,那么情况就有所不同了。这些工作不是电子表格所擅长的,而关系数据库系统却能轻易地完成。

另外一个需要考虑的地方是:在关系数据库里表示具有抽象和分散特性的数据,也不是什么大问题。在数据库建立之初,仔细考虑好信息在数据库里的表示方式是很有必要的,这样你才能按照最符合你目标的方式来安排你的数据。不过,确定好如何表示信息之后,你便需要依赖数据库引擎来收集数据,并把它以一种对你来讲很有意义的方式呈现出来。这样,你所看到的就不会是一组分散的数据块了。

例如,当从score表检索分数时,你想要看考试日期,而不想看到事件ID。这点很容易做到。数据库将在grade_``event表里根据事件ID查出考试日期,并显示在你面前。你可能还想要知道考试分数是属于测验的,还是属于考试的。这点也很容易做到。数据库可以采用同样的方式(根据事件ID)查出考分类别。别忘了,像MySQL这类的关系数据库系统最擅长的就是:将一个事物与另一样事物进行关联,从而在多个信息源里把你最想知道的信息提取出来。在成绩考评这个示例里,MySQL会负责考虑通过事件ID将信息汇集到一起,你不必去关心其中的细节。

现在,为了提前让大家了解到如何让MySQL实现这种事物之间的关联,假设你想要查看2012年9月23日的考试分数。下面这个查询可以将指定日期的考试分数查出来:

SELECT score.name, grade_event.date, score.score, grade_event.category  
FROM score INNER JOIN grade_event  
ON score.event_id = grade_event.event_id  
WHERE grade_event.date = '2012-09-23';

相当可怕,哈?这个查询通过将score表中的各行和event表中的各行进行连接(关联),检索得出学生姓名、考试日期、考试分数和考分类别等信息。结果如下所示:

+--------+------------+-------+----------+  
| name   | date       | score | category |  
+--------+----------- +-------+----------+  
| Billy  | 2012-09-23 |    15 | Q        |  
| Missy  | 2012-09-23 |    14 | Q        |  
| Johnny | 2012-09-23 |    17 | Q        |  
| Jenny  | 2012-09-23 |    19 | Q        |  
+--------+------------+-------+----------+

是不是觉得上面这个表格有点面熟?你应该很熟悉,它与图1-4里的表格布局是一样的。你不必知道事件ID就能获得结果。你只要指定你感兴趣的那个日期,然后MySQL便会根据这个日期将考试分数找出来。因此,当从数据库里以某种对我们很意义的形式将信息提取出来的时候,如果你还在担心那种抽象和分散是否会让我们迷失,那么到这个时候你应该可以看到根本不会出现这种情况。

当然,在仔细查看该查询之后,你或许又会产生一些新的疑问。换句话说,这个查询看起来又长又复杂。只是要查出某一天的考试分数,就要写得这么复杂?没错,它是有点复杂。不过,在每次你想要调用某个查询时,也有很多方法可以避免输入占用很多行的SQL语句。一般情况下,在执行完某个类似这样的查询之后,你可以将它保存起来;然后,在必要时你便可以轻易地重复使用它。关于这一做法的更多内容请参考1.5节。

为了让大家对查询过程先有所了解,我提前展示了这个示例。事实上,与我们真正用来检索考试分数的查询相比,它算是简单的。因为我们还需要对表的结构再做一次较大的改动。首先,我们将score表里的学生姓名替换成具有唯一性的学生ID。(也就是说,我们将使用成绩册里“ID”栏的值,而不使用“名称”栏里的值。)我们还要另外新建一个名为student的表,其中包含学生姓名(name)和学号(student_id)两列(见图 1-7)。

图1-7 score表、student表和grade_event表,通过学生ID和事件ID链接

为什么要做这样的改动呢?只为解决可能会出现的两名学生同名的情况。使用唯一的学生ID编号有助于把他们的分数区别开来。这与我们不用日期而是使用唯一的事件ID来区分同一天进行考试和测试的分数是一样的道理。在对表的结构做了上述改动之后,根据指定日期查询考试分数的语句又复杂了一点:

SELECT student.name, grade_event.date, score.score, grade_event.category  
FROM grade_event INNER JOIN score INNER JOIN student  
ON grade_event.event_id = score.event_id  
AND score.student_id = student.student_id  
WHERE grade_event.date = '2012-09-23';

如果你现在为看不懂这个查询命令而担心,那么请放松。大部分人都看不懂。在本教程的后半部分,我们还会遇到这个查询命令,不过前后的差异在于后面的那个版本会让你眼前一亮。真的,不开玩笑。

从图1-7里可以看到,student表里增加了一些在成绩册里没有的内容:它多了一个用于记录性别的列。可以利用这个列来统计班级里男女生的人数;也可以用它来做一些复杂的事情,如比较男女生的成绩。

至此,我们几乎完成了成绩考评项目所有表的创建。最后还需再增加一个表,用来记录考勤情况。这个表的内容相对比较简单,只包含一个学生ID和一个日期(见图1-8)。这个表的每一行都代表了一位在指定日期有缺席的学生。到期末的时候,我们将利用MySQL的统计功能来对表里的数据进行汇总,从而统计出每位学生的缺勤天数。

图1-8 absence表

1.student

现在,我们知道了那些成绩考评表的样子,下面来创建它们。用于创建student表的CREATE TABLE语句如下:

CREATE TABLE student  
(  
  name       VARCHAR(20) NOT NULL,  
  sex        ENUM('F','M') NOT NULL,  
  student_id INT UNSIGNED NOT NULL AUTO_INCREMENT,  
  PRIMARY KEY (student_id)  
) ENGINE = InnoDB;

请注意观察,我在CREATE TABLE语句里加入了一些新的内容(在末尾加上了ENGINE子句)。稍后我会解释它的用途。

你可以在mysql客户端程序里输入上面的CREATE TABLE语句,也可以在命令行里执行下列命令:

% **mysql sampdb < create_student.sql**

这条CREATE TABLE语句将创建一个名为student的表,其中包含三列:namesexstudent_id

name是一个长度可变的字符串列,它最多可以存储20个字符。这里的名字表示比“美史联盟”表里的简单,它只使用了一个单列,并没有将名和姓单独分开。这样做的原因在于:我事先知道成绩考评查询示例不需要用到多个列来表示名字。(没错,这是骗人的。我承认!实际上你可能需要使用多个列。)

sex用于表明某位学生是男生还是女生。这是一个ENUM(枚举)列,其取值只能是在该列的规范里列出的那些值当中的一个:'F'代表女生,'M'代表男生。当你想把某列的可取值限定在某个有限集合内时,ENUM会非常管用。当然,我们也可以把该列定义为CHAR(1),但ENUM可以让被允许的列值更加明确。如果你忘了它都有哪些可取值,就可以调用DESCRIBE命令来查看。MySQL会列出ENUM列的合法枚举值:

mysql> **DESCRIBE student 'sex';  
**+-------+---------------+------+-----+---------+-------+  
| Field | Type          | Null | Key | Default | Extra |  
+-------+---------------+------+-----+---------+-------+  
| sex   | enum('F','M') | NO   |     |         |       |  
+-------+---------------+------+-----+---------+-------+

ENUM列的值不一定非得是单个字符。例如,还可以把sex列定义为:ENUM ( 'female', 'male')

student_id是一个整型列,用于保存唯一的学生ID编号。通常情况下,ID编号来源于某个权威机构,如学校办公室。但本书出于示例目的,将自己编造它们。我们将使用一个AUTO_INCREMENT列,其定义与前面创建member表时所用的member_id列相类似。

如果的确需要从学校办公室获得学生ID编号,而不能自动生成它们,那么在定义student_id列时请不要为它加上AUTO_INCREMENT属性。不过,需要保留PRIMARY KEY子句,其目的在于要避免ID值出现重复或为NULL

现在,CREATE TABLE语句末尾的ENGINE子句有什么作用呢?如果存在这个子句,那么它将在创建新表时, 为MySQL应该使用的存储引擎指定名字。“存储引擎”就是一种用来管理某种表的处理器。MySQL有好几种存储引擎,都各有特色。其中两个最常用的引擎是InnoDB(MySQL 5.5版本的默认引擎)和MyISAM(MySQL 5.5版本之前的默认引擎)。

关于这两个引擎之间的不同之处请参考2.6.1节。现在,只需说明成绩考评项目表的定义显式指定了InnoDB引擎就行了,因为我们需要InnoDB引擎所提供的称为“引用完整性(referential integrity)”的功能。该功能是通过使用外键(foreign key)来实现的。也就是说,我们可以使用MySQL将某些约束施加到两个表之间的相互关系上,这种做法对成绩考评项目的各个表来讲是很有必要的。

  • 考试成绩与考试事件和学生都有关联:只有当与考试成绩相关联的学生ID和考试事件ID分别在student``表grade_event表里存在时,才允许考试成绩进入score表。
  • 类似地,考勤记录与学生有关联:只有与考勤相关联的学生ID在student表里存在时,才允许考勤情况进入absence表。

为了实施这些约束,我们需要建立一些外键关系。这里的“外”表示的意思是“在另一个表里”,而“外键”指的是必须与另一个表里的某个键值相匹配的键值。随着后面更多成绩考评项目表的创建,这些概念将会变得越来越清晰。

在早些时候,我们在创建美史联盟的表(presidentmember)时,并没有使用ENGINE子句,因此服务器会使用默认存储引擎来创建它们。如前所述,默认存储引擎是InnoDB(除非服务器被重新配置过)。student表的定义显式地包括了ENGINE = InnoDB,防止了服务器为其配置与此不同的默认值。

2.grade_event

grade_event表的定义如下所示:

CREATE TABLE grade_event  
(  
  date     DATE NOT NULL,  
  category ENUM('T','Q') NOT NULL,  
  event_id INT UNSIGNED NOT NULL AUTO_INCREMENT,  
  PRIMARY KEY (event_id)  
) ENGINE = InnoDB;

要创建grade_event表,可以在mysql客户端程序里输入上述CREATE TABLE语句,也可以在命令行上执行下面这条命令:

% **mysql sampdb < create_grade_event.sql**

d``ate``列用于存放标准的MySQL的DATE(日期类型)值,格式为'CCYY-MM-DD'(年在前)。

category表示的是分数类别。与student表里的sex列一样,category也是一个枚举列。其允许的取值是'T''Q',分别代表测试(test``)和测验(quiz``)

event_id是一个AUTO_INCREMENT列,同时也被定义为了PRIMARY KEY。它与student表里的student_id列类似。利用AUTO_INCREMENT属性,我们能方便地生成唯一事件ID的值。与student表里的student_id列类似,其特定的值并不重要,重要的是它们必须唯一。

因为这些列必须都要有值,所以它们全部被定义成了NOT NULL

3.``score

score表的创建语句如下所示:

CREATE TABLE score  
(  
  student_id INT UNSIGNED NOT NULL,  
  event_id   INT UNSIGNED NOT NULL,  
  score      INT NOT NULL,  
  PRIMARY KEY (event_id, student_id),  
  INDEX (student_id),  
  FOREIGN KEY (event_id) REFERENCES grade_event (event_id),  
  FOREIGN KEY (student_id) REFERENCES student (student_id)  
) ENGINE = InnoDB;

这个表的定义又包含了新内容:FOREIGN KEY结构。我们稍后会讲到它。

要创建score表,可以在mysql客户端程序里输入上面的语句,也可以在命令行里执行下面的命令:

% **mysql sampdb < create_score.sql**

score是一个INT列,用于保存整型分数值。如果想要保存像58.5那样带有小数部分的分数,那么最好使用能表示它们的数据类型,如DECIMAL

student_id列和event_id列都是整型列,它们分别表示每一个考试分数所对应的学生和考试事件。通过它们与student表和grade_``event表里的相应ID值链接起来,我们就能够查出学生姓名和考试日期。关于student_id列和event_id列有两个要点需要注意一下。

  • 我们已将这两列的组合设置成了一个PRIMARY KEY。这样可以确保我们不会重复记录某位学生在某次考试或测验的分数。请注意,只有event_idstudent_id的组合才具有唯一性。在score表里,这两个ID值自身都不具备唯一性。对于每一个event_id值(每位学生有一个)都会有多个分数行与之对应;对于每一个student_id值(每次考试或测验有一个)也会有多行记录相对应。
  • 每一个ID列都需要用FOREIGN KEY子句来定义约束条件。此子句的REFERENCES部分表明这个ID列是与哪个表的哪一列相对应。event_id列的约束条件为:该列里的每一个值都必须与grade_event表里的某个event_id值相匹配。类似地,score表里的每一个student_id值都必须与student表里的某个student_id值相匹配。

那个PRIMARY KEY定义可以确保我们不会创建重复的分数行。而FOREIGN KEY定义可以确保在我们的记录行不会有虚假的ID值,即要求它们必须存在于grade_event和student表里。

为什么student_id列会有一个索引呢?这是因为,对于FOREIGN KEY定义里的任何列,都应该有一个关于它们的索引,或者它们应该是某个多列索引里被首先列出的列,这样能加快查找速度。对于event_id列的FOREIGN KEY,该列被优先列在PRIMARY KEY里。对于student_id列的FOREIGN KEY,则无法使用PRIMARY KEY,因为student_id列未被首先列出来。因此,我们需要在student_id列上单独创建一个索引。

如有必要,InnoDB存储引擎会自动为出现在外键定义里的那些列创建一个索引,但它使用的索引定义不一定是你所期望的(更多有关信息请参考2.13节)。显示地定义这个索引可以避免这一问题。

4.absence

absence表用于记录学生的考勤情况,其创建语句如下所示:

CREATE TABLE absence  
(  
  student_id INT UNSIGNED NOT NULL,  
  date       DATE NOT NULL,  
  PRIMARY KEY (student_id, date),  
  FOREIGN KEY (student_id) REFERENCES student (student_id)  
) ENGINE = InnoDB;

要创建absence表,可以在mysql客户端程序里输入上述语句,也可以在命令行上执行以下命令:

% **mysql sampdb < create_absence.sql**

student_id列和date列都定义为NOT NULL,这样可以防止有缺失值。为了避免出现重复行,我们将这两列的组合定义为一个主键(primary key)。在同一天统计两次学生缺勤情况肯定是不公平的,对吧?

absence表也包含有一个外键关系,其目的在于确保每一个student_id值都与student表里的一个student_id值相匹配。

在成绩考评项目的各个表里设置外键关系,可以在让这些约束条件在数据录入阶段发挥作用,如我们只想插入那些包含合法考试事件ID值和学生ID值的记录行。不过,外键关系还有另外一种效果。它们会形成某些依赖关系,让你按照一定的顺序来创建和删除表,如下所示。

  • score表依赖于grade_event表和student表,因此在创建score表之前必须先创建其依赖的表。类似地,adsence表依赖于student表,因此在创建adsence表之前,student表必须已存在。
  • 在删除表时,必须把上面的顺序颠倒过来。如果不先删除score表,就无法删除grade_event表;如果不先删除score表和absence表,也无法删除student表。

1.4.7 添加新行

至此,我们的数据库和表就都创建好了。接下来,我们需要往表里添加一些行。不过,在往表里放入某些内容之后,得能知道如何检查表里的内容,因此,尽管有关检索操作的详细介绍要在1.4.9节才会讲到,此时也至少应该知道下面这条语句是用来查看tbl_name表里的全部内容的:

SELECT * FROM tbl_name;

例如:

mysql> **SELECT * FROM student;**  
Empty set (0.00 sec)

现在,mysql报告说该表为空,但在练习完本节的几个示例之后,你会看到不一样的结果。

往数据库里添加数据的办法有好几种。可以用INSERT语句将行手工插到表中,也可以利用文件把行添加到表里。该文件的内容既可以是一系列事先编写好的能直接提供给mysqlINSERT语句,也可以是通过LOAD DATA语句或mysqlimport客户端程序来加载的原始数据值。

本节将演示各种把记录插到表中的方法。大家应该多练习这些方法,熟悉和掌握它们的工作原理以及用法。在练习完这些方法之后,再转到1.4.8节,运行那里的命令。那些命令可以用来删除这些表,然后再重建它们,并将本书提供的数据加载到这些表里。这样,你的数据库所包含的内容就会与我在后面示例中用到的数据保持一致,而你在练习本书其他示例时也会看到相同的结果。如果你已知道如何插入行,那么可以直接跳过本节。

1.4.7.1 利用INSERT添加行

我们先使用INSERT语句来添加行,这是一条SQL语句,你可用它来指定要插入数据行的那个表,以及要插入的数据行和该行的各个列值。INSERT语句有多种格式。

(1)一次性指定全部列值。语法如下:

INSERT INTO _tbl_name_ VALUES(value1, value2, ...);

例如:

mysql> **INSERT INTO student VALUES('Kyle', 'M', NULL);  
**mysql> **INSERT INTO grade_event VALUES('2012-09-03', 'Q', NULL);**

在使用此语法时, VALUES列表必须包含表中每一列的值,并且值的顺序要与各列在表里的存储顺序保持一致。(通常情况下,该顺序就是各列在该表的CREATE TABLE语句里指定的顺序。)如果你不太确定列的顺序,那么可以利用DESCRIBE tbl_name语句来查明。

在MySQL里,你可以使用单引号或双引号将字符串和日期值括起来,不过使用单引号会更标准些。NULL值对应于student表和grade_event表里的AUTO_INCREMENT列。在AUTO_INCREMENT列里插入一个“缺失值”(missing value),可以让MySQL为该列自动生成下一个序号。

MySQL还支持使用一条INSERT语句,同时指定多个值列表的方式,将多个行插入一个表里:

INSERT INTO tbl_name VALUES(...),(...),... ;

例如:

mysql> **INSERT INTO student VALUES('Avery','F',NULL),('Nathan','M',NULL);**

与使用多条INSERT语句的方式相比,这种方式不仅能让你少打字,还能提高服务器的执行效率。请注意,将每行各列的值括起来的那对括号不可少。下列语句是非法的,因为它括号内包含的列值的个数不正确。

mysql>** INSERT INTO student VALUES('Avery','F',NULL,'Nathan','M',NULL);  
**ERROR 1136 (21S01): Column count doesn't match value count at _row_ 1

(2)命名赋值列,并列出它们的值。当你创建的行只有少数几列需要初始化时,这种方式特别有用。语法如下:

INSERT INTO _tbl_name_ (col_name1,col_name2,...) VALUES(value1,value2,...);

例如:

mysql> **INSERT INTO member (last_name,first_name) VALUES('Stein','Waldo');**

这种形式的INSERT语句也可以一次插入多个值列表:

mysql> **INSERT INTO student (name,sex) VALUES('Abby','F'),('Joseph','M');**

对于没在INSERT语句中指定的列,将被赋予默认值。例如,上面两条语句都未给member_idevent_id赋值,因此MySQL会将默认值NULL赋给它们。又因为member_idevent_id都是AUTO_INCREMENT列,因此最后的结果是这两列都会被分别赋予各自的下一个序号,这与显式地将NULL赋给它们是一样的。

(3)使用一系列的“列/值”形式进行赋值。此语法使用SET子句实现,其中包含多个col_name``= value的赋值形式,没有使用VALUES()列表的形式。

INSERT INTO tbl_name SET col_name1=value1, col_name2=value2, ... ;

例如:

mysql>** INSERT INTO member SET last_name='Stein',first_name='Waldo';**

对于没在SET子句里指定的列,将被赋予默认值。这种形式的INSERT语句无法用于一次插入多个行的情形。

既然已对INSERT语句的工作原理有所了解,那么现在便用它来检查一下,我们所建立的外键关系是否真的能够防止将不规范的行录入score表和absence表里。试着在grade_event和student表里找几个不存在的ID值,然后插入几条分别包含这些值的行:

mysql> **INSERT INTO score (event_id,student_id,score) VALUES(9999,9999,0);  
**ERROR 1452 (23000): Cannot add or update a child row: a foreign key  
constraint fails (`sampdb`.`score`, CONSTRAINT `score_ibfk_1` FOREIGN  
KEY (`event_id`) REFERENCES `grade_event` (`event_id`))  
mysql> **INSERT INTO absence SET student_id=9999, date='2012-09-16';  
**ERROR 1452 (23000): Cannot add or update a child row: a foreign key  
constraint fails (`sampdb`.`absence`, CONSTRAINT `absence_ibfk_1`  
FOREIGN KEY (`student_id`) REFERENCES `student` (`student_id`))

有错误消息出现,即表明这些约束发挥了作用。

1.4.7.2 利用文件添加新行

另一种把行载入表中的方法是,直接从文件里读取它们。该文件可以包含INSERT语句或原始数据。例如,在sampdb发行版里就有一个名为insert_president.sql的文件,它包含一系列用于将新行添加到president表里的INSERT语句。如果你是在与该文件相同的目录里,那么你可以像下面那样直接执行这些语句:

% **mysql ****Sampdb ****< insert_president.sql**

如果你已经运行了mysql,那么可以用一条source命令来读取这个文件:

mysql> **source insert_president.sql****;**

如果文件里存储的行不是INSERT语句而是原始数据,那么可以利用LOAD DATA语句或客户端程序mysqlimport来加载它们。

LOAD DATA语句是一个从文件里读取数据的批量加载程序。它需要在mysql里运行:

mysql> **LOAD DATA LOCAL INFILE 'member.txt' INTO TABLE member;**

假设member.txt文件位于客户端主机的当前目录里,那么上面这条语句会读取该文件,并将其内容发送至服务器,加载到member表里。member.txt文件可在sampdb发行版里找到。

默认情况下, LOAD DATA语句会假设各列的值是以制表符分隔的,各行末尾都是换行符。同时,假设这些值的顺序都与表里存储的列的顺序相同(文件里的\N值表示的是NULL)。你也可以用它来读取其他格式的文件,或者指定不同的列顺序。有关LOAD DATA的更多细节请参考附录E。

LOAD DATA语句里的关键字LOCAL会引发客户端程序(在本示例里,指的是mysql)读取数据文件,并把文件内容发送到服务器进行加载。如果省略了LOCAL,那么数据文件必须存在于服务器主机上,并且你需要拥有FILE服务器访问权限(大部分MySQL用户都没有这样的权限)。另外,你还要指定完整的文件路径,以便服务器能找到它。

如果在LOAD DATA语句里使用LOCAL时遇到以下错误信息,那么很可能是因为在默认情况下LOCAL功能被禁用了:

ERROR 1148 (42000): The used command is not allowed with this MySQL version

可以在mysql之后加上--local-infile选项再试一次。例如:

% **mysql --local-infile sampdb  
**mysql> **LOAD DATA LOCAL INFILE 'member.txt' INTO TABLE member;**

如果这招也不管用,那么说明服务器在启动时需要带上--local-infile选项。

另一种加载数据文件的方法是在命令提示符里使用客户端程序mysqlimport。它会为你生成一条LOAD DATA语句:

% **mysqlimport --local sampdb member.txt**

与程序mysql的用法一样,请根据需要在命令行里指定连接参数,并把它们放置在那个数据库名字的前面,紧挨着它。

对于上面这条命令, mysqlimport程序将生成一条能将member.txt文件里的内容加载到member表里的LOAD DATA语句。这是因为mysqlimport程序是根据数据文件的名字来确定表名的,同时它会把文件名中第一个句号(.)之前的所有内容都当作表名。例如,mysqlimport会把名为member.txtpresident.txt的文件分别加载到member表和president表里。这意味着,你应该仔细挑选数据文件名,否则,mysqlimport将无法使用正确的表名。如果想要加载文件member1.txtmember2.txt,那么mysqlimport会认为是要把这两个文件分别加载到名为member1member2的表里去。如果你真的想将这两个文件加载到member表里,那么可以分别将它们命名为member.1.txtmember.2.txt,或者member.txt1member.txt2

1.4.8 重置sampdb数据库

在练习完成上面介绍的这几种添加行的方法之后,为了顺利进行后面的学习,你应该重新建立和加载sampdb数据库里的各个表,把整个数据库恢复为原样。请在包含sampdb发布版文件的目录下,运用mysql程序来执行下面这些语句:

% **mysql sampdb  
**mysql> **source create_member.sql;  
**mysql> **source create_president.sql;  
**mysql> **source insert_member.sql;  
**mysql> **source insert_president.sql;  
**mysql> **DROP TABLE IF EXISTS absence, score, grade_event, student;  
**mysql> **source create_student.sql;  
**mysql> **source create_grade_event.sql;  
**mysql> **source create_score.sql;  
**mysql> **source create_absence.sql;  
**mysql> **source insert_student.sql;  
**mysql> **source insert_grade_event.sql;  
**mysql> **source insert_score.sql;  
**mysql> **source insert_absence.sql;**

如果不想单独输入这么多条语句,那么在Unix系统上,可以执行下面这条命令:

% **sh init_all_tables.sh sampdb**

而在Windows系统上,可以执行下面这条命令:

C:\> **init_all_tables.bat sampdb**

无论使用哪条命令,如果需要在命令行里指定连接参数,那么请把它们放到命令名的后面,且紧挨着它。

1.4.9 检索信息

现在,我们的表都建好了,并且加载了数据。下面一起来看看如何使用这些数据。使用SELECT语句可以检索和显示表里的信息。你可以根据自己的需要以常规或特定的方式来检索信息。例如,可以把表里的所有内容都显示出来:

SELECT * FROM president;

也可以只显示很少的数据,如一行中的一列:

SELECT birth FROM president WHERE last_name = 'Eisenhower';

SELECT语句拥有几个子句,你可以根据需要组合它们,用于检索你感兴趣的信息。这些子句可以很简单,也可以很复杂,因此语句SELECT也会随之变得简单或者复杂。不过,本书中绝对没有整页长的、需要花费一个钟头才能搞明白的查询语句。当我遇到长长的(arm-length)查询语句时,通常会跳过它们,我想你也会这样做。

SELECT语句的简化语法如下:

SELECT what to retrieve  
FROM table or tables  
WHERE conditions that data must satisfy;

在写SELECT语句时,需要先指定检索的内容,然后再加上一些可选的子句。上面显示的两个子句(FROMWHERE)是最为常见的,尽管还可以指定其他子句,如GROUP BYORDER BYLIMIT。请记住,SQL语言对语句格式并没有严格的要求,因此写你自己的SELECT语句时,不必严格像本书示例那样换行排列。

FROM子句通常是不可少的,但当你不需要给出表名时,可以省略它。例如,下面这条查询语句只显示一些表达式的值。这些值的计算并未涉及任何表,因此这里不需要FROM子句:

mysql> **SELECT 2+2, 'Hello, world', VERSION();  
**+-----+--------------+------------+  
| 2+2 | Hello, world | VERSION()  |  
+-----+--------------+------------+  
|   4 | Hello, world | 5.5.30-log |  
+-----+--------------+------------+

当的确需要使用FROM子句来指定要从哪个表检索数据时,还需要指明要查看哪些列。SELECT语句最常见的一种形式是使用一个星号(*)作为列说明符,代表“所有列”。下面这条查询语句将显示student表里的所有列:

mysql> **SELECT * FROM student;  
**+-----------+-----+------------+  
| name      | sex | student_id |  
+-----------+-----+------------+  
| Megan     | F   |          1 |  
| Joseph    | M   |          2 |  
| Kyle      | M   |          3 |  
| Katie     | F   |          4 |  
...

这些列将按它们在表里的存储顺序依次显示出来。这个顺序与你用DESCRIBE student语句看到的列顺序是一致的。(示例末尾处的省略号“...”表示该查询返回的行实际上有很多。)

也可以把自己想要查看的那些列的名字列出来。例如,只想查看学生姓名,则可以这样做:

mysql> **SELECT name FROM student;  
**+-----------+  
| name      |  
+-----------+  
| Megan     |  
| Joseph    |  
| Kyle      |  
| Katie     |  
...

如果要列出多个列名,那么需要使用逗号把它们分隔开。下面这条语句等价于SELECT ``*`` FROM student语句,但它把各列的名字明确地列了出来:

mysql> **SELECT name, sex, student_id FROM student;  
**+-----------+-----+------------+  
| name      | sex | student_id |  
+-----------+-----+------------+  
| Megan     | F   |          1 |  
| Joseph    | M   |          2 |  
| Kyle      | M   |          3 |  
| Katie     | F   |          4 |  
...

你可以按任意顺序列出各个列名:

SELECT name, student_id FROM student;  
SELECT student_id, name FROM student;

只要你愿意,甚至还可以重复列出某一列的名字,只是这样做通常没什么意义。

另外,还可以从一个以上的表里选取列,即多表“连接”(join)。更多关于连接的信息请参考1.4.9.10节。

一方面,在MySQL里,列名不区分大小写,因此下面这些检索语句都是等价的:

SELECT name, student_id FROM student;  
SELECT NAME, STUDENT_ID FROM student;  
SELECT nAmE, sTuDeNt_Id FROM student;

另一方面,数据库名和表名可能是区分大小写的。具体情况取决于服务器主机所使用的文件系统,以及MySQL的配置。Windows系统的文件名不区分大小写,所以运行在它上面的服务器也不区分数据库名和表名的大小写。在Unix系统上,文件名通常都区分大小写,因此运行在它上面的服务器会区分数据库名和表名的大小写。Mac OS X系统的扩展文件系统比较特殊,它不区分大小写。

如果想让MySQL服务器不区分数据库名和表名的大小写,那么可以对它进行配置。更多详细信息请参考11.2.6节。

1.4.9.1 指定检索条件

要想限制SELECT语句检索出来的行数,可以使用WHERE子句,指定列值所必须满足的检索条件。例如,可以搜索某个范围内的数值:

mysql> **SELECT * FROM score WHERE score > 95;  
**+------------+----------+-------+  
| student_id | event_id | score |  
+------------+----------+-------+  
|          5 |        3 |    97 |  
|         18 |        3 |    96 |  
|          1 |        6 |   100 |  
|          5 |        6 |    97 |  
|         11 |        6 |    98 |  
|         16 |        6 |    98 |  
+------------+----------+-------+

可以查找包含字符数据的字符串值。对于默认的字符集和排序方式,字符串的比较操作通常不区分大小写:

mysql> **SELECT last_name, first_name FROM president  
**    -> **WHERE last_name='ROOSEVELT';  
**+-----------+-------------+  
| last_name | first_name  |  
+-----------+-------------+  
| Roosevelt | Theodore    |  
| Roosevelt | Franklin D. |  
+-----------+-------------+  
mysql> **SELECT last_name, first_name FROM president  
**    -> **WHERE last_name='roosevelt';  
**+-----------+-------------+  
| last_name | first_name  |  
+-----------+-------------+  
| Roosevelt | Theodore    |  
| Roosevelt | Franklin D. |  
+-----------+-------------+

也可以查找日期:

mysql> **SELECT last_name, first_name, birth FROM president  
**    -> **WHERE birth < '1750-1-1';  
**+------------+------------+------------+  
| last_name  | first_name | birth      |  
+------------+------------+------------+  
| Washington | George     | 1732-02-22 |  
| Adams      | John       | 1735-10-30 |  
| Jefferson  | Thomas     | 1743-04-13 |  
+------------+------------+------------+

甚至还可以查找组合值:

mysql> **SELECT last_name, first_name, birth, state FROM president  
**    -> **WHERE birth < '1750-1-1' AND (state='VA' OR state='MA');  
**+------------+------------+------------+-------+  
| last_name  | first_name | birth      | state |  
+------------+------------+------------+-------+  
| Washington | George     | 1732-02-22 | VA    |  
| Adams      | John       | 1735-10-30 | MA    |  
| Jefferson  | Thomas     | 1743-04-13 | VA    |  
+------------+------------+------------+-------+

WHERE子句里的表达式允许使用算术运算符(见表1-1)、比较运算符(见表1-2)和逻辑运算符(见表1-3)。在表达式里还可以使用括号。在运算时,可以使用常量、表列和函数调用。本教程的语句里会用到一些MySQL的函数,但由于函数比较多,这里无法一一列出。有关这些函数的详细信息请参考附录C。

表1-1 算术运算符

运算符

含  义

+

加法

-

减法

*

乘法

/

除法

DIV

整除

%

模运算(除法余数)

 

 

表1-2 比较运算符

运算符

含  义

<

小于

<=

小于等于(不大于)

=

等于

<=>

等于(可用于NULL值)

<> 或 !=

不等于

>=

大于等于(不小于)

>

大于

表1-3 逻辑运算符

运算符

含  义

AND

逻辑与

OR

逻辑或

XOR

逻辑异或

NOT

逻辑非

当需要在查询语句里使用逻辑运算符时,千万要注意:逻辑运算符AND与人们日常生活中所说的“和”在含义上是不一样的。假设你想要找出“出生于弗吉尼亚州和马萨诸塞州的总统”。这里用到了“和”字,它似乎是在暗示你该编写如下查询语句:

mysql>** SELECT last_name, first_name, state FROM president  
**    ->** WHERE state='VA' AND state='MA';  
**Empty set (0.01 sec)

空的结果清楚表明,这条语句没起作用。为什么没起作用呢?因为这条查询语句的真正含义是“把同时出生于弗吉尼亚州和马萨诸塞州的总统”找出来,而这是不可能的。在日常生活里,你可以用“和”来表达你的查询条件;但在SQL里,必须使用OR来连接这两个条件:

mysql> **SELECT last_name, first_name, state FROM president**  
    -> **WHERE state='VA' OR state='MA';  
**+------------+-------------+-------+  
| last_name  | first_name  | state |  
+------------+-------------+-------+  
| Washington | George      | VA    |  
| Adams      | John        | MA    |  
| Jefferson  | Thomas      | VA    |  
| Madison    | James       | VA    |  
| Monroe     | James       | VA    |  
| Adams      | John Quincy | MA    |  
| Harrison   | William H.  | VA    |  
| Tyler      | John        | VA    |  
| Taylor     | Zachary     | VA    |  
| Wilson     | Woodrow     | VA    |  
| Kennedy    | John F.     | MA    |  
| Bush       | George H.W. | MA    |  
+------------+-------------+-------+

请大家务必注意日常语言与SQL语言之间的差异,不只是在自己编写查询语句时要引起注意,在为其他人编写查询时也要注意。一定要仔细倾听别人对查询内容的描述,不能将他们的描述照搬成SQL的逻辑运算符。以刚描述的那个查询为例,与查询语句相当的自然语言表述应该是:“把出生于弗吉尼亚州或马萨诸塞州的总统找出来”。

当在组织可以查找到多个独立值的查询语句时,你可能会发现,使用IN()运算符更简洁。在使用IN()之后,前面的那个查询可以改写成下面这个样子:

SELECT last_name, first_name, state FROM president  
WHERE state IN('VA','MA');

当把一个列与大量值进行比较时,使用IN()会特别方便。

1.4.9.2 NULL

NULL值很特殊。其含义是“无值”或“未知值”,所以不能采用两个“已知值”的比较方式,将它与“已知值”进行比较。如果试图将NULL与常规的算术比较运算符一起使用,那么其结果将是未定义的(undefined):

mysql> **SELECT NULL < 0, NULL = 0, NULL <> 0, NULL > 0;  
**+----------+----------+-----------+----------+  
| NULL < 0 | NULL = 0 | NULL <> 0 | NULL > 0 |  
+----------+----------+-----------+----------+  
|     NULL |     NULL |      NULL |     NULL |  
+----------+----------+-----------+----------+

事实上,你也不能让NULL与其自身进行比较,因为两个“未知值”的比较结果是无法确定的:

mysql> **SELECT NULL = NULL, NULL <> NULL;  
**+-------------+--------------+  
| NULL = NULL | NULL <> NULL |  
+-------------+--------------+  
|        NULL |         NULL |  
+-------------+--------------+

如果需要测试多个NULL值是否相等,那么必须使用IS NULLIS NOT NULL,而不能使用=、<>或者!=。例如,对于目前仍然健在的美国总统,其逝世日期在president表里表示为NULL。如果想要找到他们,可以使用下面这条查询语句:

mysql> **SELECT last_name, first_name FROM president WHERE death IS NULL;  
**+-----------+-------------+  
| last_name | first_name  |  
+-----------+-------------+  
| Carter    | James E.    |  
| Bush      | George H.W. |  
| Clinton   | William J.  |  
| Bush      | George W.   |  
| Obama     | Barack H.   |  
+-----------+-------------+

IS NOT NULL可以用来查找非NULL值。下面这条查询语句可以找到那些具有后缀名的名字:

mysql> **SELECT last_name, first_name, suffix  
**    -> **FROM president WHERE suffix IS NOT NULL;  
**+-----------+------------+--------+  
| last_name | first_name | suffix |  
+-----------+------------+--------+  
| Carter    | James E.   | Jr.    |  
+-----------+------------+--------+

MySQL特有的<=>比较运算符可用于NULLNULL的比较。将前面两个查询语句改写成使用这个运算符的查询语句:

SELECT last_name, first_name FROM president WHERE death <=> NULL;  

SELECT last_name, first_name, suffix  
FROM president WHERE NOT (suffix <=> NULL);

1.4.9.3 对查询结果排序

每位MySQL用户最终都会注意到这样一种情况:在你创建一个表,并存入一些行之后,使用 “SELECT ``* ``FROMtbl_name” 语句查询出的行,其顺序通常与插入它们时的顺序一致。这很符合人们的思维习惯,人们自然会认定查询出的行的顺序与插入它们时的顺序是相同的。但实际情况并非这样。因为在表数据初始加载完之后,删除和插入行都会改变服务器返回行的顺序。

关于行检索顺序,请记住这样一条原则:服务器不会保证返回行的先后顺序,除非你自己指定顺序。要想结果有序,需要在查询语句后面增加一条ORDER BY子句。下面这条查询语句会按姓的字母顺序返回美国总统的姓名:

mysql>** SELECT last_name, first_name FROM president  
**    ->** ORDER BY last_name;  
**+------------+---------------+  
| last_name  | first_name    |  
+------------+---------------+  
| Adams      | John Quincy   |  
| Adams      | John          |  
| Arthur     | Chester A.    |  
| Buchanan   | James         |  
...

ORDER BY子句的默认排序方式是升序排列。在其中的列名后面加上关键字ASCDESC,可以指定是按照升序排列还是按照降序排列。例如,想让美国总统的姓名按姓的逆序(降序)排列显示,那么就要使用DESC关键字:

mysql> **SELECT last_name, first_name FROM president  
**    -> **ORDER BY last_name DESC;  
**+------------+---------------+  
| last_name  | first_name    |  
+------------+---------------+  
| Wilson     | Woodrow       |  
| Washington | George        |  
| Van Buren  | Martin        |  
| Tyler      | John          |  
...

你可以对多列进行排序,而且每一列单独地按升序或降序排列。下面的查询语句用于检索president表里的行,先按出生地所在州进行逆序排列,然后在每一个相同的州里再按姓升序排列:

mysql> **SELECT last_name, first_name, state FROM president  
**    -> **ORDER BY state DESC, last_name ASC;  
**+------------+---------------+-------+  
| last_name  | first_name    | state |  
+------------+---------------+-------+  
| Arthur     | Chester A.    | VT    |  
| Coolidge   | Calvin        | VT    |  
| Harrison   | William H.    | VA    |  
| Jefferson  | Thomas        | VA    |  
| Madison    | James         | VA    |  
| Monroe     | James         | VA    |  
| Taylor     | Zachary       | VA    |  
| Tyler      | John          | VA    |  
| Washington | George        | VA    |  
| Wilson     | Woodrow       | VA    |  
| Eisenhower | Dwight D.     | TX    |  
| Johnson    | Lyndon B.     | TX    |  
...

在一个列里,对于升序排列,NULL值总是出现在开头;而对于降序排列,它总是出现在末尾。为确保NULL值出现在指定排列顺序的末尾,需要额外增加一个可以区分NULL值和非NULL值的排序列。例如,想按逝世日期降序排列所有总统,那么当前健在的(逝世日期为NULL的)那些总统就应该出现在结果顺序的末尾。而如果想让他们出现在开头,就要使用下面这条查询语句:

mysql>** SELECT last_name, first_name, death FROM president  
**    ->** ORDER BY IF(death IS NULL,0,1), death DESC, last_name;   
**+------------+---------------+------------+  
| last_name  | first_name    | death      |  
+------------+---------------+------------+  
| Bush       | George W.     | NULL       |  
| Bush       | George H.W.   | NULL       |  
| Carter     | James E.      | NULL       |  
| Clinton    | William J.    | NULL       |  
| Obama      | Barack H.     | NULL       |  
| Ford       | Gerald R.     | 2006-12-26 |  
| Reagan     | Ronald W.     | 2004-06-05 |  
| Nixon      | Richard M.    | 1994-04-22 |  
...  
| Adams      | John          | 1826-07-04 |  
| Jefferson  | Thomas        | 1826-07-04 |  
| Washington | George        | 1799-12-14 |  
+------------+---------------+------------+

其中, IF()函数的作用是计算第一个参数给出的那个表达式的值,然后根据计算结果的真假来决定是返回第二个参数(为真),还是返回第三个参数(为假)。对于这条查询语句,当遇到NULL值时, IF()函数的计算结果为0;当遇到非NULL值时,它计算结果为1。最终结果会把所有的NULL值放到非NULL值的前面。同时,对于death值相同的行,将last_name作为辅助列按姓继续进行排序。

1.4.9.4 限制查询结果

查询结果往往有很多行,如果只想看到其中的一小部分,那么可以在查询命令里增加一条LIMIT子句。如果将它与ORDER BY子句联合使用,效果会特别好。MySQL允许限制查询输出的行数,只输出结果中前面的n行。下面的查询语句将把按出生日期排在前5位的总统列出来:

mysql> **SELECT last_name, first_name, birth FROM president  
**    -> **ORDER BY birth LIMIT 5;  
**+------------+------------+------------+  
| last_name  | first_name | birth      |  
+------------+------------+------------+  
| Washington | George     | 1732-02-22 |  
| Adams      | John       | 1735-10-30 |  
| Jefferson  | Thomas     | 1743-04-13 |  
| Madison    | James      | 1751-03-16 |  
| Monroe     | James      | 1758-04-28 |  
+------------+------------+------------+

如果用DESC来逆序排列查询结果,那么可以得到最晚出生的那5位总统:

mysql> **SELECT last_name, first_name, birth FROM president**  
    -> **ORDER BY birth DESC LIMIT 5;  
**+-----------+-------------+------------+  
| last_name | first_name  | birth      |  
+-----------+-------------+------------+  
| Obama     | Barack H.   | 1961-08-04 |  
| Clinton   | William J.  | 1946-08-19 |  
| Bush      | George W.   | 1946-07-06 |  
| Carter    | James E.    | 1924-10-01 |  
| Bush      | George H.W. | 1924-06-12 |  
+-----------+-------------+------------+

LIMIT子句还允许从查询结果的中间抽出部分行。此时需要指定两个值:第一个,给出从查询结果的开头部分跳过的行数目;第二个,需要返回的行数目。下面这条查询语句与前面那条很相似,但它返回的是跳过前面10行之后的5行:

mysql> **SELECT last_name, first_name, birth FROM president  
**    -> **ORDER BY birth DESC LIMIT 10, 5;  
**+-----------+-------------+------------+  
| last_name | first_name  | birth      |  
+-----------+-------------+------------+  
| Eisenhower| Dwight D.   | 1890-10-14 |  
| Truman    | Harry S     | 1884-05-08 |  
| Roosevelt | Franklin D. | 1882-01-30 |  
| Hoover    | Herbert C.  | 1874-08-10 |  
| Coolidge  | Calvin      | 1872-07-04 |  
+-----------+-------------+------------+

如果想从某个表里随机抽取出一行或几行,那么可以联合使用LIMIT子句和ORDER BY RAND()子句:

mysql> **SELECT last_name, first_name FROM president**  
    -> **ORDER BY RAND() LIMIT 1;  
**+-----------+------------+  
| last_name | first_name |  
+-----------+------------+  
| Johnson   | Lyndon B.  |  
+-----------+------------+  
mysql> **SELECT last_name, first_name FROM president  
**    -> **ORDER BY RAND() LIMIT 3;  
**+-----------+-------------+  
| last_name | first_name  |  
+-----------+-------------+  
| Harding   | Warren G.   |  
| Bush      | George H.W. |  
| Jefferson | Thomas      |  
+-----------+-------------+

1.4.9.5 对输出列进行计算和命名

到目前为止,大部分查询语句都是直接通过检索表中的值来获得输出结果。MySQL也支持根据表达式的结果计算输出值,引不引用表都可以。下面的这条查询语句计算了一个简单表达式(一个常量)和一个复杂表达式(它使用了几种算术运算和两个函数调用,这两个函数一个用于计算表达式的平方根,另外一个用于将结果格式化成保留3位小数):

mysql> **SELECT 17, FORMAT(SQRT(25+13),3);  
**+----+-----------------------+  
| 17 | FORMAT(SQRT(25+13),3) |  
+----+-----------------------+  
| 17 | 6.164                 |  
+----+-----------------------+

在表达式里也可以使用表列,如下所示:

mysql> **SELECT CONCAT(first_name,' ',last_name), CONCAT(city,', ',state)**  
    -> **FROM president;  
**+----------------------------------+-------------------------+  
| CONCAT(first_name,' ',last_name) | CONCAT(city,', ',state) |  
+----------------------------------+-------------------------+  
| George Washington                | Wakefield, VA           |  
| John Adams                       | Braintree, MA           |  
| Thomas Jefferson                 | Albemarle County, VA    |  
| James Madison                    | Port Conway, VA         |  
...

这个查询对总统的名字进行了格式化,将名和姓用空格连接成了一个字符串。对他们的出生地也进行了格式,将所在城市和州用逗号和空格连接成了一个字符串。

计算某列的值的表达式会成为该列的名字,并被用作输出结果的标题。如果表达式很长(如前面那个查询示例所示),那么它会使输出列的宽度变得很大。为使输出更具意义和可读性,你可以利用AS name结构为该列分配另一个名字(也称“别名”):

mysql> **SELECT CONCAT(first_name,' ',last_name) AS Name,**  
    -> **CONCAT(city,', ',state) AS Birthplace  
**    -> **FROM president;  
**+-----------------------+-------------------------+  
| Name                  | Birthplace              |  
+-----------------------+-------------------------+  
| George Washington     | Wakefield, VA           |  
| John Adams            | Braintree, MA           |  
| Thomas Jefferson      | Albemarle County, VA    |  
| James Madison         | Port Conway, VA         |  
...

如果输出列的别名里包含空格,那么必须给它加上引号:

mysql> **SELECT CONCAT(first_name,' ',last_name) AS 'President Name',**  
    -> **CONCAT(city,', ',state) AS 'Place of Birth'  
**    -> **FROM president;  
**+-----------------------+-------------------------+  
| President Name        | Place of Birth          |  
+-----------------------+-------------------------+  
| George Washington     | Wakefield, VA           |  
| John Adams            | Braintree, MA           |  
| Thomas Jefferson      | Albemarle County, VA    |  
| James Madison         | Port Conway, VA         |  
...

在为列提供别名时,可以省略关键字AS

mysql> **SELECT 1**** one****, 2 two, 3 three;  
**+---+-----+-------+  
|one| two | three |  
+---+-----+-------+  
| 1 |   2 |     3 |  
+---+-----+-------+

如果一个查询结果的列名有误,或者有列丢失,那么请检查在某两个列之间是否忘了加上逗号。如果真是这种情况,那么第二列会被当成第一列的别名。例如,你原打算编写一个查询语句,选取下列总统的姓名,可惜不小心漏掉了first_namelast_name两列之间的逗号。结果,first_name列被误命名为last_name,而列last_name列却不见了:

mysql> **SELECT first_name last_name FROM president;  
+---------------+  
**| last_name     |  
+---------------+  
| George        |  
| John          |  
| Thomas        |  
| James         |  
...

1.4.9.6 处理日期

在MySQL里使用日期时,千万要记住的是年份总是在最前面。在写2012年7月27日这个日期时,请将其写成'2012-07-27'。不要像日常生活中那样,将它写成'07-27-2012''27-07-2012'。对于其他格式的输入值,或许能使用STR_TO_DATE()函数来进行转换。具体示例请参考3.2.6节。

MySQL支持多种类型的日期运算。

  • 按日期排序(我们已经操作过多次)。
  • 搜索特定日期或日期范围。
  • 从日期值里提取各组成部分,如年、月或日。
  • 计算两个日期之间的时间差。
  • 通过将一个日期加上或减去一个时间间隔,计算出另一个日期。

下面是一些与日期运算有关的查询示例。

为了通过确切值或通过另一日期的相对值,查询出特定的日期来,可以将某个DATE列与你感兴趣的那个日期值进行比较:

mysql> **SELECT * FROM grade_event WHERE date = '2012-10-01';  
**+------------+----------+----------+  
| date       | category | event_id |  
+------------+----------+----------+  
| 2012-10-01 | T        |        6 |  
+------------+----------+----------+  
mysql> **SELECT last_name, first_name, death  
**    -> **FROM president  
**    -> **WHERE death >= '1970-01-01' AND death < '1980-01-01';  
**+-----------+------------+------------+  
| last_name | first_name | death      |  
+-----------+------------+------------+  
| Truman    | Harry S    | 1972-12-26 |  
| Johnson   | Lyndon B.  | 1973-01-22 |  
+-----------+------------+------------+

为测试或检索日期的各个部分,可以使用像YEAR()MONTH()DAYOFMONTH()这样的函数。例如,下面这个查询可以把生于3月的美国总统查找出来:

mysql> **SELECT last_name, first_name, birth  
**    -> **FROM president WHERE MONTH(birth) = 3;**  
+-----------+------------+------------+  
| last_name | first_name | birth      |  
+-----------+------------+------------+  
| Madison   | James      | 1751-03-16 |  
| Jackson   | Andrew     | 1767-03-15 |  
| Tyler     | John       | 1790-03-29 |  
| Cleveland | Grover     | 1837-03-18 |  
+-----------+------------+------------+

也可以用月份名称来改写这个查询:

mysql> **SELECT last_name, first_name, birth  
**    -> **FROM president WHERE MONTHNAME(birth) = 'March';  
**+-----------+------------+------------+  
| last_name | first_name | birth      |  
+-----------+------------+------------+  
| Madison   | James      | 1751-03-16 |  
| Jackson   | Andrew     | 1767-03-15 |  
| Tyler     | John       | 1790-03-29 |  
| Cleveland | Grover     | 1837-03-18 |  
+-----------+------------+------------+

再进一步,把函数MONTH()DAYOFMONTH()结合起来使用,找出生于3月某一天的总统:

mysql> **SELECT last_name, first_name, birth  
**    -> **FROM president WHERE MONTH(birth) = 3 AND DAYOFMONTH(birth) = 29;**  
+-----------+------------+------------+  
| last_name | first_name | birth      |  
+-----------+------------+------------+  
| Tyler     | John       | 1790-03-29 |  
+-----------+------------+------------+

你偶尔会看到类似“今日名人”之类的信息,用上面的查询语句就能生成一份这样的名单。不过,如果你的查询与“当前日期”有关,那么大可不必像前面的例子那样插入一个具体的日期值。不管今日是一年里的哪一天,只要将各位总统的生日与CURDATE()函数(该函数总是返回当前日期)里的月和日进行比较,便可查出“今日出生的”总统,如下所示:

SELECT last_name, first_name, birth  
FROM president WHERE MONTH(birth) = MONTH(CURDATE())  
AND DAYOFMONTH(birth) = DAYOFMONTH(CURDATE());

如果想知道两个日期值之间的时间间隔,可以将它们相减。例如,想要知道哪位总统活得最久,那么可以用他们的逝世日期减去出生日期。此时,TIMESTAMPDIFF()函数就派上用场了,因为它有一个参数,可以指定计算结果的单位(在本例里为年,即YEAR):

mysql> **SELECT last_name, first_name, birth, death,**  
    -> **TIMESTAMPDIFF(YEAR, birth, death) AS age  
**    -> **FROM president WHERE death IS NOT NULL  
**    -> **ORDER BY age DESC LIMIT 5;  
**+-----------+------------+------------+------------+------+  
| last_name | first_name | birth      | death      | age  |  
+-----------+------------+------------+------------+------+  
| Reagan    | Ronald W.  | 1911-02-06 | 2004-06-05 |   93 |  
| Ford      | Gerald R.  | 1913-07-14 | 2006-12-26 |   93 |  
| Adams     | John       | 1735-10-30 | 1826-07-04 |   90 |  
| Hoover    | Herbert C. | 1874-08-10 | 1964-10-20 |   90 |  
| Truman    | Harry S    | 1884-05-08 | 1972-12-26 |   88 |  
+-----------+------------+------------+------------+------+

如果想要计算相差的天数,则需要使用另一种计算两个日期之间时间间隔的方法:使用TO_DAYS()函数,将日期转换为天数。检测出与某个参考日期相差有多少天是该函数的一种主要应用。例如,为找出近期需要更新成员资格的“美史联盟”成员,可以将成员的有效日期与当前日期相减。如果其结果小于某个阈值,即表明该成员的资格快要到期了。下面这条查询语句可以将成员资格已过期的以及在60天内将到期的成员查找出来:

SELECT last_name, first_name, expiration FROM member  
WHERE (TO_DAYS(expiration) - TO_DAYS(CURDATE())) < 60;

使用TIMESTAMPDIFF()函数的等效语句,如下所示:

SELECT last_name, first_name, expiration FROM member  
WHERE TIMESTAMPDIFF(DAY, CURDATE(), expiration) < 60;

要根据某个日期计算出另一个日期,可以使用函数DATE_ADD()DATE_SUB()。这两个函数的参数都是一个日期值和一个时间间隔,然后返回一个新的日期值。例如:

mysql> **SELECT DATE_ADD('1970-1-1', INTERVAL 10 YEAR);**  
+----------------------------------------+  
| DATE_ADD('1970-1-1', INTERVAL 10 YEAR) |  
+----------------------------------------+  
| 1980-01-01                             |  
+----------------------------------------+  
mysql> **SELECT DATE_SUB('1970-1-1', INTERVAL 10 YEAR);  
**+----------------------------------------+  
| DATE_SUB('1970-1-1', INTERVAL 10 YEAR) |  
+----------------------------------------+  
| 1960-01-01                             |  
+----------------------------------------+

在本节里的前面有一个查询,它选取那些逝世于20世纪70年代的美国总统,使用文字量型的日期值作为选取范围的结束点。可以重写该查询语句,使用一个文字量型的起点日期,然后在起点日期上加上一个时间间隔计算得出终点日期:

mysql> **SELECT last_name, first_name, death  
**    -> **FROM president  
**    -> **WHERE death >= '1970-1-1'  
**    -> **AND death < DATE_ADD('1970-1-1', INTERVAL 10 YEAR);  
**+-----------+------------+------------+  
| last_name | first_name | death      |  
+-----------+------------+------------+  
| Truman    | Harry S    | 1972-12-26 |  
| Johnson   | Lyndon B.  | 1973-01-22 |  
+-----------+------------+------------+

另外还有一种编写“成员资格更新”查询的方法,即使用DATE_ADD()函数:

SELECT last_name, first_name, expiration FROM member  
WHERE expiration < DATE_ADD(CURDATE(), INTERVAL 60 DAY);

如果expiration列经过了索引,那么这个查询将会比前一个更有效率。具体原因请参考第5章。

大概在本章开头,有下面这样一个查询,用于找出“在牙医诊所的患者中,有哪些患者在到期后还未来复诊”:

SELECT last_name, first_name, last_visit FROM patient  
WHERE last_visit < DATE_SUB(CURDATE(), INTERVAL 6 MONTH);

或许当时你还不太能看懂这个查询。那么现在是否已完全明白了呢?

1.4.9.7 模式匹配

MySQL支持模式匹配操作,这使我们能够在未给出精确比较值的情况下把行查出来。模式匹配需要使用像LIKENOT LIKE那样的运算符,并且需要指定一个包含通配字符的字符串。下划线“_”可以匹配任何的单个字符,百分号“%”则能匹配任何字符序列(其中包括空序列)。

下面这个模式能匹配到所有以字母Ww开头的姓:

mysql> **SELECT last_name, first_name FROM president  
**    -> **WHERE last_name LIKE 'W%';  
**+------------+------------+  
| last_name  | first_name |  
+------------+------------+  
| Washington | George     |  
| Wilson     | Woodrow    |  
+------------+------------+

下面这个查询展示了一种常见错误。这个“模式匹配”不会返回任何内容,因为它没有使用带LIKE的模式,而是使用了带算术比较运算符的模式。

mysql> **SELECT last_name, first_name FROM president**  
    -> **WHERE last_name = 'W%';  
**Empty set (0.00 sec)

上面这个比较表达式唯一成功的地方是该列正好包含了字符串'W%''w%'

下面这个模式将与那些包含有'W%''w%'(并不仅限于第一个字母)的姓相匹配:

mysql> **SELECT last_name, first_name FROM president**  
    -> **WHERE last_name LIKE '%W%';  
**+------------+------------+  
| last_name  | first_name |  
+------------+------------+  
| Washington | George     |  
| Wilson     | Woodrow    |  
| Eisenhower | Dwight D.  |  
+------------+------------+

下面这个模式将与恰好包含4个字母的姓相匹配,如下所示:

mysql> **SELECT last_name, first_name FROM president  
**    -> **WHERE last_name LIKE '____';  
**+-----------+-------------+  
| last_name | first_name  |  
+-----------+-------------+  
| Polk      | James K.    |  
| Taft      | William H.  |  
| Ford      | Gerald R.   |  
| Bush      | George H.W. |  
| Bush      | George W.   |  
+-----------+-------------+

MySQL还提供另一种基于正则表达式(regular expression)和REGEXP运算符的模式匹配。关于LIKEREGEXP的更多信息请参考3.5.1.1节和附录C。

1.4.9.8 设置和使用自定义变量

MySQL支持你定义自己的变量。这些变量可以被设置为查询结果,这使我们可以方便地把一些值存储起来以供今后的查询使用。假设想知道在Andrew Jackson总统之前出生的总统有哪些。可以先将他的出生日期检索出来,并存储到一个变量里,然后再将出生日期早于该变量值的其他总统查找出来:

mysql> **SELECT @ Jackson********_********birth := birth FROM president  
**    -> **WHERE last_name = 'Jackson' AND first_name = 'Andrew';  
**+------------------------+  
| @jackson******_******birth := birth |  
+------------------------+  
| 1767-03-15             |  
+------------------------+  
mysql> **SELECT last_name, first_name, birth FROM president  
**    -> **WHERE birth < @****jackson********_********birth ORDER BY birth;  
**+------------+------------+------------+  
| last_name  | first_name | birth      |  
+------------+------------+------------+  
| Washington | George     | 1732-02-22 |  
| Adams      | John       | 1735-10-30 |  
| Jefferson  | Thomas     | 1743-04-13 |  
| Madison    | James      | 1751-03-16 |  
| Monroe     | James      | 1758-04-28 |  
+------------+------------+------------+

自定义变量的语法为“@变量名”。在SELECT语句里,赋值语法是形如“@变量名:=值”的表达式。因此,上面的第一个查询主要负责把Andrew Jackson总统的出生日期查出来,并把它赋给一个名为@jackson_birth的变量。这条SELECT语句的查询结果仍会被显示出来;而将查询结果赋给变量的过程并不会阻碍该查询的输出显示。第二个查询会引用该变量,并在president表里找出birth值小于该变量值的那些行。

实际上,前面的那个问题可以通过一条使用连接或子查询的查询语句来解决,但这里不想对此有过多的讨论。有时,使用一个变量可能会更容易让人理解。更多关于子查询的信息请参考1.4.9.10节。

也可以使用SET`语句来对变量进行赋值。此时,“=”和“:=”都可以用作赋值运算符:

mysql>** SET @today = CURDATE();  
**mysql>** SET @one_week_ago := DATE_SUB(@today, INTERVAL 7 DAY);  
**mysql>** SELECT @today, @one_week_ago;  
**+------------+---------------+  
| @today     | @one_week_ago |  
+------------+---------------+  
| 2012-04-21 | 2012-04-14    |  
+------------+---------------+

1.4.9.9 生成统计信息

MySQL最有用的一项功能就是,能够对大量原始数据进行归纳和统计。大家都明白,单纯依靠人工手段来生成统计信息是一项既枯燥耗时,又易出错的工作。如果大家能学会使用MySQL来生成各种统计信息,那么它将会成为你的得力助手。

在一组值里把各个唯一值找出来,这是一项典型的统计工作。可以使用DISTINCT关键字清除查询结果里重复出现的行。例如,下面的查询可以将美国历任总统出生地所在的州不加重复地列举出来:

mysql> **SELECT DISTINCT state FROM president ORDER BY state;**  
+-------+  
| state |  
+-------+  
| AR    |  
| CA    |  
| CT    |  
| GA    |  
| HI    |  
| IA    |  
| IL    |  
| KY    |  
| MA    |  
| MO    |  
...

另一种形式的统计是计数,需要使用COUNT()函数。如果使用COUNT(*),那么计算出来的结果将是查询所选取到的行数。如果查询语句没有带WHERE子句,那么它会查询所有行,因此,COUNT(*)计算出来的结果就是表的行数。下面这个查询可以显示出“美史联盟”的member表里包含多少行:

mysql> **SELECT COUNT(*) FROM member;  
**+----------+  
| COUNT(*) |  
+----------+  
|      102 |  
+----------+

如果查询语句带有WHERE子句,那么COUNT(*)计算出来的结果就是该子句匹配到了多少行。下面这个查询可以显示出“到目前为止,你的班级已进行了多少次测验”:

mysql> **SELECT COUNT(*) FROM grade_event WHERE category = 'Q';  
**+----------+  
| COUNT(*) |  
+----------+  
|        4 |  
+----------+

COUNT(*)函数会统计所有被查询到的行数。与之相对的是,COUNT(列名)只会统计所有非NULL值的数目。下面这个查询展示了两个函数的区别:

mysql> **SELECT COUNT(*), COUNT(email), COUNT(expiration) FROM member;  
**+----------+--------------+-------------------+  
| COUNT(*) | COUNT(email) | COUNT(expiration) |  
+----------+--------------+-------------------+  
|      102 |           80 |                96 |  
+----------+--------------+-------------------+

从上面的查询结果可以看出, member表目前共有102行,其中只有80行在email列里有值。它同时还显示出有6名成员具有终身成员资格。(expiration列里的NULL值表示具有终身成员资格,并且由于在102条记录里有96条不为NULL,因此剩下的6条必然属于终身成员。)

组合使用COUNT()与DISTINCT,可以统计出在查询结果里有多少个不同的非NULL值。例如,想要知道美国共有多少个不同的州曾经诞生过总统,那么可以使用下面这条查询语句:

mysql> **SELECT COUNT(DISTINCT state) FROM president;  
**+-----------------------+  
| COUNT(DISTINCT state) |  
+-----------------------+  
|                    21 |  
+-----------------------+

你可以对某个数据列进行全面统计,也可以对该列进行分类统计。例如,使用下面这个查询,可以确定出班级里总共有多少名学生:

mysql> **SELECT COUNT(*) FROM student;  
**+----------+  
| COUNT(*) |  
+----------+  
|       31 |  
+----------+

不过,班级里的男生和女生分别是多少呢?有一种办法可以找到答案,即按性别分别进行统计:

mysql> **SELECT COUNT(*) FROM student WHERE sex='f';  
**+----------+  
| COUNT(*) |  
+----------+  
|       15 |  
+----------+  
mysql> **SELECT COUNT(*) FROM student WHERE sex='m';  
**+----------+  
| COUNT(*) |  
+----------+  
|       16 |  
+----------+

这个办法可行,但比较麻烦,而且很不适合于有多种不同列值的情形。假设如何采用这种方式来确定出生自美国各个州的总统人数。首先,你必须一个不少地把涉及的州全部找出来(使用查询语句SELECT DISTINCT state FROM president),然后,再针对各州执行查询语句SELECT COUNT(*)来统计出最终结果。显然这是你想避免的事情。

幸运的是,只用一个查询便可以统计出某一列里的不同值分别出现过多少次。对于那个学生列表,可以使用GROUP BY子句来分别统计男、女学生的人数,如下所示:

mysql> **SELECT sex, COUNT(*) FROM student GROUP BY sex;  
**+-----+----------+  
| sex | COUNT(*) |  
+-----+----------+  
| F   |       15 |  
| M   |       16 |  
+-----+----------+

使用同样的查询形式,可以分别统计出生自各州的总统人数,如下所示:

mysql> **SELECT state, COUNT(*) FROM president GROUP BY state;  
**+-------+----------+  
| state | COUNT(*) |  
+-------+----------+  
| AR    |        1 |  
| CA    |        1 |  
| CT    |        1 |  
| GA    |        1 |  
| HI    |        1 |
| IA    |        1 |
| IL    |        1 |  
| KY    |        1 |  
| MA    |        4 |  
| MO    |        1 |  
...

在采用这种方式进行分组统计时, GROUP BY子句会告知MySQL在统计之前应该如何对值进行分组。

与分别统计某列的不同值所出现次数的做法相比,将COUNT(*)函数与GROUP BY子句结合在一起用于进行分组统计的做法有很多优点。

  • 不用事先知道被统计列里有些什么值。
  • 只需一个查询语句。
  • 因为只用一个查询便能获得所有的结果,所以可以对输出进行排序。

前两项优点的重要性体现在:它们有助于简化查询语句的表达。第3项优点的重要性则体现在:它能以更灵活的方式显示查询结果。默认情况下,MySQL会根据GROUP BY子句里的列名来对查询结果进行排序,但你也可以用ORDER BY子句指定一个特定的排序顺序。例如,你想获得按出生地所在州分组后的总统人数,并按人数从多到少的顺序排列出来,于是可以多加一个ORDER BY子句,如下所示:

mysql> **SELECT state, COUNT(*) AS count FROM president  
**    -> **GROUP BY state ORDER BY count DESC;  
**+-------+-------+  
| state | count |  
+-------+-------+  
| VA    |     8 |  
| OH    |     7 |  
| MA    |     4 |  
| NY    |     4 |  
| NC    |     2 |  
| VT    |     2 |  
| TX    |     2 |  
| GA    |     1 |  
| IL    |     1 |  
| SC    |     1 |  
...

如果用于排序的列是由某个汇总函数产生的,那么不能直接在ORDER BY子句里引用该函数。而是应该先为该列取一个别名,然后再在ORDER BY子句里引用这个别名。上面那个查询就是这样做的,其中的COUNT(*)列的别名为count

要使用GROUP BY子句来对某个计算列的结果进行分组,需要使用别名或列位置来引用它,具体实现方法与ORDER BY相类似。下面这个查询可以确定出在一年的每个月分别有多少位总统出生:

mysql> **SELECT MONTH(birth) AS Month, MONTHNAME(birth) AS Name,**  
    -> **COUNT(*) AS count  
**    -> **FROM president GROUP BY Name ORDER BY Month;  
**+-------+-----------+-------+  
| Month | Name      | count |  
+-------+-----------+-------+  
|     1 | January   |     4 |  
|     2 | February  |     4 |  
|     3 | March     |     4 |  
|     4 | April     |     4 |  
|     5 | May       |     2 |  
|     6 | June      |     1 |  
|     7 | July      |     4 |  
|     8 | August    |     5 |  
|     9 | September |     1 |  
|    10 | October   |     6 |  
|    11 | November  |     5 |  
|    12 | December  |     3 |  
+-------+-----------+-------+

COUNT()函数可以与ORDER BYLIMIT组合在一起使用。例如,想要在president表里找出哪4个州出生的总统最多,那么可以使用下面这条查询语句:

mysql> **SELECT state, COUNT(*) AS count FROM president  
**    -> **GROUP BY state ORDER BY count DESC LIMIT 4;  
**+-------+-------+  
| state | count |  
+-------+-------+  
| VA    |     8 |  
| OH    |     7 |  
| MA    |     4 |  
| NY    |     4 |  
+-------+-------+

如果不是想用LIMIT子句来限制查询结果中的记录条数,而是想把COUNT()的某些特定值找出来,那么需要用到HAVING子句。该子句与WHERE相类似,它们都可用来设定输出行所必须满足的查询条件。与WHERE子句的不同之处在于,它可以引用像COUNT()那样的汇总函数输出的结果。下面这个查询会告诉你“哪些州曾经出现过两位及以上的总统”:

mysql> **SELECT state, COUNT(*) AS count FROM president  
**    -> **GROUP BY state HAVING count > 1 ORDER BY count DESC;**  
+-------+-------+  
| state | count |  
+-------+-------+  
| VA    |     8 |  
| OH    |     7 |  
| MA    |     4 |  
| NY    |     4 |  
| NC    |     2 |  
| VT    |     2 |  
| TX    |     2 |  
+-------+-------+

一般情况下,带有HAVING子句的查询语句,特别适合于查找在某个数据列里重复出现的值。也可用于查找不重复出现的值,此时使用HAVING count = 1即可。

COUNT()以外,还有其他几个汇总函数。函数MIN()MAX()SUM()AVG()可分别用于确定某个数据列的最小值、最大值、总计和平均值。你甚至可以同时在一个查询语句里使用它们。下面这个查询可以显示出已进行过的每次考试或测验的各种数值特征。它也会显示出有多少分数参与了各个值的计算。(可能有的学生缺勤或未被统计。)

mysql> **SELECT**  
    -> **event_id,**  
    -> **MIN(score) AS minimum,**  
    -> **MAX(score) AS maximum,**  
    -> **MAX(score)-MIN(score)+1 AS span,**  
    -> **SUM(score) AS total,**  
    -> **AVG(score) AS average,**  
    -> **COUNT(score) AS count**  
    -> **FROM score**  
    -> **GROUP BY event_id;**  

+----------+---------+---------+------+-------+---------+-------+  
| event_id | minimum | maximum | span | total | average | count |  
+----------+---------+---------+------+-------+---------+-------+  
|        1 |       9 |      20 |   12 |   439 | 15.1379 |    29 |  
|        2 |       8 |      19 |   12 |   425 | 14.1667 |    30 |  
|        3 |      60 |      97 |   38 |  2425 | 78.2258 |    31 |  
|        4 |       7 |      20 |   14 |   379 | 14.0370 |    27 |  
|        5 |       8 |      20 |   13 |   383 | 14.1852 |    27 |  
|        6 |      62 |     100 |   39 |  2325 | 80.1724 |    29 |  
+----------+---------+---------+------+-------+---------+-------+

很明显,如果从中还能明确地知道event_id列的值是表示考试还是表示测验,那么这些信息会更具有意义。想要获得该信息,还需要查询grade_``event表。关于此操作的更多细节请参考1.4.9.10节。

如果想要生成额外的输出行,显示出“统计结果的统计”,那么还需要增加一条WITH ROLLUP子句。它会让MySQL计算各分组行的“超集”(super-aggregate)值。这里有个简单的示例,它是基于先前那个按性别统计学生人数的示例改进的。WITH ROLLUP子句将生成另外一行,对两类性别的人数进行汇总:

mysql> **SELECT sex, COUNT(*) FROM student GROUP BY sex WITH ROLLUP;  
**+-----+----------+  
| sex | COUNT(*) |  
+-----+----------+  
| F   |       15 |  
| M   |       16 |  
| NULL|       31 |  
+-----+----------+

分组列里的NULL表明,相应的计数结果就是其前面那些分组统计的汇总值。

WITH ROLLUP子句还可以与其他聚集函数搭配使用。下面这条语句,除了像前面几个段落那样可以对考试成绩进行了汇总以外,还可以产生一个额外的超集行:

mysql> **SELECT  
**    -> **event_id,  
**    -> **MIN(score) AS minimum,  
**    -> **MAX(score) AS maximum,  
**    -> **MAX(score)-MIN(score)+1 AS span,  
**    -> **SUM(score) AS total,  
**    -> **AVG(score) AS average,  
**    -> **COUNT(score) AS count  
**    -> **FROM score  
**    -> **GROUP BY event_id WITH ROLLUP;****  
**+----------+---------+---------+------+-------+---------+-------+  
| event_id | minimum | maximum | span | total | average | count |  
+----------+---------+---------+------+-------+---------+-------+  
|        1 |       9 |      20 |   12 |   439 | 15.1379 |    29 |  
|        2 |       8 |      19 |   12 |   425 | 14.1667 |    30 |  
|        3 |      60 |      97 |   38 |  2425 | 78.2258 |    31 |  
|        4 |       7 |      20 |   14 |   379 | 14.0370 |    27 |  
|        5 |       8 |      20 |   13 |   383 | 14.1852 |    27 |  
|        6 |      62 |     100 |   39 |  2325 | 80.1724 |    29 |  
|     NULL |       7 |     100 |   94 |  6376 | 36.8555 |   173 |  
+----------+---------+---------+------+-------+---------+-------+

在上面这个输出里,最后一行显示出了一些聚集值,它们都是根据其前面的全部分组统计值计算出来的。

WITH ROLLUP子句很有用,因为它可以让你不必为了获得一些额外的信息,而执行另外一条查询语句。只用一条查询语句就能达到目的,当然效率会更高,因为服务器无需对数据进行两次检查。如果GROUP BY子句指定了多列,那么WITH ROLLUP还会再生成其他的超集行,其中会包含更高层的汇总值。

1.4.9.10 从多个表里检索信息

到目前为止,我们查询出来的信息都是来自一个表。不过,MySQL的能耐远不止于此。前面说过,DBMS的威力在于它们可以把源自多个表的信息结合起来,从而解答那些只靠单个表而无法解答的问题。本节将介绍如何编写涉及多个表的查询语句。

在从多个表中查询信息时,有一种类型的操作叫连接(join)。之所以叫这个名字,是因为必须把一个表与另一个表中的信息连接起来才能得到结果。此操作是通过匹配多个表里的公共值实现的。另一种类型的多表操作是将一条SELECT语句嵌套在另一条SELECT语句里使用。这种嵌套的SELECT语句叫子查询(subquery)。本节将对这两种类型的操作进行介绍。

先一起来看一个关于连接的例子。1.4.6.2节给出了一个用来检索给定日期考试或测验分数的查询命令,但在那里并未对它进行解释。现在可以对它进行解释了。那条查询语句实际上涉及了一个三方的连接操作,因此我们将它分成两步来实现。第一步,构造一条可以查出给定日期的分数的查询语句:

mysql> **SELECT student_id, date, score, category  
**    -> **FROM grade_event INNER JOIN score  
**    -> **ON grade_event.event_id = score.event_id  
**    -> **WHERE date = '2012-09-23';  
**+------------+------------+-------+----------+  
| student_id | date       | score | category |  
+------------+------------+-------+----------+  
|          1 | 2012-09-23 |    15 | Q        |  
|          2 | 2012-09-23 |    12 | Q        |  
|          3 | 2012-09-23 |    11 | Q        |  
|          5 | 2012-09-23 |    13 | Q        |  
|          6 | 2012-09-23 |    18 | Q        |  
...

这个查询先查出给定日期('20``12``-09-23' )的grade_``event行,再利用此行里的事件ID把score表里拥有同一事件ID的分数都查询出来。对于grade_``eventscore两个表里相匹配的每一个行组合,把其中的学生ID、分数、日期和事件类别都显示出来。

这个查询与之前介绍的查询语句在以下两个方面有着显著的区别。

  • FROM子句指定了多个表名,因为需要从多个表里检索信息:
FROM grade_event INNER JOIN score
  • ON子句指定了表grade_``eventscore的连接条件,即这两个表的event_id值必须相互匹配:
ON grade_event.event_id = score.event_id

请注意,我们是如何通过grade_event.event_idscore.event_id来引用event_id列的,其语法形式为:tbl_name.col_name。这样,MySQL便能知道我们是在引用哪个表。因为这两个表都有event_id列,所以在不限定表名时,会产生二义性。这条查询语句里的其他列(datescorecategory)可以直接使用,不用限定表名,因为它们只存在于其中的一个表里,不会产生二义性。

在连接语句里,我个人比较喜欢在每个列的前面都加上表名,从而可以让每一列属于哪个表变得更加清楚。在后面的连接语句里,我将一直沿用这个习惯。在为每列加上完整的表名之后,这个查询现在变成了下面这个样子:

SELECT score.student_id, grade_event.date, score.score, grade_event.category  
FROM grade_event INNER JOIN score  
ON grade_event.event_id = score.event_id  
WHERE grade_event.date = '2012-09-23';

在第一阶段的查询里,我们利用grade_event表将日期映射到了事件ID,并使用这个事件ID在score表里找到了与之匹配的分数。这个查询的输出只包含了student_id值,但是如果能把学生的姓名直接显示出来则会更具意义。第二阶段,我们将利用student表,把学生ID映射成他们的姓名。score表和student表都有student_id列,通过它可以将两个表的各个行链接起来,显示出学生的名字。最终的查询语句如下所示:

mysql> **SELECT  
**    -> **student.name, grade_event.date, score.score, grade_event.category  
**    -> **FROM grade_event INNER JOIN score INNER JOIN student  
**    -> **ON grade_event.event_id = score.event_id  
**    -> **AND score.student_id = student.student_id  
**    -> **WHERE grade_event.date = '2012-09-23';  
**+-----------+------------+-------+----------+  
| name      | date       | score | category |  
+-----------+------------+-------+----------+  
| Megan     | 2012-09-23 |    15 | Q        |  
| Joseph    | 2012-09-23 |    12 | Q        |  
| Kyle      | 2012-09-23 |    11 | Q        |  
| Abby      | 2012-09-23 |    13 | Q        |  
| Nathan    | 2012-09-23 |    18 | Q        |  
...

这个查询与以前介绍的查询命令在以下几方面有区别。

  • FROM子句现在包含了student表,因为这条查询语句除了要用到grade_``event表和score表以外,还需要用到它。
  • 在前一个查询里,student_id列不会产生二义性,因此在引用它时,既可以不限定表名(student_id),也可以限定表名(score.student_id)。但在这个查询里,因为score表和student表都有student_id列,所以肯定会出现二义性。于是,为了避免产生二义性,必须将它们分别限定为score.student_idstudent.student_id
  • ON子句里多了一个查询条件,用于指定score表里的行与student表里的行必须基于学生ID匹配在一起:
ON ... score.student_id = student.student_id
  • 这个查询会显示出学生的姓名,而不显示学生的ID。(如果想要两者都显示,只需要在输出列的列表里加上student.student_id即可。)

对于这个查询,只要插入任何日期,即可获得那天对应的分数、参加考试的学生姓名,以及考试的类别。你根本不用了解学生ID和事件ID,因为MySQL会自动查出有关的ID值并利用它们把你想要的信息找出来。

在前面的1.4.9.9节,我们运行了一个查询,它可以将score表里各类数据的数值特征统计出来。在那个查询的输出结果里,只列出了事件ID,而没有列出事件日期或类别,因为我们当时还不知道如何将score表连接到grade_event表上,将事件ID映射到考试日期和类别上去。现在,我们来实现这一效果。下面这个查询与前面那个相差无几,但它显示出来的是考试日期和类别,而不只是一些数字形式的事件ID:

mysql> **SELECT  
**    -> **grade_event.date,grade_event.category,  
**    -> **MIN(score.score) AS minimum,  
**    -> **MAX(score.score) AS maximum,  
**    -> **MAX(score.score)-MIN(score.score)+1 AS span,**  
    -> **SUM(score.score) AS total,  
**    -> **AVG(score.score) AS average,  
**    -> **COUNT(score.score) AS count  
**    -> **FROM score INNER JOIN grade_event  
**    -> **ON score.event_id = grade_event.event_id  
**    -> **GROUP BY grade_event.date;  
**+------------+----------+---------+---------+------+-------+---------+-------+  
| date       | category | minimum | maximum | span | total | average | count |  
+------------+----------+---------+---------+------+-------+---------+-------+  
| 2012-09-03 | Q        |       9 |      20 |   12 |   439 | 15.1379 |    29 |  
| 2012-09-06 | Q        |       8 |      19 |   12 |   425 | 14.1667 |    30 |  
| 2012-09-09 | T        |      60 |      97 |   38 |  2425 | 78.2258 |    31 |  
| 2012-09-16 | Q        |       7 |      20 |   14 |   379 | 14.0370 |    27 |  
| 2012-09-23 | Q        |       8 |      20 |   13 |   383 | 14.1852 |    27 |  
| 2012-10-01 | T        |      62 |     100 |   39 |  2325 | 80.1724 |    29 |  
+------------+----------+---------+---------+------+-------+---------+-------+

虽然GROUP BY列带有限定符,但对于这条查询语句来说并不是必须的。GROUP BY引用了多个输出列,不过名叫date的列只有一个,因此MySQL可以清楚地知道你所指的是哪一个。

即使有源自多个表的多个列,你也可以使用像COUNT()AVG()这样的函数,为它们生成汇总信息。下面这个查询可以为事件日期与学生性别的每种组合,确定出各分数的数目,以及平均分数:

mysql> **SELECT grade_event.date, student.sex,  
**    -> **COUNT(score.score) AS count, AVG(score.score) AS average  
**    -> **FROM grade_event INNER JOIN score INNER JOIN student  
**    -> **ON grade_event.event_id = score.event_id  
**    -> **AND score.student_id = student.student_id  
**    -> **GROUP BY grade_event.date, student.sex;  
**+------------+-----+-------+---------+  
| date       | sex | count | average |  
+------------+-----+-------+---------+  
| 2012-09-03 | F   |    14 | 14.6429 |  
| 2012-09-03 | M   |    15 | 15.6000 |  
| 2012-09-06 | F   |    14 | 14.7143 |  
| 2012-09-06 | M   |    16 | 13.6875 |  
| 2012-09-09 | F   |    15 | 77.4000 |  
| 2012-09-09 | M   |    16 | 79.0000 |  
| 2012-09-16 | F   |    13 | 15.3077 |  
| 2012-09-16 | M   |    14 | 12.8571 |  
| 2012-09-23 | F   |    12 | 14.0833 |  
| 2012-09-23 | M   |    15 | 14.2667 |  
| 2012-10-01 | F   |    14 | 77.7857 |  
| 2012-10-01 | M   |    15 | 82.4000 |  
+------------+-----+-------+---------+

我们可以用一条类似的查询语句来完成成绩考评项目的其中一项任务,即在期末的时候计算每个学生的总成绩:

SELECT student.student_id, student.name,  
SUM(score.score) AS total, COUNT(score.score) AS n  
FROM grade_event INNER JOIN score INNER JOIN student  
ON grade_event.event_id = score.event_id  
AND score.student_id = student.student_id  
GROUP BY score.student_id  
ORDER BY total;

成绩考评项目的另一项任务是汇总所有学生的缺勤情况。所有缺勤情况都记录在absence表里,其中包括学生ID和日期。为获得学生的名字(不只是ID),我们必须基于student_id的值,将absence表连接到student表上。下面这个查询可以列出学生的ID号和姓名,以及缺勤情况:

mysql> **SELECT student.student_id, student.name,  
**    -> **COUNT(absence.date) AS absences  
**    -> **FROM student INNER JOIN absence  
**    -> **ON student.student_id = absence.student_id  
**    -> **GROUP BY student.student_id;  
**+------------+-------+----------+
| student_id | name  | absences |
+------------+-------+----------+
|          3 | Kyle  |        1 |
|          5 | Abby  |        1 |
|         10 | Peter |        2 |
|         17 | Will  |        1 |
|         20 | Avery |        1 |
+------------+-------+----------+

如果你只想知道有哪些学生缺勤,那么这个查询输出已能满足需要。如果想要把这个列表交到学校办公室,那么他们可能会问:“其他学生的情况怎么样呢?我们希望看到每位学生的情况。”这就是个有所不同的问题了。它既要求统计缺勤学生的数量,也要求统计无缺勤情况学生的数量。既然这个问题有所不同,那么回答这个问题的查询也就会有所不同。

使用内连接(inner join)来回答此问题并不合适,我们需要使用LEFT JOIN子句。该子句会告知MySQL,对于连接里的第一个表(即LEFT JOIN关键字左边的那个表),为从其里面查询出的每一行产生一个输出行。通过将student表指定为第一个表,我们将能获得每位学生的输出,其中甚至包括那些absence表里没有的学生。在编写此查询语句时,可以在FROM子句里的两个表之间使用LEFT JOIN(而不是用逗号把这两个表分开),然后使用ON子句说明如何匹配这两个表中的行。此查询如下所示:

mysql> **SELECT student.student_id, student.name,  
**    -> **COUNT(absence.date) AS absences  
**    -> **FROM student LEFT JOIN absence  
**    -> **ON student.student_id = absence.student_id  
**    -> **GROUP BY student.student_id;  
**+------------+-----------+----------+
| student_id | name      | absences |
+------------+-----------+----------+
|          1 | Megan     |        0 |
|          2 | Joseph    |        0 |
|          3 | Kyle      |        1 |
|          4 | Katie     |        0 |
|          5 | Abby      |        1 |
|          6 | Nathan    |        0 |
|          7 | Liesl     |        0 |
...

连接操作并非只能用于两个不同的表。这乍听起来有点儿奇怪,但你完全可以把某个表与其自身连接起来。例如,想确定是否有某位总统与另一位总统出生在同一个城市,这时便需要检查每位总统的出生地与其他总统的出生地是否一致:

mysql> **SELECT p1.last_name, p1.first_name, p1.city, p1.state  
**    -> **FROM president AS p1 INNER JOIN president AS p2  
**    -> **ON p1.city = p2.city AND p1.state = p2.state  
**    -> **WHERE (p1.last_name <> p2.last_name OR p1.first_name <> p2.first_name)  
**    -> **ORDER BY state, city, last_name;  
**+-----------+-------------+-----------+-------+  
| last_name | first_name  | city      | state |  
+-----------+-------------+-----------+-------+  
| Adams     | John Quincy | Braintree | MA    |  
| Adams     | John        | Braintree | MA    |  
+-----------+-------------+-----------+-------+

这条查询命令有以下两个地方需要特别注意。

  • 它需要引用同一个表中的两个实例,因此我们必须为它创建两个别名(p1p2),并用它们来将表中的同名列区别开来。由于列已有别名,所以在为表指定别名时, AS关键字就是可选项了。
  • 每位总统的记录都与其本身相匹配,但这并不是我们想要的输出结果。在确保参与比较的总统名字都不相同的情况下, WHERE子句便能防止出现“行与其本身相匹配”的情况。

用一个类似的查询可以查出在同月同日出生的总统。不过,如果直接比较某两位总统的出生日期,那么查询结果里就会缺少那些生于同月同日但不同年的总统。因此,我们必须用函数MONTH()DAYOF``M``ONTH()来比较出生日期里的月和日:

mysql> **SELECT p1.last_name, p1.first_name, p1.birth  
**    -> **FROM president AS p1 INNER JOIN president AS p2  
**    -> **WHERE MONTH(p1.birth) = MONTH(p2.birth)  
**    -> **AND DAYOFMONTH(p1.birth) = DAYOFMONTH(p2.birth)  
**    -> **AND (p1.last_name <> p2.last_name OR p1.first_name <> p2.first_name)  
**    -> **ORDER BY p1.last_name;  
**+-----------+------------+------------+  
| last_name | first_name | birth      |  
+-----------+------------+------------+  
| Harding   | Warren G.  | 1865-11-02 |  
| Polk      | James K.   | 1795-11-02 |  
+-----------+------------+------------+

使用DAYOFYEAR()来代替MONTH()DAYOF``M``ONTH()的组合,可以得到一个稍微简单一点儿的查询语句。但其查询结果却可能不正确,因为它没有考虑到出现闰年的情况。

另一种类型的多表检索操作是使用“子查询”,即把一条SELECT语句嵌套在另一条SELECT语句里。子查询有几种类型,详细内容将在2.9节进行讨论。我们现在只看两个示例。假设需要把全勤的学生都找出来。此要求等价于把没在absence表里出现过的学生都找出来,因此我们可以这样做:

mysql> **SELECT * FROM student  
**    -> **WHERE student_id NOT IN (SELECT student_id FROM absence);  
**+-----------+-----+------------+  
| name      | sex | student_id |  
+-----------+-----+------------+  
| Megan     | F   |          1 |  
| Joseph    | M   |          2 |  
| Katie     | F   |          4 |  
| Nathan    | M   |          6 |  
| Liesl     | F   |          7 |  
...

嵌套于内层的SELECT语句会确定出absence表里的student_id值集合,而外层的那个SELECT语句则会检索出student表里与该集合中的ID值都不匹配的那些行。

子查询还能为1.4.9.8节提出的那个问题(即有哪些总统出生在Andrew Jackson总统之前)提供一种单语句解决方案。当时的解决方案是使用两条语句和一个用户变量,而现在可以用一条子查询语句来解决,如下所示:

mysql> **SELECT last_name, first_name, birth FROM president  
**    -> **WHERE birth < (SELECT birth FROM president  
**    -> **WHERE last_name = 'Jackson' AND first_name = 'Andrew');**  
+------------+------------+------------+  
| last_name  | first_name | birth      |  
+------------+------------+------------+  
| Washington | George     | 1732-02-22 |  
| Adams      | John       | 1735-10-30 |  
| Jefferson  | Thomas     | 1743-04-13 |  
| Madison    | James      | 1751-03-16 |  
|