本章主要介绍MySQL关系数据库管理系统(Relational Database Management System,RDBMS)和MySQL所使用的结构化查询语言(Structured Query Language,SQL)。其中,列出了应该掌握的基本术语和概念,描述了示例所要用到的样本数据库sampdb
,并且提供了一个用于展示如何使用`MySQL创建数据库并与之进行交互操作的教程。
如果你对数据库系统还不甚了解,或者还不是很肯定自己是否需要学习它,甚至还不肯定是否需要使用它,那么请从本章开始。如果你对MySQL和SQL还一无所知,并且需要一个入门指南,那么也请从本章开始。对MySQL或其他数据库系统已有一定经验的读者,则可以略读一下本章的内容。不过,为了你能对全书所用那个数据库sampdb
的目的和内容更熟悉,希望可以阅读一下1.2节。
本节描述的是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是如何工作的呢?找到答案的最好方式就是自己动手体验一下。为此,我们需要有一个可以操作的数据库。
本节将描述本书所使用的示例数据库。在你学习使用MySQL的过程中,可以用这个数据库提供的示例进行尝试,该数据库是针对之前描述过的两种情况设计的。
下面,我们进一步分析一下这两个场景的需求。
在考虑“将什么放入数据库”之前考虑“要从数据库里获得什么样的内容”,这似乎有些本末倒置。毕竟,大家都是这么认为的:要先输入数据,然后才能检索它。不过,你使用数据库的方式取决于你的目标是什么。而且,与想要放入数据库的内容相比,想要从数据库检索的内容则与那些目标的关系更加密切。只有在计划今后使用这些信息之后,你才会想要花费时间和精力把它们放入数据库。
该项目的场景是:你是联盟秘书,眼下正使用某个文字处理文档维护那份成员资格名单。对于像“生成打印名录”这样的事情,处理起来当然没什么问题。但是,当你想要获得其他更多信息时,就会受到限制。你还有几个目标要实现。
数据库不是世界上最令人兴奋的工具,因此我不会鼓吹像“使用数据库能激发人的创造性思维”这样不切实际的话。不过,如果你不再把信息看成是一种累赘(如同你在使用文字处理文档时所想的那样),而是把它想成是可相对轻松处理的事情(如同你希望使用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脚本或批处理脚本。
现在一起来看另一个要使用示例数据库的情况。该项目的场景是:你是一名负责成绩考评的教师。你想将成绩处理工作,从使用成绩册的手工操作方式转换成使用MySQL的电子表示方式。在这种情况下,从数据库获取信息的方式隐含在你目前使用成绩册的方式中。
最终目标是要避免手动排序、避免手动汇总分数和考勤情况。换句话说,你希望MySQL可以对分数排序,并在期末完成所有与每个学生的总分和缺席次数相关的计算。为实现这些目标,你需要班里学生的名单、每次考试和测验的分数,以及全部学生的缺勤日期。
如果你对这里的“美史联盟”和“成绩考评”两个项目都不感兴趣,那么你可能会想“还有什么场景一定适合你呢”。答案就是“无穷无尽”。其实,这两个项目可以说明你用MySQL和与之相关的工具可以做什么事情。稍微想象一下,你便能看到示例数据库查询是如何应用于
你所要解决的那些特定问题的。假设你正好就在我前面提到的那个牙科诊所里工作。虽然在这本书里不会看到很多与牙科学有关的查询,但是会看到这里所发现的很多查询都可以应用到患者记录维护、办公记载等那些工作中。例如,“确定联盟里哪些成员需要更新他们的成员资格”与“确定最近哪些患者未来复诊”这两个任务便非常相似。它们都是基于日期的查询,因此只要你学会了“成员资格更新”的查询,那么便可以将该技巧用于编写“久未复诊患者”的查询,从而带来更多的收获。
你可能已经注意到了,尽管这是一本关于数据库的书籍,但到目前为止,你还未遇到过多少晦涩难懂的专业技术术语。事实上,尽管我们对如何使用示例数据库有过大致描述,但关于数据库到底是什么样子,我却只字未提。可是,既然我们要设计数据库,并实现它,那么我们就不能再避而不谈有关的术语了。这正是本节的主要内容。本节所描述的术语都是本书要用到的,因此希望大家能够熟悉它们。庆幸的是,关系数据库的许多概念都很简单。人们之所以喜欢关系数据库,很大程度上便是因为其基本概念都简明易懂。
在数据库领域,MySQL被划分为关系数据库管理系统(RDBMS)。我们下面来拆解一下。
数据库(DataBase,即RDBMS里的DB)是一个用来存储信息的仓库,它的结构简单、规则。
数据库里的数据集都被组织成表(table)。
每个表由多个行(row)和列(column)组成。
表中的每一行称为一条记录(record)。
记录可以包含多项信息;表里的每一列对应于其中的一项。
管理系统(Management System,即RDBMS里的MS)是一个软件,我们可以通过它来插入(insert)、检索(retrieve)、修改(modify)或删除(delete)记录。
关系数据库是如何把数据组织到表里的呢?又是如何把不同表的信息关联在一起的呢?下面来看一个例子。假设你在经营一个网站,它有一项横幅广告服务。你与多家想要刊登广告的公司签订了合同,满足它们想要在人们访问你网站页面时显示其广告的需要。每当有访客点击其中的某个页面时,你就提供一个嵌有广告的页面发送给访客浏览器,这样你就能从刊登这条广告的公司那里获得一点费用。这就是所谓的广告“点击”。为了表示这些信息,你需要用到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的著作。)
为了与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
的看法也会悄然地发生变化——它不再是一组怪诞的表达,而是一种有助于信息描述的强大工具。
当使用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指代完整的MySQL RDBMS,而mysql
指代特定客户端程序的名字。虽然它们的发音相同,但我们通过字母大小写和字体差异对它们进行了区分。
说到发音,MySQL被读作“my-ess-queue-ell”。具体细节可以在MySQL参考手册(MySQL Reference Manual)里查阅到。此外,SQL的读法有“sequel”和“ess-queue-ell”两种,具体是哪种由读它的人决定。本书假定SQL的发音为“ess-queue-ell”。
现在你已具备了所有的预备知识。下面来实际操作一下MySQL。
本节提供了一个带有多个示例的教程,其目的在于帮助你熟悉MySQL。主要内容包括创建一个示例数据库和多个表,通过对这些表里的信息执行插入、检索、删除和修改操作,练习与示例数据库的交互。在此过程中,你将学会以下内容。
mysql
,它首先读取你输入的SQL查询语句,把它们发送到服务器执行,然后把执行结果显示在你面前。客户端mysql
可以在MySQL所支持的所有平台上运行,并且提供了与服务器进行交互的最直接的方式。不过根据需要,有些示例会使用mysqlimport
或者mysqlshow
来代替。 本书将示例数据库命名为sampdb
。如果在你的系统上已经有人占用了这个名字,或者你的MySQL管理员为你分配了另一个名字,那么你需要换一个名字。不管是何种情况,你都需要把本书示例中的sampdb
替换为你实际使用的数据库名称。
即使你的系统里有多个用户,且他们都各自拥有自己的示例数据库,本节所有示例里的表名也都可以直接使用。在MySQL里,只要每个人都用自己的数据库,那么大家完全可以使用相同的表名,不会有任何问题。MySQL将表限制在各自的数据库里,防止了相互干扰。
本教程在好几个地方都要用到“示例数据库发行包”( 也称作“sampdb
数据库的发行版”)。该发行包里包含了许多用来安装示例数据库的查询语句和数据。发行包的获取办法和安装步骤可以在附录A里查到。在发行包解压之后,它会自动创建一个名为sampdb的子目录,其中包含了你所需要的许多文件。顺便提个建议:每次你操作数据库里的示例时,最好都能切换到一个新的目录。
如果想要在任何目录里都可以方便地运行MySQL程序,那么你最好能把位于MySQL目录下包含着这些程序的bin
目录,添加到命令解释器的搜索路径里。具体操作方法是,参考本书A.3.3节,把该目录的路径名称添加到环境变量PATH里
。
为练习本教程里的各个示例,除获得示例数据库发行包外,还必须满足以下几项基本要求。
所需的软件包括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 adm
、secret
、sampdb
分别替换为管理员分配给你的用户名、密码和数据库名。
为连接到MySQL服务器,请在命令提示符(即Unix系统的shell提示符,或者Windows下的控制台窗口提示符)里调用mysql程序。命令如下:
% **mysql**options
本书使用%
来表示命令提示符。它是Unix系统的其中一个标准提示符;另一个是$
。在Windows系统里,你所看到的提示符有点像C:\>
。当输入这些示例里所示的命令时,请不要输入提示符本身。
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的用户名和密码分别是sampadm
和secret
。如果运行MySQL服务器的那台主机与你运行mysql
程序的主机相同,那么你可以省略选项-h``, 此时mysql
命令会像下面那样连接服务器:
% **mysql -p -u sampadm
**Enter password: ******
在输入完这条命令之后,mysql
会显示Enter password:``,
提示你输入密码,然后你便可以输入它(输入的secret
会在屏幕上显示为6个星号******
)。
如果一切顺利,mysql
将会显示出一条欢迎消息和一个mysql>
提示符,此时表明它在等你发起查询命令。完整的启动过程近似如下:
%** 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节。
在连接上服务器以后,你便可以发起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>
变为了->
。这是在提醒你:mysql
认为你还要继续输入语句内容。这是个很重要的反馈。如果你忘了在语句末尾加上分号,那么这个变化的提示符会提醒你注意:mysql
仍在耐心等待你继续输入语句内容。否则,你会很不耐烦地等在一边,心里疑惑:为何MySQL执行你的语句要花这么长的时间。命令mysql
还有另外几种提示符,附录F对它们进行了详细介绍。
如果你已经输入了一条多行语句,但突然决定不执行它了,那么可以输入\c
来清除(即取消)它:
mysql>** SELECT NOW(),
** ->** VERSION(),
** ->** \c
**mysql>
请注意,提示符是如何变回mysql>
的。这种变化表明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>
作为标志,并且这些示例都提供了语句输出结果。如果你输入与示例显示一样的语句,那得到的输出结果也应该是相同的。前面未加提示符的语句主要是用来做说明用的,你不需要执行它们。当然,如果愿意,执行一下也无妨。提醒一下,请记得在每条语句的末尾加上一个分号作为结束符。
数据库的使用涉及以下几个步骤。
(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>
提示符处输入一条USE sampdb
语句。
本节将创建示例数据库sampdb
所需要的那些表。首先,创建“美史联盟”场景所需要的表,然后,再为“成绩考评”项目创建所需的表。有些数据库书讲到在此处便会开始讨论“数据库的分析与设计”、“实体联系图”、“规范化过程”(Normalization Procedure)等内容。有很多书专门讲解这些内容,所以本书在这里只想说明我们的数据库应该是个什么样子,具体来讲涉及这样几个方面:它应该包含哪些表、每个表都应包含什么内容,以及在决定如何表示这些数据时需要考虑哪些问题。
这里所选择的表示方式并不是绝对的。在其他场合,你可以选用不同的方式来表示相似的数据。到底选择哪一种,需要由应用程序和数据的具体用途来决定。
美史联盟的表相当简单,包括以下两个表。
president
表。其中包含美国历任总统的描述性记录。我们需要用它来实现联盟网站上的在线小测验(对联盟通讯儿童专栏里出现的小测验进行交互式模拟)。member
表。用于保存联盟每位成员的最新个人资料。我们可以用它来创建成员名录的印刷版本和在线版本,用它来向到期成员自动发送提醒通知,还可以用它做很多其他事情。 president
表包含一些与美国历任总统生平相关的基本信息。
如果优先输入名,就无法按姓排序。
如果优先输入姓,就无法按名在前、姓在后的顺序显示它们。
难以查找姓名。例如,想要查找某人的姓,那么必须先使用一种模式,然后去查找与之匹配的姓名。与直接查找姓的做法相比,这种做法的效率很低,速度很慢。
为避免出现这些限制, president
表将使用两列分别表示姓和名。
那个“名”列还要存放中间名或其缩写。这种做法不会影响到以后的姓名排序操作,因为我们不太可能对中间名进行排序(也不太可能对名进行排序)。另外,姓名也可以正常显示,因为无论是以“Bush, George W.”或“George W. Bush”中的哪一种格式来输出,姓名里的中间名总是排在名字的后面。
还有一点需要注意。如果一位总统(如Jimmy Carter)的姓名后面带有一个“Jr.”,那么这个“Jr.”应该放在哪里呢?根据英文姓名输出格式习惯,这位总统的名字可以显示为“James E. Carter, Jr.”,或者“Carter, James E., Jr.”。这里的“Jr.”与名和姓没有联系,因此我们还需要另外创建一个列,用它来保存姓名后缀。这种情况表明:当你在选择数据的表示形式时,即使只是一个单值,也可能会引发某些问题。它同时还表明:在把数据值放入数据库之前,最好尽可能多地对数据值进行深入的了解。如果对数据值的真正含义了解不够,那么在表启用之后,可能还要被迫更改它的结构。虽然这种事不能算作是一场灾难,但总的说来还是尽量避免为妙。
NULL
的意思即表示“无值”,因此我们可以在逝世日期列里用它来表示该位总统“依然健在”。member
表 从每条记录都保存着单独某个人的个人资料这一角度来看,存放“美史联盟”成员列表的member
表与刚才介绍的president
表很相似。只是member
表的每一行还包含了以下这些列。
president
表相同的3列表示法:姓、名和姓名后缀。member
表里的行,与你所创建的成员相关的其他表建立关联时,使用编号则会比使用姓名更容易实现。)NULL
会更合适,因为“无值”在逻辑上正好对应于“永不失效”。我们假设所有联盟成员都居住在美国。当然,对于那些成员遍布于世界各地的组织机构来说,这个假设可能显得有些简单。如果想要处理涉及多个国家的地址,那么你将遭遇到一些棘手的问题,必须要解决好不同国家所使用的地址格式不同的问题。例如,邮政编码就不是一项国际标准;另外,有很多国家都只有“省”的概念,而无“州”的说法。
现在,我们准备创建美史联盟表。为此,我们需要使用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
表用到了两种数据类型:VARCHAR
和DATE
。VARCHAR(n)
表示的是:该列可以存放长度可变的字符型值,且最大长度为n
个字符。也就是说,它们包含了长度不定的字符串,但其长度存在上限。那个n
的值即表明了你所期望的数据长度。例如,将state
定义为VARCHAR(2)
类型,即表示我们需要以两个字母的缩写形式输入州名。而其他值为字符串类型列,其数据长度必须要再长一点才能容纳下更长的值。
我们用到的另一个数据类型是DATE
。很显然,此类型表示该列用于保存日期值。不过,日期的表示格式可能会让你感到意外。MySQL要求将日期表示为'CCYY-MM-DD'
格式,其中的CC
、YY
、MM
、DD
分别代表了世纪、世界的年份、月和日。这也是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_id
和expiration
是例外,它们分别保存的是序号和日期。
使用成员资格编号列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_INCREMENT
和PRIMARY 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
表的类似信息。(如果你想知道为什么未定义为允许NULL
的Default
列会显示NULL
,那么我在这里告诉你,那是因为NULL
也可用于表明该列没有显式的DEFAULT
子句。)
如果你不记得一个表中的列名、想知道其数据类型,或者想了解其宽度是多少,那么可以使用DESCRIBE
。你还可以利用它来查看MySQL在表行里存储各列的先后顺序。当你执行INSERT
或LOAD 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 COLUMNS
与SHOW 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_schema
和sampdb
。其中,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-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
表增加一列,用T
或Q
分别表示考试(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
表里的内容。T
或Q
。于是,你又在表格的顶部把考试日期与考试类型关联起来了。这正是我刚才讲过的两组信息当中的第二组,而它也正好对应于grade_``event
表里的内容。换句话说,尽管你可能还未意识到这一点,但你在成绩册里所做的事,与我把信息放到两个表里的做法并无多大差异。唯一的区别在于,纸质成绩册里的两类信息没有明确地分离开来。
成绩册里的页的概念体现了我们对信息的思维方式,同时这也表明:把信息妥善地放到数据库里并不是一件简单的事情。在日常生活中,人们习惯于把不同信息综合起来,然后把它们当作一个整体来考虑。但数据库无法那样工作,这也正是它们看起来不太自然的原因。我们习惯把信息统一起来,这也使得我们有时很难清楚地分辨出自己所拥有的信息是有多种类型,还是只有一种。正因如此,“以数据库系统的方式进行思考”,考虑如何表现数据才具有挑战性。
图1-5里的grade_``event
表还隐含了这样一个要求:所有的日期必须唯一,因为每个日期要用于链接score
和grade_``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表
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
的表,其中包含三列:name
、sex
和student_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将某些约束施加到两个表之间的相互关系上,这种做法对成绩考评项目的各个表来讲是很有必要的。
student``表
和grade_event
表里存在时,才允许考试成绩进入score
表。student
表里存在时,才允许考勤情况进入absence
表。为了实施这些约束,我们需要建立一些外键关系。这里的“外”表示的意思是“在另一个表里”,而“外键”指的是必须与另一个表里的某个键值相匹配的键值。随着后面更多成绩考评项目表的创建,这些概念将会变得越来越清晰。
在早些时候,我们在创建美史联盟的表(president
和member
)时,并没有使用ENGINE
子句,因此服务器会使用默认存储引擎来创建它们。如前所述,默认存储引擎是InnoDB(除非服务器被重新配置过)。student
表的定义显式地包括了ENGINE = InnoDB
,防止了服务器为其配置与此不同的默认值。
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_id
和student_id
的组合才具有唯一性。在score
表里,这两个ID值自身都不具备唯一性。对于每一个event_id
值(每位学生有一个)都会有多个分数行与之对应;对于每一个student_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节)。显示地定义这个索引可以避免这一问题。
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.9节才会讲到,此时也至少应该知道下面这条语句是用来查看tbl_name
表里的全部内容的:
SELECT * FROM tbl_name;
例如:
mysql> **SELECT * FROM student;**
Empty set (0.00 sec)
现在,mysql
报告说该表为空,但在练习完本节的几个示例之后,你会看到不一样的结果。
往数据库里添加数据的办法有好几种。可以用INSERT
语句将行手工插到表中,也可以利用文件把行添加到表里。该文件的内容既可以是一系列事先编写好的能直接提供给mysql
的INSERT
语句,也可以是通过LOAD DATA
语句或mysqlimport
客户端程序来加载的原始数据值。
本节将演示各种把记录插到表中的方法。大家应该多练习这些方法,熟悉和掌握它们的工作原理以及用法。在练习完这些方法之后,再转到1.4.8节,运行那里的命令。那些命令可以用来删除这些表,然后再重建它们,并将本书提供的数据加载到这些表里。这样,你的数据库所包含的内容就会与我在后面示例中用到的数据保持一致,而你在练习本书其他示例时也会看到相同的结果。如果你已知道如何插入行,那么可以直接跳过本节。
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_id
和event_id
赋值,因此MySQL会将默认值NULL
赋给它们。又因为member_id
和event_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`))
有错误消息出现,即表明这些约束发挥了作用。
另一种把行载入表中的方法是,直接从文件里读取它们。该文件可以包含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.txt
和president.txt
的文件分别加载到member
表和president
表里。这意味着,你应该仔细挑选数据文件名,否则,mysqlimport
将无法使用正确的表名。如果想要加载文件member1.txt
和member2.txt
,那么mysqlimport
会认为是要把这两个文件分别加载到名为member1
和member2
的表里去。如果你真的想将这两个文件加载到member
表里,那么可以分别将它们命名为member.1.txt
和member.2.txt
,或者member.txt1
和member.txt2
。
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**
无论使用哪条命令,如果需要在命令行里指定连接参数,那么请把它们放到命令名的后面,且紧挨着它。
现在,我们的表都建好了,并且加载了数据。下面一起来看看如何使用这些数据。使用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
语句时,需要先指定检索的内容,然后再加上一些可选的子句。上面显示的两个子句(FROM
和WHERE
)是最为常见的,尽管还可以指定其他子句,如GROUP BY
、ORDER BY
和LIMIT
。请记住,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节。
要想限制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()
会特别方便。
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 NULL
或IS 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特有的<=>比较运算符可用于NULL
与NULL
的比较。将前面两个查询语句改写成使用这个运算符的查询语句:
SELECT last_name, first_name FROM president WHERE death <=> NULL;
SELECT last_name, first_name, suffix
FROM president WHERE NOT (suffix <=> NULL);
每位MySQL用户最终都会注意到这样一种情况:在你创建一个表,并存入一些行之后,使用 “SELECT ``* ``FROM
tbl_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
子句的默认排序方式是升序排列。在其中的列名后面加上关键字ASC
或DESC
,可以指定是按照升序排列还是按照降序排列。例如,想让美国总统的姓名按姓的逆序(降序)排列显示,那么就要使用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
作为辅助列按姓继续进行排序。
查询结果往往有很多行,如果只想看到其中的一小部分,那么可以在查询命令里增加一条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 |
+-----------+-------------+
到目前为止,大部分查询语句都是直接通过检索表中的值来获得输出结果。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_name
和last_name
两列之间的逗号。结果,first_name
列被误命名为last_name
,而列last_name列
却不见了:
mysql> **SELECT first_name last_name FROM president;
+---------------+
**| last_name |
+---------------+
| George |
| John |
| Thomas |
| James |
...
在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);
或许当时你还不太能看懂这个查询。那么现在是否已完全明白了呢?
MySQL支持模式匹配操作,这使我们能够在未给出精确比较值的情况下把行查出来。模式匹配需要使用像LIKE
和NOT LIKE
那样的运算符,并且需要指定一个包含通配字符的字符串。下划线“_
”可以匹配任何的单个字符,百分号“%
”则能匹配任何字符序列(其中包括空序列)。
下面这个模式能匹配到所有以字母W
或w
开头的姓:
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
运算符的模式匹配。关于LIKE
和REGEXP
的更多信息请参考3.5.1.1节和附录C。
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 |
+------------+---------------+
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 BY
和LIMIT
组合在一起使用。例如,想要在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
还会再生成其他的超集行,其中会包含更高层的汇总值。
到目前为止,我们查询出来的信息都是来自一个表。不过,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_``event
和score
两个表里相匹配的每一个行组合,把其中的学生ID、分数、日期和事件类别都显示出来。
这个查询与之前介绍的查询语句在以下两个方面有着显著的区别。
FROM
子句指定了多个表名,因为需要从多个表里检索信息:FROM grade_event INNER JOIN score
ON
子句指定了表grade_``event
和score
的连接条件,即这两个表的event_id
值必须相互匹配:ON grade_event.event_id = score.event_id
请注意,我们是如何通过grade_event.event_id
和score.event_id
来引用event_id
列的,其语法形式为:tbl_name.col_name
。这样,MySQL便能知道我们是在引用哪个表。因为这两个表都有event_id
列,所以在不限定表名时,会产生二义性。这条查询语句里的其他列(date
、score
和category
)可以直接使用,不用限定表名,因为它们只存在于其中的一个表里,不会产生二义性。
在连接语句里,我个人比较喜欢在每个列的前面都加上表名,从而可以让每一列属于哪个表变得更加清楚。在后面的连接语句里,我将一直沿用这个习惯。在为每列加上完整的表名之后,这个查询现在变成了下面这个样子:
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_id
和student.student_id
。ON
子句里多了一个查询条件,用于指定score
表里的行与student
表里的行必须基于学生ID匹配在一起:ON ... score.student_id = student.student_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