SQL学习指南(第3版)

978-7-115-58380-2
作者: [美]艾伦·博利厄(Alan Beaulieu)
译者: 杨云
编辑: 傅道坤

图书目录:

详情

《SQL学习指南(第3版)》介绍了SQL语言的基础知识以及高级特性,包括SQL基本查询、过滤、多数据表查询、集合、数据操作、分组和聚合、子查询、连接、条件逻辑、事务、索引和约束、视图等内容。同时,为了适应近年来数据库领域的发展变化,本书针对大数据、SQL跨平台数据库服务和数据分析等领域的需求,增加了处理大型数据库的实现策略和扩展技术,以及报表和分析工具等内容。 《SQL学习指南(第3版)》内容循序渐进,每章的主题相对独立,并提供了丰富、可扩展的示例,同时还配备精选练习,有利于读者有效学习和快速掌握SQL语言。本书适合作为数据库应用开发者和数据库管理员的必备入门书,也可供SQL相关从业者查阅和参考。

图书摘要

版权信息

书名:SQL学习指南(第3版)

ISBN:978-7-115-58380-2

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [美] 艾伦•博利厄(Alan Beaulieu)

译    杨 云

责任编辑 傅道坤

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Copyright © 2020 Alan Beaulieu. All rights reserved.

Simplified Chinese Edition, jointly published by O’Reilly Media, Inc. and Posts & Telecom Press, 2022. Authorized translation of the English edition, 2020 O’Reilly Media, Inc., the owner of all rights to publish and sell the same.

All rights reserved including the rights of reproduction in whole or in part in any form.

本书中文简体字版由O’Reilly Media, Inc.授权人民邮电出版社有限公司出版。未经出版者书面许可,对本书的任何部分不得以任何方式复制或抄袭。

版权所有,侵权必究。


本书介绍了SQL语言的基础知识以及高级特性,包括SQL基本查询、过滤、多数据表查询、集合、数据操作、分组和聚合、子查询、连接、条件逻辑、事务、索引和约束、视图等内容。同时,为了适应近年来数据库领域的发展变化,本书针对大数据、SQL跨平台数据库服务和数据分析等领域的需求,增加了处理大型数据库的实现策略和扩展技术,以及报表和分析工具等内容。

本书内容循序渐进,每章的主题相对独立,并提供了丰富、可扩展的示例,同时还配备精选练习,有利于读者有效学习和快速掌握SQL语言。本书适合作为数据库应用开发者和数据库管理员的必备入门书,也可供SQL相关从业者查阅和参考。


O’Reilly以“分享创新知识、改变世界”为己任。40多年来我们一直向企业、个人提供成功所必需之技能及思想,激励他们创新并做得更好。

O’Reilly业务的核心是独特的专家及创新者网络,众多专家及创新者通过我们分享知识。我们的在线学习(Online Learning)平台提供独家的直播培训、图书及视频,使客户更容易获取业务成功所需的专业知识。几十年来,O’Reilly图书一直被视为学习开创未来之技术的权威资料。我们每年举办的诸多会议是活跃的技术聚会场所,来自各领域的专业人士在此建立联系,讨论最佳实践并发现可能影响技术行业未来的新趋势。

我们的客户渴望做出推动世界前进的创新之举,我们希望能助他们一臂之力。

“O’Reilly Radar博客有口皆碑。”

      ——Wired

“O’Reilly凭借一系列非凡想法(真希望当初我也想到了)建立了数百万美元的业务。”

      ——Business 2.0

“O’Reilly Conference是聚集关键思想领袖的绝对典范。”

      ——CRN

“一本O’Reilly的书就代表一个有用、有前途、需要学习的主题。”

      ——Irish Times

“Tim是位特立独行的商人,他不光放眼于最长远、最广阔的领域,并且切实地按照Yogi Berra的建议去做了:‘如果你在路上遇到岔路口,那就走小路。’回顾过去,Tim似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大路也不错。”

      ——Linux Journal


编程语言一直在不断地发展变化,如今所使用的编程语言很少有十年以上的历史,其中为数不多的例子包括:COBOL,目前仍大量应用于大型机环境中;Java,诞生于20世纪90年代中期,已成为最流行的编程语言之一;C,在操作系统和服务器开发以及嵌入式系统中依然颇受欢迎。在数据库领域,SQL的根源可以追溯到20世纪70年代。

SQL最初是一种为生成、操作和检索关系型数据库的数据而创建的语言,而关系型数据库已经有40多年的历史。但在过去的十年间,Hadoop、Spark和NoSQL等数据平台已经获得了众多的关注,吞食了关系型数据库的市场。然而,正如本书最后几章所讨论的那样,SQL语言也在不断演变,以便从各种平台检索数据,无论这些数据是存储在数据表、文档还是平面文件中。

无论是否会用到关系型数据库,只要你从事数据科学、商业智能或其他数据分析领域的工作,都可能需要了解SQL以及Python和R等语言/平台。数据无处不在,铺天盖地,通常以迅猛之势出现在你的面前,能够从这些数据中提取出有意义信息的人才在市场上供不应求。

有很多书会把读者当作新手或者门外汉,这类书的内容往往流于表面。另一种书则属于参考指南,会事无巨细地描述语言中每个语句的各种写法,如果你非常清楚自己要干什么,但是需要了解明确的语法,那么这类指南可以助你一臂之力。本书则另辟蹊径,努力寻找这两者的平衡点,从SQL语言的背景开始,逐步学习基础知识,然后进阶探索一些能让读者一鸣惊人的高级特性。此外,本书最后一章展示了如何在非关系型数据库中查询数据,这是入门书中很少涉及的话题。

本书共分为18章和2个附录,具体内容如下。

第1章:背景知识,探讨计算机化数据库的发展历史,包括关系模型和SQL语言的兴起。

第2章:创建和填充数据库,演示如何创建MySQL数据库、生成本书中示例所需的数据表并向其中填充数据。

第3章:查询入门,介绍select语句并进一步演示最常见的子句(select、from、where等)。

第4章:过滤,演示可以在select、update或delete语句的where子句中使用的各种条件。

第5章:多数据表查询,介绍查询如何通过数据表连接使用多个数据表。

第6章:使用集合,专门介绍集合以及如何在查询中运用集合。

第7章:数据生成、操作和转换,演示一些用于操作或转换数据的内建函数。

第8章:分组和聚合,介绍如何聚合数据。

第9章:子查询,介绍子查询(作者个人的偏爱)并展示了其用法以及应用场景。

第10章:再谈连接,进一步探讨各种类型数据表的连接。

第11章:条件逻辑,探讨如何在select、insert、update和delete语句中使用条件逻辑(即if-then-else)。

第12章:事务,介绍事务并展示其用法。

第13章:索引和约束,探讨索引和约束相关的内容。

第14章:视图,介绍如何构建可以使用户远离数据复杂性的界面。

第15章:元数据,演示数据字典的效用。

第16章:分析函数,涵盖各种分析函数,用于生成排名、小计以及其他在报表生成和分析中经常使用的值。

第17章:处理大型数据库,演示各种用于简化大型数据库管理和遍历的技术。

第18章:SQL和大数据,探讨SQL语言的变化,以便从非关系型数据平台中检索数据。

附录A:示例数据库的ER图,介绍用于本书中所有示例的数据库模式。

附录B:练习答案,提供每章练习的答案。

感谢我的编辑Jeff Bleiel,正是在你的帮助下,本书才顺利付梓,同时感谢Thomas Nield、Ann White-Watkins和Charles Givre,感谢你们帮我审阅书稿。还要感谢Deb Baker、Jess Haberman以及参与本书出版事务的O’Reilly Media公司的其他同仁。最后,感谢我的妻子Nancy、女儿Michelle和Nicole,感谢你们的激励与鼓舞。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书审校等工作,可以发邮件给本书的责任编辑(fudaokun@ptpress.com.cn)。

如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近40年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


在着手干活之前,为了更好地理解关系型数据库和SQL语言的演进历程,还是有必要研究一下数据库技术的历史。因此,我将先介绍一些基本的数据库概念,回顾计算机化数据存储和检索的历史。

如果有读者急着想开始编写SQL查询,可以直接跳到第3章,不过我建议随后再回头阅读前两章,以便更好地了解SQL语言的历史和功用性。

数据库就是一组相关信息。例如,电话簿就是一个数据库,其中包含了生活在某一地区内所有人的姓名、电话号码和地址。尽管电话簿肯定是最为普及且常用的数据库,但它也存在以下问题。

电话簿的种种缺点同样体现在所有的手工数据存储系统上,比如存储在文件柜中的患者记录。由于纸质数据库使用不便,一些早先开发的计算机应用程序就是数据库系统,采用了计算机化的数据存储和检索机制。由于数据库系统使用电子化代替纸张存储数据,因此能够更快地检索数据、以多种方式编制索引,并为用户群提供即时信息。

早期的数据库系统用磁带存储被管理的数据。磁带的数量通常远多于磁带机,因此在请求特定数据时,需要由技术人员手动装卸磁带。由于那个时代的计算机内存很小,对同一数据的多次请求往往需要多次读取磁带。尽管这些数据库系统比起纸质数据库有了很大的改进,但与如今的数据库技术相差甚远(现代数据库系统能够管理PB级的数据,利用服务器集群进行访问,每个服务器在高速内存中缓存的数据可达数十GB,不过我讲的可能有点超前了)。

本节介绍了关系型数据库出现之前的一些背景信息,如果读者急于学习SQL,可以直接跳到下一节。

在计算机化数据库系统发展的最初几十年里,数据以各种形式存储并展现给用户。例如,在层次数据库系统中,以一个或多个树形结构来表示数据。图1-1展示了以树形结构表示的George Blake和Sue Smith的银行账户数据。

图1-1 账户数据的层次化视图

George和Sue的数据树都包含了各自的账户以及交易信息。层次数据库系统提供了定位特定客户信息树的工具,并能够遍历该树找到所需的账户和/或交易。树中的每个节点都具有0个或1个父节点,以及0个、1个或多个子节点。这种配置称为单根层次结构。

另一种数据管理方式是网状数据库系统,它表现为多个记录以及定义不同记录之间关系的多个链接。图1-2展示了George和Sue的账户在此系统中的视图。

图1-2 账户数据的网状视图

为了查找Sue的MoneyMkt账户交易信息,需要执行以下步骤:

1.查找Sue Smith的客户记录;

2.沿着Sue Smith的客户记录链接找到其账户列表;

3.遍历账户列表直至找到MoneyMkt账户;

4.沿着MoneyMkt账户记录链接找到其交易列表。

网状数据库系统的一个值得注意的特性是图1-2最右侧的一组product记录。注意,每个product记录(Checking、Savings等)都指向一个account记录列表,以指定这些账户记录的产品类型。因此account记录可以通过多个入口(customer记录或product记录)进行访问,这使得网状数据库具有多根层次的特点。

层次数据库和网状数据库依然活跃在今天,不过通常只能在大型机世界中找到。另外,层次数据库已在目录服务领域中重获新生,比如Microsoft的Active Directory和开源的Apache Directory Server。然而,在20世纪70年代初,一种表示数据的全新方式开始生根,这种方式更为严谨,且易于理解和实现。

1970年,IBM研究院的E.F.Codd博士发表了一篇题为“A Relational Model of Data for Large Shared Data Banks”(大型共享数据银行的数据关系模型)的论文,提出使用数据表集合来表示数据,但相关实体之间并不是用指针来导航的,而是借助冗余数据来链接不同表中的记录。图1-3展示了George和Sue在关系模型中的账户信息。

图1-3 账户数据的关系视图

图1-3中包含了4个数据表:Customer、Product、Account、Transaction。首先查看图中顶部的Customer表,该表共有3列:cust_id(客户的ID号)、fname(客户的名字)、lname(客户的姓氏)。Customer表共有2行,分别为George Blake 和Sue Smith 的数据。数据表的列数视不同的数据库服务器而异,但数量通常足够大,无须为此担心(比如Microsoft SQL Server允许最多1,024列的数据表)。至于数据表的行数,相较于受数据库服务器的限制,更多是受限于物理设备(比如,可用的磁盘空间大小)和可维护性(比如,多大的数据表才不会造成管理方面的麻烦)。

关系型数据库中每个数据表都包含能够唯一标识某一行的信息[称为主键(primary key)],以及完整描述实体所需的额外信息。再来看Customer表,每位客户的cust_id列都保存着不同的数字;比如,George Blake可由客户ID #1来唯一标识。其他客户都不能够获得这个标识符,在Customer表中,不再需要其他信息来定位George Blake的数据。

每种数据库服务器都提供了相应的机制来生成用作主键的唯一数字,所以你不用操心跟踪已分配的数字。

尽管也可以选择使用fname列和lname列共同作为主键(由两个或多于两个列组成的主键称为复合主键),但在银行账户中出现两个或多个人同名的情况是很常见的事。因此,我选择cust_id列作为Customer表的主键。

在本例中,选择fname/lname作为主键,称之为自然键(natural key),选择cust_id作为主键,则称之为代理键(surrogate key)。使用哪一种键取决于数据库设计人员,但在本例中,该怎么选择是显而易见的,因为人的姓氏(last name)可能会改变(比如有的人结婚后使用其配偶的姓氏),而主键列在被赋值后是绝不允许被修改的。

一些数据表中还包含了导航到其他数据表的信息,这就是之前提到的“冗余数据”。例如,Account表中的cust_id列包含了已开设过账户的客户的唯一标识,product_cd列则包含了该账户所关联产品的唯一标识。这些列称为外键(foreign key),其作用与层次化和网状的账户信息形式中不同实体之间的连线一样。如果要查找特定账户记录,想知道开户人的详细信息,可以获取cust_id列的值,用该值在Customer表中查询相应的行(用关系型数据库的专业术语来说,该过程称为连接,我们会在第3章对其进行介绍,在第5章和第10章展开深入讨论)。

同样的数据存储多次,看起来似乎是一种浪费,但是对于该存储什么样的冗余数据,关系模型十分清晰。例如,在Account表中加入一列,用于已开设账户的客户的唯一标识符,这种做法没有问题,但如果在表中再增加客户的名字和姓氏,就不合适了。如果客户改名了,需要确保数据库中仅有一处保存了客户的姓名;否则,可能出现数据在一处更改了,而在另一处没有更改,造成数据库中数据不可靠。适合保存姓名的是Customer表,只有cust_id的值应该来自其他表。在一列中包含多种信息,比如在name列中包含姓氏和名字,或是在address列中包含街道、城市、州和邮政编码信息,同样不合适。改进数据库设计以确保独立信息仅出现在一处(外键除外)的过程称为规范化(normalization)。

回到图1-3中的4个数据表,你也许想知道如何使用这些表来查找George Blake的支票账户(checking account)交易。首先,在Customer表中找到George Blake的唯一标识符。然后,在Account表中找到满足以下条件的行:cust_id列包含George的唯一标识符,product_cd列与Product表中name列等于"Checking"的行匹配。最后,定位到Transaction表中account_id列与Account表中唯一标识符匹配的行。这个过程看起来挺复杂,其实使用SQL语言的话,一个命令就能搞定,很快你就会看到。

前几节介绍了一些新术语,是时候给出正式的定义了。表1-1中列出了本书后续要用到的术语及其定义。

表1-1 术语及其定义

术语

定义

实体

数据库用户关注的对象,包括客户、部门、地理位置等

数据表中存储的数据片段

列的集合,共同用于完整地描述某个实体或对某个实体的操作。也称为记录

行的集合,要么保存在内存中(非持久性),要么保存在永久存储中(持久性)

结果集

非持久性数据表的别称,通常是SQL查询的结果

主键

用于唯一标识数据表中各行的一列或多列

外键

用于唯一标识其他数据表中某行的一列或多列

Codd基于对关系模型的定义,他提出了一种叫作DSL/Alpha的语言,用于操作关系数据表中的数据。在Codd的论文发表后不久,IBM委托一个小组根据Codd的想法建立一个原型。这个小组创建了DSL/Alpha的简化版本SQUARE。经过对SQUARE的改进,产生了SEQUEL语言,最终该语言被命名为SQL。尽管SQL最初是用于操作关系型数据库中的数据,但如今已经演变为一种可以处理各种数据库技术的语言(正如你将在本书结尾看到的)。

SQL现在已有40多年的历史,其间经历了很大的变化。20世纪80年代中期,美国国家标准协会(American National Standards Institute,ANSI)开始制定SQL语言的第一个标准,该标准最终于1986年发布。随后经过改进,陆续在1989年、1992年、1999年、2003年、2006年、2008年、2011年、2016年发布了新版本。在改良核心语言的同时,SQL语言中还添加了一些新特性,引入了面向对象功能等。之后的标准侧重于相关技术的集成,比如可扩展标记语言(XML)和JavaScript对象表示法(JSON)。

SQL与关系模型相辅相成,因为SQL查询的结果就是数据表(在该上下文中,也叫作结果集)。所以,在关系型数据库中,新的永久性数据表可以通过存储查询的结果集来创建。同样,查询可以使用永久性数据表和其他查询的结果集作为输出(我们会在第9章讨论相关细节)。

最后强调一点:SQL并不是某种缩写[尽管很多人坚持认为它表示“Structured Query Language”(结构化查询语言)]。在提及该语言时,可以使用独立的字母(S.Q.L)或使用“sequel”。

SQL语言被划分为若干部分:我们在本书中研究的部分包括SQL模式语句(SQL schema statement),用于定义存储在数据库中的数据结构;SQL数据语句(SQL data statement),用于操作之前使用SQL模式语句定义的数据结构;SQL事务语句(SQL transaction statement),用于启动、结束、回滚事务(相关概念会在第12章介绍)。例如,要想在数据库中创建新表,可以使用SQL模式语句create table,要在新表中填充数据,则需要使用SQL数据语句insert。

下面给出创建数据表corporation的SQL模式语句:

CREATE TABLE corporation
 (corp_id SMALLINT,
  name VARCHAR(30),
  CONSTRAINT pk_corporation PRIMARY KEY (corp_id)
 );

该语句创建出一个包含两列数据(corp_id和name)的表,其中corp_id列作为该表的主键。这个语句的具体细节(比如MySQL可用的数据类型)我们留待在第2章中仔细研究。接下来的SQL数据语句向corporation表中插入了一行关于Acme Paper Corporation的数据:

INSERT INTO corporation (corp_id, name)
VALUES (27, 'Acme Paper Corporation');

该语句向corporation表中添加了一行数据,其中corp_id列的值为27,name列的值为Acme Paper Corporation。

最后是一个简单的select语句,检索刚才创建好的数据:

mysql< SELECT name
    -> FROM corporation
    -> WHERE corp_id = 27;
+------------------------+
| name                   |
+------------------------+
| Acme Paper Corporation |
+------------------------+

通过SQL模式语句所创建的所有数据库元素都被存储在一个名为“数据字典”(data dictionary)的特殊表集合内。这些关于数据库的数据被称为“元数据”,我们将在第15章介绍。与用户创建的数据表一样,数据字典表也可以通过select语句查询,从而允许在运行时查看数据库中当前的数据结构。例如,用户需要创建展示上月新增账户的报表,那么既可以在Account表中硬编码事先已经知道的列名,也可以通过查询数据字典确定当前有哪些列,然后每次动态地生成报表。

本书大部分篇幅将聚焦于SQL语言中与数据相关的部分,包括select、update、insert、delete命令。SQL模式语句将在第2章中说明,这将引导你完成一些简单的数据表的设计和创建。一般来说,除了语法,对于SQL模式语句无须进行太多的讨论,而SQL数据语句,尽管数量寥寥,但包含了大量值得仔细研究的地方。因此,虽然我会介绍不少SQL模式语句,但本书的大多数章节还是把重点放在SQL数据语句上。

如果你有编程经验,肯定已经习惯了定义变量和数据结构、使用条件逻辑(if-then-else)和循环结构(do while...end)、把代码分解为可复用的小片段(对象、函数、过程)。代码经过编译后执行,其执行结果精确地(其实,并非总是精确地)符合预期。无论是使用Java、Python、Scala或其他过程化语言,你都可以完全控制程序的行为。

过程化语言定义了所预期的结果以及生成该结果的实现机制或过程。非过程化语言同样定义了所预期的结果,但是生成结果的过程则留给外部代理来实现。

对于SQL,你需要放弃部分已经习惯的控制权,因为SQL语句只定义了必要的输入和输出,至于如何执行语句,则由名为优化器(optimizer)的数据库引擎组件来处理。优化器的工作是检查SQL语句,考虑数据表的配置以及可用的索引,并决定最有效的执行路径(好吧,也未必总是最有效的)。大多数数据库引擎都允许你通过指定优化器提示(optimizer hint)来影响优化器的决策,比如建议使用特定的索引等,但大多数SQL用户从来不需要深入到该复杂层面,把这种事交给数据库管理员或性能调优专家来处理就行了。

因此,单凭SQL无法编写完整的应用程序,除非你只是编写简单的脚本来处理某些数据,否则需要将SQL与编程语言集成起来。一些数据库厂商已经为用户考虑了这些,比如Oracle的PL/SQL语言、MySQL的存储过程语言以及Microsoft的Transact-SQL语言。在这些语言中,SQL数据语句是该语言语法的一部分,允许无缝地将数据库查询与过程化语句集成到一起。如果你使用的是Java或Python这种非数据库特定语言,则需要使用工具集/API才能在代码中执行SQL语句。有些工具集由数据库厂商提供,有些则由第三方厂商或开源代码提供者所创建。表1-2展示了将SQL集成到特定语言的部分可用选择。

表1-2 SQL集成工具集

语言

工具集

Java

JDBC (Java Database Connectivity)

C#

ADO.NET (Microsoft)

Ruby

Ruby DBI

Python

Python DB

Go

Package database/sql

如果仅仅需要交互式执行SQL命令,所有的数据库厂商都提供了至少一种简单的命令行工具,用于向数据库引擎提交SQL命令并检查执行结果。大多数厂商还提供了图形化的工具,其中包含一个显示SQL命令的窗口以及一个显示SQL命令执行结果的窗口。此外,SQuirrel等第三方工具可以通过JDBC连接到很多不同的数据库服务器。因为本书中的示例都是在MySQL数据库中执行的,所以我选择使用MySQL安装自带的命令行工具来运行示例和格式化运行结果。

之前答应过要展示一个能够返回George Blake的支票账户所有交易的SQL语句,下面就是这些SQL语句:

SELECT t.txn_id, t.txn_type_cd, t.txn_date, t.amount
FROM individual i
  INNER JOIN account a ON i.cust_id = a.cust_id
  INNER JOIN product p ON p.product_cd = a.product_cd
  INNER JOIN transaction t ON t.account_id = a.account_id
WHERE i.fname = 'George' AND i.lname = 'Blake'
  AND p.name = 'checking account';

+--------+-------------+---------------------+--------+
| txn_id | txn_type_cd | txn_date            | amount |
+--------+-------------+---------------------+--------+
|     11 | DBT         | 2008-01-05 00:00:00 | 100.00 |
+--------+-------------+---------------------+--------+
1 row in set (0.00 sec)

现在先不涉及过多的细节,该查询查找满足两个条件的行,即individual表中姓名为George Blake的行和product表中的name列为checking的行,并通过account表将两者关联起来,然后分4列返回transaction表中提交到该账户的所有交易信息。如果你恰好知道George Blake的account_id为8,product_cd为'CHK',就可以简单地根据客户ID在account表中找到George Blake的支票账户,并使用账户ID找出相应的交易:

SELECT t.txn_id, t.txn_type_cd, t.txn_date, t.amount
FROM account a
  INNER JOIN transaction t ON t.account_id = a.account_id
WHERE a.cust_id = 8 AND a.product_cd = 'CHK';

在随后的章节中,我会逐一介绍以上查询中涉及的所有概念(而且远不止这些),但这里仅展示一下其基本结构。

上述查询包含三种不同的子句:select、from和where。几乎所有查询至少都会包含这三种子句,尽管还有更多子句可用于实现更具体的目标。它们的用途如下所示:

SELECT /* 一个或多个东西 */ ...
FROM   /* 一处或多处 */ ...
WHERE  /* 一个或多个条件 */ ...

大多数SQL实现将位于“/”和“/”之间的文本视为注释。
 

在构建查询时,通常首先要确定需要哪些数据表,接着将其加入from子句。然后,向where子句加入条件,将数据表中不需要的数据过滤掉。最后,确定要检索数据表中的哪些列并将其加入select子句。下面这个简单的示例展示了如何查找姓氏为“Smith”的所有客户:

SELECT cust_id, fname
FROM individual
WHERE lname = 'Smith';

该查询搜索individual表,在其中找出lname列匹配字符串'Smith'的所有行,并返回这些行的列cust_id和fname。

除了查询数据库,很可能还需要在数据库中插入和修改数据。下面这个简单的例子,展示了如何在product表中插入新行:

INSERT INTO product (product_cd, name)
VALUES ('CD', 'Certificate of Depysit')

糟糕,这里把“Deposit”拼错了。无妨,可以使用update语句来解决:

UPDATE product
SET name = 'Certificate of Deposit'
WHERE product_cd = 'CD';

注意,与select语句一样,update语句也包含了where 子句。原因在于update语句必须标识出要修改的行,在本例中,只需要指定product_cd列与字符串'CD'相匹配的那些行。因为product_cd列是product表的主键,所以update语句只会修改一行(或者一行都不修改,如果数据表中不存在该值的话)。不管什么时候执行SQL数据语句,都会接收到来自数据库引擎的反馈,说明有多少行受到该语句的影响。如果使用交互式工具(比如之前提到的命令行工具),那么会接收到来自下列语句的反馈信息:

如果你使用了过程化语言以及之前提到的某种工具集,当你执行SQL数据语句之后,可以使用工具集提供的调用来获取语句的执行结果。一般而言,最好是检查一下执行结果,以免语句执行后出现意想不到的结果(比如忘了在delete语句中加入where子句,这会把整个数据表的行全部清空!)

商业化的关系型数据库已经有30多年的历史了。其中一些比较成熟且流行的商业产品包括:

这些数据库服务器的功能都差不多,尽管其中有些长于处理海量或超大吞吐量的数据库,而有些更适合处理对象、大文件或XML文档等。所有这些服务器都很好地遵从了最新的ANSI SQL标准。这是一件好事,本书将展示如何编写无须进行任何修改(或仅需要极少量的修改)就能够在这些平台上运行的SQL语句。

除了商业数据库服务器,开源社区以创建出能够与之抗衡的竞品为目标,在过去的20年中也开展了大量的活动,其中最常用的两个开源数据库服务器是PostgreSQL和MySQL。MySQL是免费的,其下载和安装过程都非常简单。因此,本书的所有示例均在MySQL(8.0版)上运行,并使用命令行工具格式化查询结果。即使你已经使用了其他数据库服务器,而且根本不打算使用MySQL,我也强烈建议你安装最新版的MySQL服务器,加载样本模式和数据,用本书中的数据和示例进行实验。

然而,请牢记以下注意事项:

本书并不是一本关于MySQL如何实现SQL的书。

确切地说,本书旨在教授编写不加修改或稍作修改就能运行在MySQL以及Oracle Database、DB2、SQL Server新近版本上的SQL语句。

从本书第2版到第3版的这十年间,数据库领域发生了诸多变化。尽管关系型数据库依然被大量使用,并且还将持续使用一段时间,但新的数据库技术已经涌现出来,以满足Amazon和Google等公司的需求,其中包括Hadoop、Spark、NoSQL、NewSQL,这些分布式的可扩展系统通常部署在商品服务器集群上。详细探讨这些技术超出了本书的范围,但它们与关系型数据库都存在一个共同点:SQL。

因为各种组织机构经常使用多种技术存储数据,有必要将SQL从特定的数据库服务器中抽取出来,提供一种能够跨数据库的服务。例如,报表可能需要将存储在Oracle、Hadoop、JSON文件、CSV文件、Unix日志文件中的数据汇总在一起。新一代工具已经出现,以应对这种挑战,其中最有前途的工具之一是 Apache Drill,它是一款开源查询引擎,允许用户编写查询,以访问存储在大多数数据库或文件系统中的数据。我们将在第18章探究Apache Drill。

接下来的4章的主要目标是介绍SQL数据语句,重点放在select语句的三个主要子句。此外,你将看到很多使用Sakila模式(在第2章中介绍)的示例,本书中所有的示例都围绕其展开。这是因为使用同一个已熟悉的数据库,可以使读者更易于洞察到问题的核心,而不是每次都需要从头了解所使用的数据表。如果读者觉得总是使用同一组数据表有点乏味,可以向示例数据库中添加新数据表,或者干脆自己建一个数据库来练手。

把基础打牢之后,剩余的章节将深入更多的概念,其中大多数概念相互独立。因此,如果你发现自己有些地方搞不明白,可以直接跳过去,随后再重新学习。待你阅读完成本书并练习了所有的示例后,就已经在成为一名经验丰富的SQL从业者的路上迈出了坚实的一步。


本章提供了创建第一个数据库所需的信息,以及为本书的示例创建数据表和相关数据的信息。你还将学习各种数据类型以及如何使用其创建数据表。因为书中的示例均运行于MySQL数据库,所以本章内容在一定程度上偏向于使用MySQL的特性和语法,不过大多数概念同样适用于其他数据库服务器。

如果你想用本书示例中的数据进行练习,有两种方法:

如果选择第二种方法,一旦启动场景,就会安装并启动MySQL服务器,然后加载Sakila模式和数据。这些准备好之后,就会出现一个标准的mysql>提示符,然后便可以开始查询样本数据库了。这显然是最简单的方法,我预计大多数读者都会选择这种方法。如果你觉得不错,就可以直接阅读下一节了。

如果你喜欢有自己的数据副本,并希望进行的任何修改都是永久性的,或是对安装MySQL服务器感兴趣,可以选择第一种方法,也可以选择使用托管在Amazon Web Services或Google Cloud等环境中的MySQL服务器。不管是哪种情况,都需要自行安装和配置,不过这已经超出了本书的范围。准备好数据库之后,按照下列步骤来加载Sakila样本数据库。

首先,运行命令行客户端mysql,输入密码,然后执行下述步骤。

1.进入异步社区,找到本书的页面,从中下载sakila database。

2.把文件放入本地目录下,比如C:\temp\sakila-db(换成你自己的目录路径,后两步要用到)。

3.输入source c:\temp\sakila-db\sakila-schema.sql,然后按Enter键。

4.输入source c:\temp\sakila-db\sakila-data.sql,然后按Enter键。

现在,你就得到了一个包含本书示例所需的全部数据的可用数据库。

Sakila数据库包含一家虚构的电影租借公司的数据,其中有商店、库存、电影、客户、付款等数据表。虽然真实的电影租借商店基本上已成为过去,但我们可以忽略数据表staff和address,把数据表store改名为streaming_service,将商店重新包装成一家流媒体电影公司。不过本书中的示例还是坚持使用原先的脚本(script)[1](双关语)。

除非你使用的是临时数据库会话(2.1节中提到的第二种方法),否则需要命令行工具mysql才能与数据库交互。为此,打开一个Windows或Unix shell,执行mysql。例如,如果你使用root账号登录,需要执行下列命令:

mysql -u root -p;

然后会要求输入密码,密码无误后,就会出现mysql>提示符。要想查看所有可用的数据库,使用下列命令:

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sakila             |
| sys                |
+--------------------+
5 rows in set (0.01 sec)

因为要使用Sakila数据库,所以要通过use命令指定数据库:

mysql> use sakila;
Database changed

启动命令行工具mysql的时候,可以同时指定要使用的用户名和数据库:

mysql -u root -p sakila;

这样就不必每次启动工具时都输入use sakila;。现在,会话已经建立,数据库也指定好了,接下来就可以执行SQL语句并查看执行结果了。例如,如果你想知道当前日期和时间,执行下列查询:

mysql> SELECT now();
+---------------------+
| now()               |
+---------------------+
| 2019-04-04 20:44:26 |
+---------------------+
1 row in set (0.01 sec)

now()是MySQL的内建函数,可以返回当前日期和时间。如你所见,命令行工具mysql会使用由字符+、−、|组成的矩形来格式化查询结果。结果输出完毕之后(在本例中,结果只有一行),mysql还会显示返回的行数以及执行该SQL语句的时长。

缺失的子句

某些数据库服务器要求查询语句中必须包含from子句,并在其中至少指明一个数据表,比如广泛使用的Oracle Database。如果只是调用函数,Oracle为此提供了一个特殊的数据表dual,该数据表仅由名为dummy的一列组成,并且只包含一行。为了与Oracle Database兼容,MySQL也提供了dual数据表,之前获取当前日期和时间的查询也可以写作:

mysql> SELECT now()
          FROM dual;
+---------------------+
| now()               |
+---------------------+
| 2019-04-04 20:44:26 |
+---------------------+
1 row in set (0.01 sec)

如果你没有使用Oracle Database,也不需要与其保持兼容,那么完全可以忽略dual数据表,只使用不包含from子句的select语句。

用了命令行工具mysql之后,只需要输入quit;或exit;就可以返回Windows或UNIX shell。

一般来说,所有流行的数据库都能够存储同样的数据类型,比如字符串、日期、数值等。它们之间的差异通常在于一些特殊的数据类型,比如XML、JSON文档或者空间数据(spatial data)。因为本书介绍的是SQL,而且你将遇到的列中98%都属于简单数据类型,所以本章只涉及字符型、日期型和数值型。使用SQL查询JSON文档留待第18章讨论。

字符型数据可以使用定长或变长字符串来存储,两者的不同点在于定长字符串会使用空格向右填充,并始终占用同样数量的字节;变长字符串不需要向右填充,且占用的字节数不固定。在定义字符型的列时,必须指定该列所能存储字符串的最大长度。例如,如果希望存储最大长度为20个字符的字符串,可以使用下面的定义方式:

char(20)    /* 定长 */
varchar(20) /* 变长 */

采用定长字符串的列,目前允许的最大长度为255字节,而采用变长字符串的列,最大长度则为65,535字节。如果需要存储更长的字符串(比如电子邮件、XML文档等),则要使用某种文本类型(mediumtex和longtext),本节随后会讲到。一般来说,如果列中存储的所有字符串长度都一样(比如州的缩写),应该使用char类型;如果字符串长度各不相同,则应该使用varchar类型。char和varchar类型的用法在主流数据库服务器中都差不多。

在Oracle Database中使用varchar时会导致异常。Oracle用户在定义变长字符串列的时候应该使用varchar2。

1.字符集

对于使用拉丁字母的语言,其字符数量很少,只需要单字节就能存储每个字符。其他一些语言,如日语和韩语,则包含了大量字符,需要多字节来存储每个字符,这种字符集称为多字节字符集。

MySQL可以使用各种字符集存储数据,无论是单字节还是多字节。可以使用show命令查看数据库服务器所支持的字符集,如下所示。

mysql> SHOW CHARACTER SET;
+----------+---------------------------------+---------------------+--------+
| Charset  | Description                     | Default collation   | Maxlen |
+----------+---------------------------------+---------------------+--------+
| armscii8 | ARMSCII-8 Armenian              | armscii8_general_ci |      1 |
| ascii    | US ASCII                        | ascii_general_ci    |      1 |
| big5     | Big5 Traditional Chinese        | big5_chinese_ci     |      2 |
| binary   | Binary pseudo charset           | binary              |      1 |
| cp1250   | Windows Central European        | cp1250_general_ci   |      1 |
| cp1251   | Windows Cyrillic                | cp1251_general_ci   |      1 |
| cp1256   | Windows Arabic                  | cp1256_general_ci   |      1 |
| cp1257   | Windows Baltic                  | cp1257_general_ci   |      1 |
| cp850    | DOS West European               | cp850_general_ci    |      1 |
| cp852    | DOS Central European            | cp852_general_ci    |      1 |
| cp866    | DOS Russian                     | cp866_general_ci    |      1 |
| cp932    | SJIS for Windows Japanese       | cp932_japanese_ci   |      2 |
| dec8     | DEC West European               | dec8_swedish_ci     |      1 |
| eucjpms  | UJIS for Windows Japanese       | eucjpms_japanese_ci |      3 |
| euckr    | EUC-KR Korean                   | euckr_korean_ci     |      2 |
| gb18030  | China National Standard GB18030 | gb18030_chinese_ci  |      4 |
| gb2312   | GB2312 Simplified Chinese       | gb2312_chinese_ci   |      2 |
| gbk      | GBK Simplified Chinese          | gbk_chinese_ci      |      2 |
| geostd8  | GEOSTD8 Georgian                | geostd8_general_ci  |      1 |
| greek    | ISO 8859-7 Greek                | greek_general_ci    |      1 |
| hebrew   | ISO 8859-8 Hebrew               | hebrew_general_ci   |      1 |
| hp8      | HP West European                | hp8_english_ci      |      1 |
| keybcs2  | DOS Kamenicky Czech-Slovak      | keybcs2_general_ci  |      1 |
| koi8r    | KOI8-R Relcom Russian           | koi8r_general_ci    |      1 |
| koi8u    | KOI8-U Ukrainian                | koi8u_general_ci    |      1 |
| latin1   | cp1252 West European            | latin1_swedish_ci   |      1 |
| latin2   | ISO 8859-2 Central European     | latin2_general_ci   |      1 |
| latin5   | ISO 8859-9 Turkish              | latin5_turkish_ci   |      1 |
| latin7   | ISO 8859-13 Baltic              | latin7_general_ci   |      1 |
| macce    | Mac Central European            | macce_general_ci    |      1 |
| macroman | Mac West European               | macroman_general_ci |      1 |
| sjis     | Shift-JIS Japanese              | sjis_japanese_ci    |      2 |
| swe7     | 7bit Swedish                    | swe7_swedish_ci     |      1 |
| tis620   | TIS620 Thai                     | tis620_thai_ci      |      1 |
| ucs2     | UCS-2 Unicode                   | ucs2_general_ci     |      2 |
| ujis     | EUC-JP Japanese                 | ujis_japanese_ci    |      3 |
| utf16    | UTF-16 Unicode                  | utf16_general_ci    |      4 |
| utf16le  | UTF-16LE Unicode                | utf16le_general_ci  |      4 |
| utf32    | UTF-32 Unicode                  | utf32_general_ci    |      4 |
| utf8     | UTF-8 Unicode                   | utf8_general_ci     |      3 |
| utf8mb4  | UTF-8 Unicode                   | utf8mb4_0900_ai_ci  |      4 |
+----------+---------------------------------+---------------------+--------+
41 rows in set (0.04 sec)

如果第4列maxlen的值大于1,则该字符集为多字节字符集。

在之前版本的MySQL服务器中,默认字符集是latin1,但在版本8中改为了utf8mb4。你可以为数据库中每个字符型的列选择不同的字符集,甚至可以在同一个数据表内存储不同的字符集数据。如果为数据列指定非默认字符集,只需要在类型定义后加上系统支持的字符集名称,例如:

varchar(20) character set latin1

在MySQL中,还可以设置整个数据库的默认字符集:

create database european_sales character set latin1;

对一本入门书而言,目前已介绍的字符集知识已经足够了,但涉及国际化主题的内容远不止于此。

2.文本数据

如果需要存储的数据超出了varchar类型的最大长度(64KB),则需要使用文本类型。

表2-1展示了可用的文本类型及其最大长度。

表2-1 MySQL文本类型及其最大长度

文本类型

最大长度/字节

tinytext

255

text

65,535

mediumtext

16,777,215

longtext

4,294,967,295

在选择文本类型时,需要注意下列事项。

如果创建的列用于存储形式不限(free-form)的数据,比如使用notes列保存客户与公司客服部门之间的沟通数据,那么使用varchar类型通常就足够了。不过如果需要存储文档,应该选择mediumtext或longtext类型。

char和varchar2类型的列在Oracle Database中的最大长度分别为2000字节和4,000字节。对于更大的文档,可以选择clob类型。SQL Server可以处理最大长度为8,000字节的char和varchar类型的数据,但可以在定义为varchar(max)的列中存储最大长度达2GB的数据。

尽管设计单一的数值类型“numeric”似乎也算合理,但实际上数值类型不止一种,分别反映了使用数值的不同方式:

指明客户订单是否已经运送的列

此类型的值称为布尔型,0表示false,1表示true。

系统生成的事务数据表的主键

此类型的值通常从1开始,每次增加1,最终的值可能非常大。

客户电子购物篮中的商品编号

此类型的值可以是1到200(对于购物狂)之间的正整数。

电路板钻孔机的位置数据

高精度的科学或制造业数据往往需要精确到小数点后8位。

为了处理此类型的数据(还有更多类型的数据),MySQL提供了多种数值类型。最常用的是存储整数的数值类型,在这种类型前面可以加上unsigned,以通知服务器该列中存储的所有数据均大于或等于0。表2-2展示了5种用于存储整数的不同数据类型。

表2-2 MySQL的整数类型

类型

signed的取值范围

unsigned的取值范围

tinyint

−128~127

0~255

smallint

−32,768~32,767

0~65,535

mediumint

−8,388,608~8,388,607

0~16,777,215

int

−2,147,483,648~2,147,483,647

0~4,294,967,295

bigint

−2^63~2^63−1

0~2^64−1

如果使用上述整数类型创建列,MySQL会为数据分配适合的存储空间,从1字节(tinyint)到8字节(bigint)。因此,应该选择正好能够容纳要保存的最大数值的类型,以避免浪费存储空间。

对于浮点数(如3.1415927),可以选择表2-3中所示的数值类型。

表2-3 MySQL的浮点数类型

类型

取值范围

float(p,s)

−3.402823466E+38~−1.175494351E−38
和1.175494351E−38~3.402823466E+38

double(p,s)

−1.7976931348623157E+308~−2.2250738585072014E−308
和2.2250738585072014E−308~1.7976931348623157E+308

当使用浮点数类型时,可以指定其精度(小数点左右两边所允许的数字位数)和有效位(小数点右边所允许的数字位数),不过这不是必需的。这两个值在表2-3中由p和s表示。如果为浮点数类型的列指定了精度和有效位,记住,超出有效位和/或精度的数据会被四舍五入。例如,一个定义为float(4,2)的列将会存储4位数字,其中2位在小数点左边,另外2位在小数点右边。因此,该列允许出现数值27.44和8.19,但是17.8675将会被四舍五入为17.87,而如果试图向定义为float(4,2)的列中存储数值178.375,则会产生错误。

和整数类型一样,浮点数类型的列也可以被定义为unsigned,但这只表示禁止列中存储负数,并不会改变该列所存储数据的取值范围。

除了字符串和数值,几乎免不了与日期和/或时间打交道。这种类型的数据称为时间型(temporal)数据,数据库中使用时间型数据的一些示例包括:

MySQL为以上所有情况都提供了合适的数据类型。表2-4展示了MySQL支持的时间数据类型。

表2-4 MySQL的时间数据类型

类型

默认格式

取值范围

date

YYYY-MM-DD

1000-01-01~9999-12-31

datetime

YYYY-MM-DD HH:MI:SS

1000-01-01 00:00:00.000000
~9999-12-31 23:59:59.999999

timestamp

YYYY-MM-DD HH:MI:SS

1970-01-01 00:00:00.000000
~2038-01-18 22:14:07.999999

year

YYYY

1901~2155

time

HHH:MI:SS

−838:59:59.000000~838:59:59.000000

数据库服务器可以用各种方式存储时间型数据,格式化字符串(表2-4的第2列)的目的在于指明这些数据在被检索时的显示方式,以及在插入或更新时间型数据列时该如何构建日期字符串。因此,如果需要以默认格式YYYY-MM-DD向date列中插入日期2005年3月23日,可以使用字符串'2005-03-23'。第7章将全面探讨如何构建和显示时间型数据。

datetime、timestamp和time类型也允许包含小数点后面最多有6位数字的秒数(微秒)。当使用这些数据类型定义列时,可以提供一个0~6的数字,例如,如果指定datetime(2),则表示允许时间精确到1/100秒。

每种数据库服务器所允许的时间类型列的日期范围各不相同。Oracle Datebase接受的日期范围是公元前4712年至公元9999年,SQL Server则只能处理公元1753年至公元9999年(除非使用SQL Server 2008的datetime2数据类型,其日期范围从公元1年至公元9999年)。MySQL位于Oracle和SQL Server之间,其时间范围是公元1000年至公元9999年。对于大多数跟踪当前和未来事件的系统来说,这并没有什么不同,但是如果存储的是历史日期,就需要注意了。

表2-5描述了表2-4中日期格式的各个组成部分。

表2-5 日期格式的组成部分

组成部分

定义

取值范围

YYYY

年份,包括世纪

1000~9999

MM

月份

01(1月)~12(12月)

DD

01~31

HH

小时

00~23

HHH

小时(已逝去的)

−838~838

MI

分钟

00~59

SS

00~59

下面介绍如何使用各种时间类型来实现之前给出的示例。

第7章将探讨这些时间数据类型的用法。

现在你已经清楚地知道了MySQL数据库中可以存储的数据类型,是时候学习如何使用这些类型来创建数据表了。首先从定义保存个人信息的数据表开始。

设计数据表的一个好方法是先来点头脑风暴,想想把哪些有用的信息纳入表中。下面是我经过短暂思考之后,认为能够描述个人的信息种类:

这些信息显然还不够详尽,不过目前已经够用了。下一步是指定列名和数据类型。表2-6展示了初步设计结果。

表2-6 person数据表(初步设计结果)

类型

允许的值

name

varchar(40)

eye_color

char(2)

BL、BR、GR

birth_date

date

address

varchar(100)

favorite_foods

varchar(200)

name、address、favorite_foods列的类型为varchar,数据格式不限。eye_color列只允许2个字符,且必须为BR、BL或GR。birth_date列的类型为date,这里不需要时间部分。

我们在第1章介绍过规范化的概念,也就是在设计数据库时要确保不出现重复列(不包括外键)或复合列。再检查一遍person数据表,存在以下问题。

考虑到这些问题后,表2-7给出了person数据表的规范化版本。

表2-7 person数据表(二次修改结果)

类型

允许的值

person_id

smallint(unsigned)

first_name

varchar(20)

last_name

varchar(20)

eye_color

char(2)

BR、BL、GR

birth_date

date

street

varchar(30)

city

varchar(20)

state

varchar(20)

country

varchar(20)

postal_code

varchar(20)

person数据表现在已经有了能够保证唯一性的主键(person_id),下一步是构建favorite_food数据表,在其中加入指向person数据表的外键,如表2-8所示。

表2-8 favorite_food数据表

类型

person_id

smallint(unsigned)

food

varchar(20)

person_id列和food列组成了favorite_food数据表的主键,person_id列也是指向person数据表的外键。

这些设计足够了吗?

将favorite_foods列移出person数据表肯定是个好主意,但这样就算完成设计了吗?假如有人把“pasta”列为喜爱的食物,而另一个人把“spaghetti”列为喜爱的食物呢?[2]两者是同一种东西吗?为了防止此类问题发生,可以让人们从列表中选择他们喜爱的食物(而不是手动输入),这样的话,就需要创建包含food_id列和food_name列的food数据表,然后修改favorite_food数据表,使其包含指向food数据表的外键。尽管这种设计是完全规范化的,但如果你只是想保存用户所输入的食物名称,那么保持原有的数据表设计即可。

保存个人信息以及个人喜爱食物的两个数据表已经设计完毕,下一步需要生成SQL语句,以在数据库中创建数据表。创建person数据表的语句如下所示:

CREATE TABLE person
 (person_id SMALLINT UNSIGNED,
  fname VARCHAR(20),
  lname VARCHAR(20),
  eye_color CHAR(2),
  birth_date DATE,
  street VARCHAR(30),
  city VARCHAR(20),
  state VARCHAR(20),
  country VARCHAR(20),
  postal_code VARCHAR(20),
  CONSTRAINT pk_person PRIMARY KEY (person_id)
 );

在上述语句中,除了最后一项,其他部分的含义显而易见。在定义数据表时,需要告知数据库服务器将哪一列或哪几列作为数据表的主键。这可以通过在数据表上创建约束(constraint)来实现。数据表定义中可加入多种类型的约束。上述语句中的约束为主键约束,它创建在person_id列上并被命名为pk_person。

继续讨论约束,对person数据表而言,还有另一种约束也能派上用场。在表2-6中,我添加了第3列,用于展示某些列允许出现的值(比如eye_color列的'BR'和'BL')。可以添加检查约束(check constraint)来限制特定列的值。MySQL允许在定义列时关联检查约束,如下所示:

eye_color CHAR(2) CHECK (eye_color IN ('BR','BL','GR')),

检查约束在大多数数据库服务器中都能够如所期望的那样工作,然而,MySQL虽然允许定义检查约束,但并不强制使用。实际上,MySQL提供了另一种名为enum的字符数据类型,将检查约束并入了数据类型定义:

eye_color ENUM('BR','BL','GR'),

下面是person数据表的定义,其中包含数据类型为enum的eye_color列:

CREATE TABLE person
 (person_id SMALLINT UNSIGNED,
  fname VARCHAR(20),
  lname VARCHAR(20),
  eye_color ENUM('BR','BL','GR'),
  birth_date DATE,
  street VARCHAR(30),
  city VARCHAR(20),
  state VARCHAR(20),
  country VARCHAR(20),
  postal_code VARCHAR(20),
  CONSTRAINT pk_person PRIMARY KEY (person_id)
 );

在本章的后续部分,你会看到如果向列中添加了违反检查约束(在MySQL中则为枚举值)的数据会有什么后果。

现在就可以使用命令行工具mysql运行create table语句了。如下所示:

mysql> CREATE TABLE person
    -> (person_id SMALLINT UNSIGNED,
    ->  fname VARCHAR(20),
    ->  lname VARCHAR(20),
    ->  eye_color ENUM('BR','BL','GR'),
    ->  birth_date DATE,
    ->  street VARCHAR(30),
    ->  city VARCHAR(20),
    ->  state VARCHAR(20),
    ->  country VARCHAR(20),
    ->  postal_code VARCHAR(20),
    ->  CONSTRAINT pk_person PRIMARY KEY (person_id)
    -> );
Query OK, 0 rows affected (0.37 sec)

处理完create table语句之后,MySQL服务器返回消息"Query OK, 0 rows affected",告知用户该语句没有语法错误。

如果你想确认person数据表的确已经创建好了,可以使用describe命令(或者简写为desc)查看数据表定义:

mysql> desc person;
+-------------+----------------------+------+-----+---------+-------+
| Field       | Type                 | Null | Key | Default | Extra |
+-------------+----------------------+------+-----+---------+-------+
| person_id   | smallint(5) unsigned | NO   | PRI | NULL    |       |
| fname       | varchar(20)          | YES  |     | NULL    |       |
| lname       | varchar(20)          | YES  |     | NULL    |       |
| eye_color   | enum('BR','BL','GR') | YES  |     | NULL    |       |
| birth_date  | date                 | YES  |     | NULL    |       |
| street      | varchar(30)          | YES  |     | NULL    |       |
| city        | varchar(20)          | YES  |     | NULL    |       |
| state       | varchar(20)          | YES  |     | NULL    |       |
| country     | varchar(20)          | YES  |     | NULL    |       |
| postal_code | varchar(20)          | YES  |     | NULL    |       |
+-------------+----------------------+------+-----+---------+-------+
10 rows in set (0.00 sec)

执行describe命令后输出的第1列和第2列的含义显而易见,第3列显示该列是否可以在插入数据时被忽略。这一点暂时先不讨论(在“什么是null”部分中略作介绍),留到第4章详细讲解。第4列显示该列是否作为键(主键或外键),在本例中,person_id列被标记为主键。第5列显示该列如果在插入数据时被忽略,是否向其填充默认值。第6列(Extra)显示适用于某列的其他相关信息。

什么是null?

在某些情况下,不可能或不适合向数据表中的某列提供具体的值。例如,在添加新客户订单数据时,ship_date列还无法确定。此时,称该列为null(注意,我并没有说该列等于null),以指明缺失的值。null被用于各种无法提供值的情况,比如:

在创建数据表时,可以指定哪些列允许为null(默认),哪些列不允许为null(在类型定义后添加关键字not null)。

person数据表已经创建好了,接下来创建favorite_food数据表:

mysql> CREATE TABLE favorite_food
    -> (person_id SMALLINT UNSIGNED,
    -> food VARCHAR(20),
    -> CONSTRAINT pk_favorite_food PRIMARY KEY (person_id, food),
    -> CONSTRAINT fk_fav_food_person_id FOREIGN KEY (person_id)
    -> REFERENCES person (person_id)
    -> );
Query OK, 0 rows affected (0.10 sec)

看起来和创建person数据表的create table语句差不多,除了下列不同:

如果一开始创建数据表的时候忘了设置外键约束,随后可以通过alter table语句添加。

执行create table语句之后,使用describe查看数据表结构:

mysql> desc favorite_food;
+-----------+----------------------+------+-----+---------+-------+
| Field     | Type                 | Null | Key | Default | Extra |
+-----------+----------------------+------+-----+---------+-------+
| person_id | smallint(5) unsigned | NO   | PRI | NULL    |       |
| food      | varchar(20)          | NO   | PRI | NULL    |       |
+-----------+----------------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

数据表现在已经准备好了,下一步就是添加数据了。

随着数据表person和favorite_food的就绪,现在可以开始学习4个SQL数据语句了:insert、update、delete和select。

因为这两个数据表中还没有数据,所以第一个要介绍的SQL数据语句就是insert语句。该语句由三个主要部分组成:

并不需要向数据表中的每一列提供数据(除非所有列都被定义为not null)。在某些情况下,未包含在初始insert语句中的那些列可以随后通过update语句赋值。有时候,某列中的特定行可能始终得不到值(比如在发货前被取消的客户订单,会导致ship_date列不再适用)。

1.生成数值型主键数据

在向person数据表中插入数据之前,先讨论一下如何生成数值型主键还是有帮助的。除了随机选择数字,还有以下两种选择:

尽管第一种选择看起来是合理的,但在多用户环境下会出现问题,因为两个用户可能会在同一时间访问数据表并生成两个相同的主键值。对此,如今市面上所有的数据库服务器都提供了一种安全稳健的方法来生成数值型主键。在一些数据库服务器中,如Oracle Database,使用称为序列(sequence)的独立模式对象(schema object);在MySQL中,只需简单地为主键列启用自增(auto-increment)特性。通常来说,应该在创建数据表时就完成此项工作,不过这也给了我们一个机会来学习另一个SQL模式语句:alter table。该语句用于修改已有的数据表定义:

ALTER TABLE person MODIFY person_id SMALLINT UNSIGNED AUTO_INCREMENT;

上述语句重新定义了person数据表中的person_id列。如果查看数据表结构,你会看到出现在person_id的Extra列中的特性auto_increment:

mysql> DESC person;
+-----------+----------------------+------+-----+---------+-----------------+
| Field     | Type                 | Null | Key | Default | Extra           |
+-----------+----------------------+------+-----+---------+-----------------+
| person_id | smallint(5) unsigned | NO   | PRI | NULL    | auto_increment  |
| .         |                      |      |     |         |                 |
| .         |                      |      |     |         |                 |
| .         |                      |      |     |         |                 |

向person数据表插入数据时,只需向person_id列提供null值,MySQL会用下一个可用数值填充该列(默认情况下,MySQL从1开始自增)。

2.insert语句

一切就绪,可以开始添加数据了。下面的语句在person数据表中为William Turner创建了一行:

mysql> INSERT INTO person
    ->   (person_id, fname, lname, eye_color, birth_date)
    -> VALUES (null, 'William','Turner', 'BR', '1972-05-27');
Query OK, 1 row affected (0.22 sec)

反馈信息("Query OK, 1 row affected")表明语句的语法没有问题,已经成功地向数据库添加了一行(因为这是insert语句)。你可以使用select语句查看刚才新加入数据表的数据:

mysql> SELECT person_id, fname, lname, birth_date
    -> FROM person;
+-----------+---------+--------+------------+
| person_id | fname   | lname  | birth_date |
+-----------+---------+--------+------------+
|         1 | William | Turner | 1972-05-27 |
+-----------+---------+--------+------------+
1 row in set (0.06 sec)

如上所示,MySQL服务器为主键生成的值为1。因为现在person数据表中只有一行,所以此处省略了查询条件,只是简单地检索出数据表中的所有行。如果数据表中不止一行,可以添加where子句,检索出person_id列值为1的行:

mysql> SELECT person_id, fname, lname, birth_date
    -> FROM person
    -> WHERE person_id = 1;
+-----------+---------+--------+------------+
| person_id | fname   | lname  | birth_date |
+-----------+---------+--------+------------+
|         1 | William | Turner | 1972-05-27 |
+-----------+---------+--------+------------+
1 row in set (0.00 sec)

尽管上述查询指定的是特定的主键值,但是也可以使用数据表中的任意列来搜索行,下列查询搜索lname列为'Turner'的所有行:

mysql> SELECT person_id, fname, lname, birth_date
    -> FROM person
    -> WHERE lname = 'Turner';
+-----------+---------+--------+------------+
| person_id | fname   | lname  | birth_date |
+-----------+---------+--------+------------+
|         1 | William | Turner | 1972-05-27 |
+-----------+---------+--------+------------+
1 row in set (0.00 sec)

在继续讨论之前,之前的insert语句中有几处地方值得一提。

William Turner还提供了关于他喜爱的3种食物的信息,因此还需要3个insert语句来保存他的食物偏好:

mysql> INSERT INTO favorite_food (person_id, food)
    -> VALUES (1, 'pizza');
Query OK, 1 row affected (0.01 sec)
mysql> INSERT INTO favorite_food (person_id, food)
    -> VALUES (1, 'cookies');
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO favorite_food (person_id, food)
    -> VALUES (1, 'nachos');
Query OK, 1 row affected (0.01 sec)

下面检索William喜爱的食物,并使用order by子句将食物按字母顺序排序:

mysql> SELECT food
    -> FROM favorite_food
    -> WHERE person_id = 1
    -> ORDER BY food;
+---------+
| food    |
+---------+
| cookies |
| nachos  |
| pizza   |
+---------+
3 rows in set (0.02 sec)

order by子句告知服务器如何对查询返回的数据进行排序。如果没有该子句,对于检索得到的数据,无法保证特定的顺序。

为了让William不感到孤单,你可以执行另一个insert语句,向person数据表中添加Susan Smith:

mysql> INSERT INTO person
    -> (person_id, fname, lname, eye_color, birth_date,
    -> street, city, state, country, postal_code)
    -> VALUES (null, 'Susan','Smith', 'BL', '1975-11-02',
    -> '23 Maple St.', 'Arlington', 'VA', 'USA', '20220');
Query OK, 1 row affected (0.01 sec)

Susan友善地提供了自己的地址,所以相较于William,我们多插入了5列数据。如果再次查询数据表,会发现Susan对应行的主键值为2:

mysql> SELECT person_id, fname, lname, birth_date
    -> FROM person;
+-----------+---------+--------+------------+
| person_id | fname   | lname  | birth_date |
+-----------+---------+--------+------------+
|         1 | William | Turner | 1972-05-27 |
|         2 | Susan   | Smith  | 1975-11-02 |
+-----------+---------+--------+------------+
2 rows in set (0.00 sec)

可以获取XML格式的数据吗?

如果你要和XML数据打交道,会很高兴地看到大部分数据库服务器都已经提供了简便的方法来根据查询生成XML格式的输出。例如,对于MySQL,可以在调用mysql工具时使用--xml选项,所有查询的输出都会自动转换成XML格式。下面展示了如何获取XML文档格式的食物偏好数据:

C:\database> mysql -u lrngsql -p --xml bank
Enter password: xxxxxx
Welcome to the MySQL Monitor...

Mysql> SELECT * FROM favorite_food;
<?xml version="1.0"?>

<resultset statement="select * from favorite_food"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <row>
        <field name="person_id">1</field>
        <field name="food">cookies</field>
  </row>
  <row>
        <field name="person_id">1</field>
        <field name="food">nachos</field>
  </row>
  <row>
        <field name="person_id">1</field>
        <field name="food">pizza</field>
  </row>
</resultset>
3 rows in set (0.00 sec)

对于SQL Server,则无须配置命令行工具,只需要在查询末尾添加for xml子句:

SELECT * FROM favorite_food FOR XML AUTO, ELEMENTS

最初向数据表中添加William Turner的相关数据时,未在insert语句中加入其地址信息。下列语句展示了随后如何通过update语句填充相关的列:

mysql> UPDATE person
    -> SET street = '1225 Tremont St.',
    ->   city = 'Boston',
    ->   state = 'MA',
    ->   country = 'USA',
    ->   postal_code = '02138'
    -> WHERE person_id = 1;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1  Changed: 1  Warnings: 0

服务器返回两行响应消息:"Rows matched: 1"提示数据表中有一行符合where子句中给出的条件,"Changed: 1"提示数据表中有一行被修改。因为where子句指定了William所在行的主键值,所以结果和预期的一模一样。

根据where子句中给出的条件,也可以使用单个语句修改多行。考虑下列where子句的执行结果:

WHERE person_id < 10

因为William和Susan的person_id值都小于10,所以两者对应的行都会被修改。如果省略where子句,update语句会修改数据表中的每一行。

William和Susan看起来实在合不来,其中一个人只能离开。因为William先来,所以只能删除Susan了:

mysql> DELETE FROM person
    -> WHERE person_id = 2;
Query OK, 1 row affected (0.01 sec)

同样,主键用于期望选定的行,因此只从数据表中删除一行。和update语句一样,可以根据where子句中给出的条件删除多行,如果忽略where子句,则会删除所有行。

到目前为止,本章所有的SQL数据语句都符合语法并遵循规则。然而,根据数据表person和favorite_food的定义,在插入或修改数据时,容易出现不少错误。本节将展示可能遇到的一些常见的错误以及MySQL服务器如何响应。

由于数据表定义中包含主键约束,因此MySQL会确保重复的主键值不会被插入数据表中。下列语句忽略了person_id列的自增特性,将person数据表中另一行的person_id值设为1:

mysql> INSERT INTO person
    -> (person_id, fname, lname, eye_color, birth_date)
    -> VALUES (1, 'Charles','Fulton', 'GR', '1968-01-15');
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'

只要保证person_id列的值不同,完全可以创建两个具有相同的姓名、地址、出生日期等列的数据行(至少在当前的模式对象中如此)。

favorite_food数据表定义包括在person_id列上创建的外键约束。该约束确保favorite_food数据表中person_id列的所有值都来自person数据表。下面展示了如果在创建行时违反这一约束的结果:

mysql> INSERT INTO favorite_food (person_id, food)
    -> VALUES (999, 'lasagna');
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraintfails ('sakila'.'favorite_food', CONSTRAINT 'fk_fav_food_person_id' FOREIGNKEY('person_id') REFERENCES 'person' ('person_id'))

在这个示例中,由于favorite_food数据表的部分数据依赖person数据表,因此可以将favorite_food数据表视为子表,将person数据表视为父表。如果需要向这两个表中插入数据,必须在favorite_food中插入数据前,先在person中创建一行。

仅当使用InnoDB存储引擎创建数据表时,外键约束才是强制的。我们会在第12章讨论MySQL的存储引擎。

person数据表中的eye_color列将取值限制为'BR'(棕色)、'BL'(蓝色)、'GR'(绿色)。如果你试图错误地对该列设置其他值,会得到如下响应:

mysql> UPDATE person
    -> SET eye_color = 'ZZ'
    -> WHERE person_id = 1;
ERROR 1265 (01000): Data truncated for column 'eye_color' at row 1

该错误消息有点不好理解,它可以让你大概了解到,服务器对所提供的eye_color列的值不满意。

如果用于填充日期类型列的字符串不符合要求的格式,会产生错误。下面的示例中使用的日期格式不符合默认的日期格式(YYYY-MM-DD):

mysql> UPDATE person
    -> SET birth_date = 'DEC-21-1980'
    -> WHERE person_id = 1;
ERROR 1292 (22007): Incorrect date value: 'DEC-21-1980' for column   
'birth_date' at row 1

一般而言,最好是明确指定格式化字符串,而不是依赖默认格式。下列语句使用str_to_date函数指定了格式化字符串:

mysql> UPDATE person
    -> SET birth_date = str_to_date('DEC-21-1980' , '%b-%d-%Y')
    -> WHERE person_id = 1;
Query OK, 1 row affected (0.12 sec)
Rows matched: 1  Changed: 1  Warnings: 0

数据库服务器和William皆大欢喜。(我们让William年轻了8岁,省下了昂贵的整容手术费!)

本章之前讨论各种各种时间数据类型时,展示过如“YYYY-MM-DD”这样的日期格式化字符串。尽管很多数据库服务器都采用这种格式化风格,但MySQL使用“%Y”来指定4位数字的年份。下面是一些在MySQL中将字符串转换为datetime类型时可能会用到的格式化字符:

%a 星期几的简写,比如Sun、Mon、...

%b 月份名称的简写,比如Jan、Feb、...

%c 月份的数字形式(0…12)

%d 月份中的天数(00…31)

%f 微秒数(000000…999999)

%H 24小时制中的小时(00…23)

%h 12小时制中的小时(01…12)

%i 小时中的分钟数(00…59)

%j 一年中的天数(001…366)

%M 月份的全称(January…December)

%m 月份的数值形式

%p AM或PM

%s 秒数(00…59)

%W 星期几的全称(Sunday…Saturday)

%w 一星期中的天数(0=周日;6=周六)

%Y 4位数字表示的年份

在本书的剩余部分中,大多数示例都要用到样本数据库Sakila(可从异步社区下载,具体下载方式可见2.1节),它是由一位就职于MySQL的热心人士开发的[3]。这个数据库的模型是一家DVD租借连锁店,虽然有点过时,但只要发挥一点想象力,就可以将其重新包装成视频流公司。其中部分数据表包括customer、film、actor、payment、rental和category。当按照本章开头给出的步骤加载MySQL服务器并生成样本数据时,整个模式以及相关数据应该就已经创建好了。数据表和列以及其相互关系的图表,参见附录A。

表2-9展示了Sakila模式中用到的一些数据表及其简要的定义。

表2-9 Sakila模式定义

数据表名称

定义

film

已发行且可租借的电影

actor

演员

customer

观看电影的客户

category

电影类别

payment

租借付款信息

language

电影语言

film_actor

电影演员

inventory

可租借的电影

你可以用这些数据表进行各种试验,包括添加自己的数据表来扩展业务功能。如果你不希望样本数据有变,可以随时删除数据库,并从下载的文件中重新创建。如果你使用的是临时会话,其间所做的任何修改都会在会话关闭时丢失,所以你可能想保留一个变更脚本,以便可以重现所做的任何修改。

如果想查看数据库中可用的数据表,可以使用show tables命令:

mysql> show tables;
+----------------------------+
| Tables_in_sakila           |
+----------------------------+
| actor                      |
| actor_info                 |
| address                    |
| category                   |
| city                       |
| country                    |
| customer                   |
| customer_list              |
| film                       |
| film_actor                 |
| film_category              |
| film_list                  |
| film_text                  |
| inventory                  |
| language                   |
| nicer_but_slower_film_list |
| payment                    |
| rental                     |
| sales_by_film_category     |
| sales_by_store             |
| staff                      |
| staff_list                 |
| store                      |
+----------------------------+
23 rows in set (0.02 sec)

除了Sakila模式中的23个数据表,可能还包括本章中创建的两个数据表:person和favorite_food。这两个表在后续章节中不会再使用,可以放心地使用下列语句将其删除:

mysql> DROP TABLE favorite_food;
Query OK, 0 rows affected (0.56 sec)
mysql> DROP TABLE person;
Query OK, 0 rows affected (0.05 sec)

如果想查看数据表中的列,可以使用describe语句。下面是customer数据表的describe语句输出:

mysql> desc customer;
+-------------+-------------+------+-----+------------+---------------------+
| Field       | Type        | Null | Key | Default    | Extra               |
+-------------+-------------+------+-----+------------+---------------------+
| customer_id | smallint(5) | NO   | PRI | NULL       | auto_increment      |
                 unsigned
| store_id    | tinyint(3)  | NO   | MUL | NULL       |                     |
                 unsigned
| first_name  | varchar(45) | NO   |     | NULL       |                     |
| last_name   | varchar(45) | NO   | MUL | NULL       |                     |
| email       | varchar(50) | YES  |     | NULL       |                     |
| address_id  | smallint(5) | NO   | MUL | NULL       |                     |
                 unsigned
| active      | tinyint(1)  | NO   |     | 1          |                     |
| create_date | datetime    | NO   |     | NULL       |                     |
| last_update | timestamp   | YES  |     | CURRENT_   | DEFAULT_GENERATED on
                                             TIMESTAMP    update CURRENT_
                                                          TIMESTAMP         |
+-------------+-------------+------+-----+------------+---------------------+

你对样本数据库越熟悉,就越能更好地理解后续章节中的示例以及概念。

[1] “script”一词也有“电影剧本”之意。

[2] pasta和spaghetti均指“意大利面”。  ——译者注

[3] Sakila样本数据库最初由MySQL AB文档团队的前成员Mike Hillyer开发。——译者注


相关图书

SQL实践教程(第10版)
SQL实践教程(第10版)
SQL初学教程(第3版)
SQL初学教程(第3版)
SQL入门经典(第6版)
SQL入门经典(第6版)
SQL优化核心思想
SQL优化核心思想
SQL初学者指南(第2版)
SQL初学者指南(第2版)
Oracle PL/SQL必知必会
Oracle PL/SQL必知必会

相关文章

相关课程