Skip to main content

本·福达 (Ben Forta)

· 45 min read

Adobe公司教育计划高级总监,世界知名的技术作家,在计算机产品开发、支持、培训和营销等方面拥有几十年的丰富经验。多年来,他撰写了SQL、正则表达式、JSP、WAP和Windows开发等方面的40余部技术图书,其中不少是世界畅销书,已被翻译为多达15种语言并在全世界出版发行。

引言

书里没有挑战题的答案,但是别担心,你可以在配套的网站找到答案:http://forta.com/books/0135182794。

本书中的所有数据库示例(或者创建数据库示例的SQL脚本例子)对于这些DBMS都是适用的,它们可以在本书的网页http: //forta.com/books/0135182794上获得。

第1课 了解SQL

1.1 数据库基础

1.1.1 数据库

数据库这个术语的用法很多,但就本书而言(从SQL的角度来看),数据库是以某种有组织的方式存储的数据集合。最简单的办法是将数据库想象为一个文件柜。文件柜只是一个存放数据的物理位置,它不管数据是什么,也不管数据是如何组织的。

数据库(database)

保存有组织的数据的容器(通常是一个文件或一组文件)。

注意:误用导致混淆

人们通常用数据库这个术语来代表他们使用的数据库软件,这是不正确的,也因此产生了许多混淆。确切地说,数据库软件应称为数据库管理系统(DBMS)。数据库是通过DBMS创建和操纵的容器,而具体它究竟是什么,形式如何,各种数据库都不一样。

1.1.2 表

你往文件柜里放资料时,并不是随便将它们扔进某个抽屉就完事了的,而是在文件柜中创建文件,然后将相关的资料放入特定的文件中。在数据库领域中,这种文件称为表。表是一种结构化的文件,可用来存储某种特定类型的数据。表可以保存顾客清单、产品目录,或者其他信息清单。

表(table)

某种特定类型数据的结构化清单。

这里的关键一点在于,存储在表中的数据是同一种类型的数据或清单。决不应该将顾客的清单与订单的清单存储在同一个数据库表中,否则以后的检索和访问会很困难。应该创建两个表,每个清单一个表。数据库中的每个表都有一个名字来标识自己。这个名字是唯一的,即数据库中没有其他表具有相同的名字。

说明:表名

使表名成为唯一的,实际上是数据库名和表名等的组合。有的数据库还使用数据库拥有者的名字作为唯一名的一部分。也就是说,虽然在一个数据库中不能两次使用相同的表名,但在不同的数据库中完全可以使用相同的表名。

表具有一些特性,这些特性定义了数据在表中如何存储,包括存储什么样的数据,数据如何分解,各部分信息如何命名等信息。描述表的这组信息就是所谓的模式(schema),模式可以用来描述数据库中特定的表,也可以用来描述整个数据库(和其中表的关系)。

模式

关于数据库和表的布局及特性的信息。

1.1.3 列和数据类型

表由列组成。列存储表中某部分的信息。

列(column)

表中的一个字段。所有表都是由一个或多个列组成的。

理解列的最好办法是将数据库表想象为一个网格,就像个电子表格那样。网格中每一列存储着某种特定的信息。例如,在顾客表中,一列存储顾客编号,另一列存储顾客姓名,而地址、城市、州以及邮政编码全都存储在各自的列中。

提示:数据分解

正确地将数据分解为多个列极为重要。例如,城市、州、邮政编码应该总是彼此独立的列。通过分解这些数据,才有可能利用特定的列对数据进行分类和过滤(如找出特定州或特定城市的所有顾客)。如果城市和州组合在一个列中,则按州进行分类或过滤就会很困难。你可以根据自己的具体需求来决定把数据分解到何种程度。例如,一般可以把门牌号和街道名一起存储在地址里。这没有问题,除非你哪天想用街道名来排序,这时,最好将门牌号和街道名分开。

数据库中每个列都有相应的数据类型。数据类型(datatype)定义了列可以存储哪些数据种类。例如,如果列中存储的是数字(或许是订单中的物品数),则相应的数据类型应该为数值类型。如果列中存储的是日期、文本、注释、金额等,则应该规定好恰当的数据类型。

数据类型

允许什么类型的数据。每个表列都有相应的数据类型,它限制(或允许)该列中存储的数据。

数据类型限定了可存储在列中的数据种类(例如,防止在数值字段中录入字符值)。数据类型还帮助正确地分类数据,并在优化磁盘使用方面起重要的作用。因此,在创建表时必须特别关注所用的数据类型。

注意:数据类型兼容

数据类型及其名称是SQL不兼容的一个主要原因。虽然大多数基本数据类型得到了一致的支持,但许多高级的数据类型却没有。更糟的是,偶然会有相同的数据类型在不同的DBMS中具有不同的名称。对此用户毫无办法,重要的是在创建表结构时要记住这些差异。

1.1.4 行

表中的数据是按行存储的,所保存的每个记录存储在自己的行内。如果将表想象为网格,网格中垂直的列为表列,水平行为表行。例如,顾客表可以每行存储一个顾客。表中的行编号为记录的编号。

行(row)

表中的一个记录。说明:是记录还是行?你可能听到用户在提到行时称其为数据库记录(record)。这两个术语多半是可以互通的,但从技术上说,行才是正确的术语。

1.1.5 主键

表中每一行都应该有一列(或几列)可以唯一标识自己。顾客表可以使用顾客编号,而订单表可以使用订单ID。雇员表可以使用雇员ID。书目表则可以使用国际标准书号ISBN。

主键(primary key)

一列(或几列),其值能够唯一标识表中每一行。

唯一标识表中每行的这个列(或这几列)称为主键。主键用来表示一个特定的行。没有主键,更新或删除表中特定行就极为困难,因为你不能保证操作只涉及相关的行,没有伤及无辜。

提示:应该总是定义主键

虽然并不总是需要主键,但多数数据库设计者都会保证他们创建的每个表具有一个主键,以便于以后的数据操作和管理。

表中的任何列都可以作为主键,只要它满足以下条件:

  • ❑ 任意两行都不具有相同的主键值;
  • ❑ 每一行都必须具有一个主键值(主键列不允许空值NULL);
  • ❑ 主键列中的值不允许修改或更新;
  • ❑ 主键值不能重用(如果某行从表中删除,它的主键不能赋给以后的新行)。

主键通常定义在表的一列上,但并不是必须这么做,也可以一起使用多个列作为主键。在使用多列作为主键时,上述条件必须应用到所有列,所有列值的组合必须是唯一的(但其中单个列的值可以不唯一)。

还有一种非常重要的键,称为外键,我们将在第12课中介绍。

1.2 什么是SQL

SQL(发音为字母S-Q-L或sequel)是Structured Query Language(结构化查询语言)的缩写。SQL是一种专门用来与数据库沟通的语言。与其他语言(如英语或Java、C、PHP这样的编程语言)不一样,SQL中只有很少的词,这是有意而为的。设计SQL的目的是很好地完成一项任务——提供一种从数据库中读写数据的简单有效的方法。

SQL有哪些优点呢? ❑ SQL不是某个特定数据库厂商专有的语言。绝大多数重要的DBMS支持SQL,所以学习此语言使你几乎能与所有数据库打交道。 ❑ SQL简单易学。它的语句全都是由有很强描述性的英语单词组成,而且这些单词的数目不多。 ❑ SQL虽然看上去很简单,但实际上是一种强有力的语言,灵活使用其语言元素,可以进行非常复杂和高级的数据库操作。

下面我们将开始真正学习SQL。

说明:SQL的扩展

许多DBMS厂商通过增加语句或指令,对SQL进行了扩展。这种扩展的目的是提供执行特定操作的额外功能或简化方法。虽然这种扩展很有用,但一般都是针对个别DBMS的,很少有两个厂商同时支持这种扩展。标准SQL由ANSI标准委员会管理,从而称为ANSI SQL。所有主要的DBMS,即使有自己的扩展,也都支持ANSI SQL。各个实现有自己的名称,如Oracle的PL/SQL、微软SQL Server用的Transact-SQL等。本书讲授的SQL主要是ANSI SQL。在使用某种DBMS特定的SQL时,会特别说明。

1.3 动手实践

第2课 检索数据

这一课介绍如何使用SELECT语句从表中检索一个或多个数据列。

2.1 SELECT语句

正如第1课所述,SQL语句是由简单的英语单词构成的。这些单词称为关键字,每个SQL语句都是由一个或多个关键字构成的。最经常使用的SQL语句大概就是SELECT语句了。它的用途是从一个或多个表中检索信息。

关键字(keyword)

作为SQL组成部分的保留字。关键字不能用作表或列的名字。附录D列出了某些经常使用的保留字。

为了使用SELECT检索表数据,必须至少给出两条信息——想选择什么,以及从什么地方选择。

2.2 检索单个列 我们将从简单的SQL SELECT语句讲起,此语句如下所示:

SELECT prod_name
FROM product;

分析▼ 上述语句利用SELECT语句从Products表中检索一个名为prod_name的列。所需的列名写在SELECT关键字之后,FROM关键字指出从哪个表中检索数据。

提示:结束SQL语句

多条SQL语句必须以分号(;)分隔。 多数DBMS不需要在单条SQL语句后加分号,但也有DBMS可能必须在单条SQL语句后加上分号。当然,如果愿意可以总是加上分号。事实上,即使不一定需要,加上分号也肯定没有坏处。 提示:SQL语句和大小写请注意,

SQL语句不区分大小写,因此SELECT与select是相同的。同样,写成Select也没有关系。许多SQL开发人员喜欢对SQL关键字使用大写,而对列名和表名使用小写,这样做代码更易于阅读和调试。不过,一定要认识到虽然SQL是不区分大小写的,但是表名、列名和值可能有所不同(这有赖于具体的DBMS及其如何配置)。

提示:使用空格在处理SQL语句时,其中所有空格都被忽略。

SQL语句可以写成长长的一行,也可以分写在多行。下面这种写法的作用是一样的。

SELECT

prod_name

FROM product;

多数SQL开发人员认为,将SQL语句分成多行更容易阅读和调试。

2.3 检索多个列

要想从一个表中检索多个列,仍然使用相同的SELECT语句。唯一的不同是必须在SELECT关键字后给出多个列名,列名之间必须以逗号分隔。

提示:当心逗号

在选择多个列时,一定要在列名之间加上逗号,但最后一个列名后不加。如果在最后一个列名后加了逗号,将出现错误。

下面的SELECT语句从Products表中选择3列。

SELECT  prod_id,prod_name,prod_price FROM  product;

说明:数据表示

SQL语句一般返回原始的、无格式的数据,不同的DBMS和客户端显示数据的方式略有不同(如对齐格式不同、小数位数不同)。 数据的格式化是表示问题,而不是检索问题。因此,如何表示一般会在显示该数据的应用程序中规定。 通常很少直接使用实际检索出的数据(没有应用程序提供的格式)。

2.4 检索所有列

除了指定所需的列外(如上所述,一列或多列), SELECT语句还可以检索所有的列而不必逐个列出它们。 在实际列名的位置使用星号(*)通配符可以做到这点,如下所示。

SELECT  * FROM  product ;

分析▼ 如果给定一个通配符(*),则返回表中所有列。 列的顺序一般是表中出现的物理顺序,但并不总是如此。 不过,SQL数据很少直接显示(通常,数据返回给应用程序,根据需要进行格式化,再表示出来)。 因此,这不应该造成什么问题。

注意:使用通配符

一般而言,除非你确实需要表中的每一列,否则最好别使用*通配符。 虽然使用通配符能让你自己省事,不用明确列出所需列,但检索不需要的列通常会降低检索速度和应用程序的性能。

提示:检索未知列

使用通配符有一个大优点。由于不明确指定列名(因为星号检索每一列),所以能检索出名字未知的列。

2.5 检索不同的值

如前所述,SELECT语句返回所有匹配的行。但是,如果你不希望每个值每次都出现,该怎么办呢?例如,你想检索Products表中所有产品供应商的ID:

办法就是使用DISTINCT关键字,顾名思义,它指示数据库只返回不同的值。

SELECT DISTINCT  node_root FROM kn_node ;

分析▼ SELECT DISTINCT vend_id告诉DBMS只返回不同(具有唯一性)的vend_id行,所以正如下面的输出,只有3行。如果使用DISTINCT关键字,它必须直接放在列名的前面。

注意:不能部分使用DISTINCT

DISTINCT关键字作用于所有的列,不仅仅是跟在其后的那一列。 例如,你指定SELECT DISTINCT vend_id, prod_price,则9行里的6行都会被检索出来,因为指定的两列组合起来有6个不同的结果。

2.6 限制结果

SELECT语句返回指定表中所有匹配的行,很可能是每一行。 如果你只想返回第一行或者一定数量的行,该怎么办呢?这是可行的,然而遗憾的是,各种数据库中的这一SQL实现并不相同。

  • 在SQL Server中使用SELECT时,可以用TOP关键字来限制最多返回多少行,如下所示:
SELECT TOP 5  node_root FROM kn_node ;

分析▼上面代码使用SELECT TOP 5语句,只检索前5行数据。

  • 如果你使用的是DB2,就得使用下面这样的DB2特有的SQL语句:
SELECT  prod_name FROM  product FETCH FIRST 5 ROWS ONLY

分析▼FETCH FIRST 5 ROWS ONLY就会按字面的意思去做的(只取前5行)。

  • 如果你使用Oracle,需要基于ROWNUM(行计数器)来计算行,像这样:
SELECT  prod_name FROM  product ROWNum <= 5 ;
  • 如果你使用MySQL、MariaDB、PostgreSQL或者SQLite,需要使用LIMIT子句,像这样:
SELECT prod_name
FROM product
limit 5;

分析▼ 上述代码使用SELECT语句来检索单独的一列数据。LIMIT 5指示MySQL等DBMS返回不超过5行的数据。 这个语句的输出参见下面的代码。为了得到后面的5行数据, 需要指定从哪儿开始以及检索的行数,像这样:

SELECT prod_name
FROM product
LIMIT 5 OFFSET 5;

分析▼ LIMIT 5 OFFSET 5指示MySQL等DBMS返回从第5行起的5行数据。第一个数字是检索的行数,第二个数字是指从哪儿开始。这个语句的输出是:

所以,LIMIT指定返回的行数。LIMIT带的OFFSET指定从哪儿开始。在我们的例子中,Products表中只有9种产品, 所以LIMIT 5 OFFSET 5只返回了4行数据(因为没有第5行)。

注意:第0行第一个被检索的行是第0行,而不是第1行。

因此,LIMIT 1 OFFSET 1会检索第2行,而不是第1行。

提示:MySQL、MariaDB和SQLite捷径

MySQL、MariaDB和SQLite可以把LIMIT 4 OFFSET 3语句简化为LIMIT 3,4。使用这个语法,逗号之前的值对应OFFSET,逗号之后的值对应LIMIT(反着的,要小心)。说明:并非所有的SQL实现都一样我加入这一节只有一个原因,就是要说明,SQL虽然通常都有相当一致的实现,但你不能想当然地认为它总是这样。非常基本的语句往往是相通的,但较复杂的语句就不同了。当你针对某个问题寻找SQL解决方案时,一定要记住这一点。

2.7 使用注释

可以看到,SQL语句是由DBMS处理的指令。 如果你希望包括不进行处理和执行的文本,该怎么办呢? 为什么你想要这么做呢? 原因有以下几点。 ❑ 我们这里使用的SQL语句都很短,也很简单。 然而,随着SQL语句变长,复杂性增加,你就会想添加一些描述性的注释,这便于你自己今后参考,或者供项目后续参与人员参考。 这些注释需要嵌入在SQL脚本中,但显然不能进行实际的DBMS处理。(相关示例可以参见附录B中使用的create.sql和populate.sql。) ❑ 这同样适用于SQL文件开始处的内容,它可能包含程序描述以及一些说明,甚至是程序员的联系方式。 (相关示例也可参见附录B中的那些.sql文件。) ❑ 注释的另一个重要应用是暂停要执行的SQL代码。 如果你碰到一个长SQL语句,而只想测试它的一部分,那么应该注释掉一些代码,以便DBMS略去这些注释。 很多DBMS都支持各种形式的注释语法。我们先来看行内注释:

SELECT   node_root -- 注释
FROM kn_node ;

分析▼注释使用--(两个连字符)嵌在行内。——之后的文本就是注释,例如,这用来描述CREATE TABLE语句中的列就很不错。下面是另一种形式的行内注释(但这种形式有些DBMS不支持)。

# 注释
SELECT node_root
FROM kn_node ;

在一行的开始处使用#,这一整行都将作为注释。你在本书提供的脚本create.sql和populate.sql中可以看到这种形式的注释。 你也可以进行多行注释,注释可以在脚本的任何位置停止和开始。

SELECT   node_root /* 多行注释
多行注释
*/
FROM kn_node ;

分析▼ 注释从/*开始,到*/结束,/*和*/之间的任何内容都是注释。这种方式常用于把代码注释掉,就如这个例子演示的,这里定义了两个SELECT语句,但是第一个不会执行,因为它已经被注释掉了。

2.8 小结

这一课学习了如何使用SQL的SELECT语句来检索单个表列、多个表列以及所有表列。你也学习了如何返回不同的值,如何注释代码。同时不好的消息是,复杂的SQL语句往往不够通用。下一课将讲授如何对检索出来的数据进行排序。

2.9 挑战题

1.编写SQL语句,从Customers表中检索所有的ID(cust_id)。 2.OrderItems表包含了所有已订购的产品(有些已被订购多次)。编写SQL语句,检索并列出已订购产品(prod_id)的清单(不用列每个订单,只列出不同产品的清单)。提示:最终应该显示7行。 3.编写SQL语句,检索Customers表中所有的列,再编写另外的SELECT语句,仅检索顾客的ID。使用注释,注释掉一条SELECT语句,以便运行另一条SELECT语句。(当然,要测试这两个语句。)

提示:答案在哪里?本书挑战题的答案在http://forta.com/books/0135182794,或至图灵社区本书主页www.ituring.com.cn/book/2649下载。

第3课 排序检索数据

这一课讲授如何使用SELECT语句的ORDER BY子句,根据需要排序检索出的数据。

3.1 排序数据

正如上一课所述,下面的SQL语句返回某个数据库表的单个列。但请看其输出,并没有特定的顺序。

SELECT   node_root
FROM kn_node ;

其实,检索出的数据并不是随机显示的。 如果不排序,数据一般将以它在表中出现的顺序显示,这有可能是数据最初添加到表中的顺序。 但是,如果数据随后进行过更新或删除,那么这个顺序将会受到DBMS重用回收存储空间的方式的影响。 因此,如果不明确控制的话,则最终的结果不能(也不应该)依赖该排序顺序。 关系数据库设计理论认为,如果不明确规定排序顺序,则不应该假定检索出的数据的顺序有任何意义。

子句(clause)

SQL语句由子句构成,有些子句是必需的,有些则是可选的。 一个子句通常由一个关键字加上所提供的数据组成。 子句的例子有我们在前一课看到的SELECT语句的FROM子句。

为了明确地排序用SELECT语句检索出的数据,可使用ORDER BY子句。 ORDER BY子句取一个或多个列的名字,据此对输出进行排序。请看下面的例子:

SELECT   node_root
FROM kn_node ORDER BY node_root;

除了指示DBMS软件对prod_name列以字母顺序排序数据的ORDER BY子句外,这条语句与前面的语句相同。

注意:ORDER BY子句的位置

在指定一条ORDER BY子句时,应该保证它是SELECT语句中最后一条子句。 如果它不是最后的子句,将会出错。

提示:通过非选择列进行排序

通常,ORDER BY子句中使用的列将是为显示而选择的列。 但是,实际上并不一定要这样,用非检索的列排序数据是完全合法的。

3.2 按多个列排序

经常需要按不止一个列进行数据排序。 例如,如果要显示雇员名单,可能希望按姓和名排序(首先按姓排序,然后在每个姓中再按名排序)。 如果多个雇员有相同的姓,这样做很有用。 要按多个列排序,只须指定这些列名,列名之间用逗号分开即可(就像选择多个列时那样)。 下面的代码检索3个列,并按其中两个列对结果进行排序——首先按价格,然后按名称排序。

SELECT   node_root,kn_id
FROM kn_node
ORDER BY node_root,kn_id;

重要的是理解在按多个列排序时,排序的顺序完全按规定进行。换句话说,对于上述例子中的输出,仅在多个行具有相同的prod_price值时才对产品按prod_name进行排序。 如果prod_price列中所有的值都是唯一的,则不会按prod_name排序。

3.3 按列位置排序

除了能用列名指出排序顺序外,ORDER BY还支持按相对列位置进行排序。为理解这一内容,我们来看个例子:

SELECT   name,node_root,kn_id
FROM kn_node
ORDER BY 2,3;

可以看到,这里的输出与上面的查询相同,不同之处在于ORDER BY子句。SELECT清单中指定的是选择列的相对位置而不是列名。ORDER BY 2表示按SELECT清单中的第二个列prod_price进行排序。ORDER BY 2, 3表示先按prod_price,再按prod_name进行排序。这一技术的主要好处在于不用重新输入列名。但它也有缺点。首先,不明确给出列名有可能造成错用列名排序。其次,在对SELECT清单进行更改时容易错误地对数据进行排序(忘记对ORDER BY子句做相应的改动)。 最后,如果进行排序的列不在SELECT清单中,显然不能使用这项技术。

提示:按非选择列排序

显然,当根据不出现在SELECT清单中的列进行排序时,不能采用这项技术。 但是,如果有必要,可以混合使用实际列名和相对列位置。

SELECT   name,node_root,kn_id,shortname
FROM kn_node
ORDER BY 2,shortname;

3.4 指定排序方向

数据排序不限于升序排序(从A到Z),这只是默认的排序顺序。 还可以使用ORDER BY子句进行降序(从Z到A)排序。 为了进行降序排序,必须指定DESC关键字。

SELECT   name,node_root,kn_id,shortname
FROM kn_node
ORDER BY kn_id DESC
;

如果打算用多个列排序,该怎么办?下面的例子以降序排序产品(最贵的在最前面),再加上产品名:

SELECT   name,node_root,kn_id,shortname
FROM kn_node
ORDER BY node_root,kn_id DESC
;

分析▼ DESC关键字只应用到直接位于其前面的列名。 在上例中,只对prod_price列指定DESC,对prod_name列不指定。 因此,prod_price列以降序排序,而prod_name列(在每个价格内)仍然按标准的升序排序。

警告:在多个列上降序排序如果想在多个列上进行降序排序,必须对每一列指定DESC关键字。

请注意,DESC是DESCENDING的缩写,这两个关键字都可以使用。 与DESC相对的是ASC(或ASCENDING),在升序排序时可以指定它。 但实际上,ASC没有多大用处,因为升序是默认的(如果既不指定ASC也不指定DESC,则假定为ASC)。

提示:区分大小写和排序顺序在对文本性数据进行排序时,A与a相同吗?a位于B之前,还是Z之后? 这些问题不是理论问题,其答案取决于数据库的设置方式。 在字典(dictionary)排序顺序中,A被视为与a相同,这是大多数数据库管理系统的默认做法。 但是,许多DBMS允许数据库管理员在需要时改变这种行为(如果你的数据库包含大量外语字符,可能必须这样做)。 这里的关键问题是,如果确实需要改变这种排序顺序,用简单的ORDER BY子句可能做不到。你必须请求数据库管理员的帮助。

3.5 小结

这一课学习了如何用SELECT语句的ORDER BY子句对检索出的数据进行排序。 这个子句必须是SELECT语句中的最后一条子句。 根据需要,可以利用它在一个或多个列上对数据进行排序。

3.6 挑战题 1.编写SQL语句,从Customers中检索所有的顾客名称(cust_names),并按从Z到A的顺序显示结果。 2.编写SQL语句,从Orders表中检索顾客ID(cust_id)和订单号(order_num),并先按顾客ID对结果进行排序,再按订单日期倒序排列。 3.显然,我们的虚拟商店更喜欢出售比较贵的物品,而且这类物品有很多。编写SQL语句,显示OrderItems表中的数量和价格(item_price),并按数量由多到少、价格由高到低排序。 4.下面的SQL语句有问题吗?(尝试在不运行的情况下指出。)select vend_name FROM vendors order verd_name desc;

第4课 过滤数据

这一课将讲授如何使用SELECT语句的WHERE子句指定搜索条件。

4.1 使用WHERE子句

数据库表一般包含大量的数据,很少需要检索表中的所有行。 通常只会根据特定操作或报告的需要提取表数据的子集。 只检索所需数据需要指定搜索条件(search criteria),搜索条件也称为过滤条件(filter condition)。 在SELECT语句中,数据根据WHERE子句中指定的搜索条件进行过滤。 WHERE子句在表名(FROM子句)之后给出,如下所示:

SELECT   name,node_root,kn_id,shortname
FROM kn_node
WHERE node_root = '集合'
;

这条语句从products表中检索两个列,但不返回所有行,只返回prod_price值为3.49的行,

提示:有多少个0?

你在练习这个示例时,会发现显示的结果可能是3.49、3.490、3.4900等。 出现这样的情况,往往是因为DBMS指定了所使用的数据类型及其默认行为。 所以,如果你的输出可能与书上的有点不同,不必焦虑,毕竟从数学角度讲,3.49和3.4900是一样的。

提示:SQL过滤与应用过滤数据也可以在应用层过滤。

为此,SQL的SELECT语句为客户端应用检索出超过实际所需的数据,然后客户端代码对返回数据进行循环,提取出需要的行。 通常,这种做法极其不妥。优化数据库后可以更快速有效地对数据进行过滤。 而让客户端应用(或开发语言)处理数据库的工作将会极大地影响应用的性能,并且使所创建的应用完全不具备可伸缩性。 此外,如果在客户端过滤数据,服务器不得不通过网络发送多余的数据,这将导致网络带宽的浪费。

注意:WHERE子句的位置

在同时使用ORDER BY和WHERE子句时,应该让ORDER BY位于WHERE之后,否则将会产生错误(关于ORDER BY的使用,请参阅第3课)。

4.2 WHERE子句操作符

  • where 子句操作符
=  等于            > 大于
<> 不等于 >= 大于等于
!= 不等于 !> 不大于
< 小于 BETWEEN 在两个指定值之间
<= 小于等于 IS NULL 为NULL值
!< 不小于

注意:操作符兼容

表4-1中列出的某些操作符是冗余的(如< >与!=相同,!< 相当于>=)。并非所有DBMS都支持这些操作符。想确定你的DBMS支持哪些操作符,请参阅相应的文档。

提示:何时使用引号如果仔细观察上述WHERE子句中的条件,会看到有的值括在单引号内,而有的值未括起来。单引号用来限定字符串。如果将值与字符串类型的列进行比较,就需要限定引号。用来与数值列进行比较的值不用引号。

注意:是!=还是<>?!=和<>通常可以互换。但是,并非所有DBMS都支持这两种不等于操作符。如果有疑问,请参阅相应的DBMS文档。
SELECT   name,node_root,kn_id,shortname
FROM kn_node
WHERE kn_id BETWEEN 2 AND 3

NULL无值(no value),它与字段包含0、空字符串或仅仅包含空格不同。

确定值是否为NULL,不能简单地检查是否等于NULL。SELECT语句有一个特殊的WHERE子句,可用来检查具有NULL值的列。这个WHERE子句就是IS NULL子句。其语法如下:

SELECT   name,node_root,kn_id,shortname
FROM kn_node
WHERE kn_id IS NULL

注意:NULL和非匹配通过过滤选择不包含指定值的所有行时,你可能希望返回含NULL值的行。但是这做不到。因为NULL比较特殊,所以在进行匹配过滤或非匹配过滤时,不会返回这些结果。

4.3 小结

这一课介绍了如何用SELECT语句的WHERE子句过滤返回的数据。我们学习了如何检验相等、不相等、大于、小于、值的范围以及NULL值。

4.4 挑战题

1.编写SQL语句,从Products表中检索产品ID(prod_id)和产品名称(prod_name),只返回价格为9.49美元的产品。 2.编写SQL语句,从Products表中检索产品ID(prod_id)和产品名称(prod_name),只返回价格为9美元或更高的产品。 3.结合第3课和第4课编写SQL语句,从OrderItems表中检索出所有不同订单号(order_num),其中包含100个或更多的产品。 4.编写SQL语句,返回Products表中所有价格在3美元到6美元之间的产品的名称(prod_name)和价格(prod_price),然后按价格对结果进行排序。(本题有多种解决方案,我们在下一课再讨论,不过你可以使用目前已学的知识来解决它。)

第5课 高级数据过滤

这一课讲授如何组合WHERE子句以建立功能更强、更高级的搜索条件。我们还将学习如何使用NOT和IN操作符。

5.1 组合WHERE子句

第4课介绍的所有WHERE子句在过滤数据时使用的都是单一的条件。为了进行更强的过滤控制,SQL允许给出多个WHERE子句。这些子句有两种使用方式,即以AND子句或OR子句的方式使用。

操作符(operator)

用来联结或改变WHERE子句中的子句的关键字,也称为逻辑操作符(logical operator)。

5.1.1 AND操作符

SELECT   name,node_root,kn_id,shortname
FROM kn_node
WHERE kn_id >1 AND node_root='集合'

5.1.2 OR操作符

SELECT  name,node_root,kn_id,shortname
FROM kn_node
WHERE kn_id >1 OR node_root='集合'
;

OR操作符与AND操作符正好相反,它指示DBMS检索匹配任一条件的行。事实上,许多DBMS在OR WHERE子句的第一个条件得到满足的情况下,就不再计算第二个条件了(在第一个条件满足时,不管第二个条件是否满足,相应的行都将被检索出来)。

提示:在WHERE子句中使用圆括号

任何时候使用具有AND和OR操作符的WHERE子句,都应该使用圆括号明确地分组操作符。不要过分依赖默认求值顺序,即使它确实如你希望的那样。使用圆括号没有什么坏处,它能消除歧义。

5.2 IN操作符

IN操作符用来指定条件范围,范围中的每个条件都可以进行匹配。IN取一组由逗号分隔、括在圆括号中的合法值。下面的例子说明了这个操作符。

SELECT   name,node_root,kn_id,shortname
FROM kn_node
WHERE node_root IN ('集合','知识点')
;

为什么要使用IN操作符?其优点如下。 ❑ 在有很多合法选项时,IN操作符的语法更清楚,更直观。 ❑ 在与其他AND和OR操作符组合使用IN时,求值顺序更容易管理。 ❑ IN操作符一般比一组OR操作符执行得更快(在上面这个合法选项很少的例子中,你看不出性能差异)。❑ IN的最大优点是可以包含其他SELECT语句,能够更动态地建立WHERE子句。

5.3 NOT操作符

WHERE子句中的NOT操作符有且只有一个功能,那就是否定其后所跟的任何条件。因为NOT从不单独使用(它总是与其他操作符一起使用),所以它的语法与其他操作符有所不同。NOT关键字可以用在要过滤的列前,而不仅是在其后。

SELECT name,node_root,kn_id,shortname
FROM kn_node
WHERE node_root NOT IN ('集合')
;

这里的NOT否定跟在其后的条件:


SELECT name,node_root,kn_id,shortname
FROM kn_node
WHERE NOT node_root = '集合'

为什么使用NOT? 对于这里的这种简单的WHERE子句,使用NOT确实没有什么优势。 但在更复杂的子句中,NOT是非常有用的。 例如,在与IN操作符联合使用时,NOT可以非常简单地找出与条件列表不匹配的行。

说明:MariaDB中的NOT

MariaDB支持使用NOT否定IN、BETWEEN和EXISTS子句。大多数DBMS允许使用NOT否定任何条件。

5.4 小结

这一课讲授如何用AND和OR操作符组合成WHERE子句,还讲授了如何明确地管理求值顺序,如何使用IN和NOT操作符。

5.5 挑战题

1.编写SQL语句,从Vendors表中检索供应商名称(vend_name),仅返回加利福尼亚州的供应商(这需要按国家[USA]和州[CA]进行过滤,没准其他国家也存在一个加利福尼亚州)。提示:过滤器需要匹配字符串。2.编写SQL语句,查找所有至少订购了总量100个的BR01、BR02或BR03的订单。你需要返回OrderItems表的订单号(order_num)、产品ID(prod_id)和数量,并按产品ID和数量进行过滤。提示:根据编写过滤器的方式,可能需要特别注意求值顺序。3.现在,我们回顾上一课的挑战题。编写SQL语句,返回所有价格在3美元到6美元之间的产品的名称(prod_name)和价格(prod_price)。使用AND,然后按价格对结果进行排序。 4.下面的SQL语句有问题吗?(尝试在不运行的情况下指出。)

第6课 用通配符进行过滤

这一课介绍什么是通配符、如何使用通配符,以及怎样使用LIKE操作符进行通配搜索,以便对数据进行复杂过滤。

6.1 LIKE操作符

%匹配词% 表示前包含和后包含

SELECT   name,node_root,kn_id,shortname
FROM kn_node
WHERE node_root LIKE '%识%'
;

通配符(wildcard)用来匹配值的一部分的特殊字符。 搜索模式(search pattern)由字面值、通配符或两者组合构成的搜索条件。

谓词(predicate)操作符何时不是操作符?答案是,它作为谓词时。从技术上说,LIKE是谓词而不是操作符。虽然最终的结果是相同的,但应该对此术语有所了解,以免在SQL文献或手册中遇到此术语时不知所云。

6.1.1 百分号(%)通配符

最常使用的通配符是百分号(%)。在搜索串中,%表示任何字符出现任意次数。

说明:区分大小写根据DBMS的不同及其配置,搜索可以是区分大小写的。如果区分大小写,则’fish%’与Fish bean bag toy就不匹配。

提示:根据部分信息搜索电子邮件地址有一种情况下把通配符放在搜索模式中间是很有用的,就是根据邮件地址的一部分来查找电子邮件,例如WHERE email LIKE 'b%@forta.com'。

说明:请注意后面所跟的空格有些DBMS用空格来填补字段的内容。 例如,如果某列有50个字符,而存储的文本为Fish bean bag toy(17个字符),则为填满该列需要在文本后附加33个空格。 这样做一般对数据及其使用没有影响,但是可能对上述SQL语句有负面影响。 子句WHERE prod_name LIKE 'F%y’只匹配以F开头、以y结尾的prod_name。如果值后面跟空格,则不是以y结尾,所以Fish bean bag toy就不会检索出来。简单的解决办法是给搜索模式再增加一个%号:'F%y%’还匹配y之后的字符(或空格)。 更好的解决办法是用函数去掉空格。请参阅第8课。注意:请注意NULL通配符%看起来像是可以匹配任何东西,但有个例外,这就是NULL。子句WHERE prod_name LIKE '%’不会匹配产品名称为NULL的行。

6.1.2 下划线(_)通配符

另一个有用的通配符是下划线(_)。下划线的用途与%一样, 但它只匹配单个字符,而不是多个字符。

SELECT   name,node_root,kn_id,shortname
FROM kn_node
WHERE node_root LIKE '_识点'
;

说明:请注意后面所跟的空格与上例一样,可能需要给这个模式添加一个通配符。

6.1.3 方括号([ ])通配符

方括号([])通配符用来指定一个字符集,它必须匹配指定位置(通配符的位置)的一个字符。

说明:并不总是支持集合与前面描述的通配符不一样,并不是所有DBMS都支持用来创建集合的[]。 微软的SQL Server支持集合,但是MySQL,Oracle,DB2,SQLite都不支持。为确定你使用的DBMS是否支持集合,请参阅相应的文档。

SELECT   name,node_root,kn_id,shortname
FROM kn_node
WHERE node_root LIKE '[JM]%'
;

此语句的WHERE子句中的模式为’[JM]%'。 这一搜索模式使用了两个不同的通配符。 [JM]匹配方括号中任意一个字符,它也只能匹配单个字符。 因此,任何多于一个字符的名字都不匹配。[JM]之后的%通配符匹配第一个字符之后的任意数目的字符,返回所需结果。

此通配符可以用前缀字符^(脱字号)来否定。例如,下面的查询匹配以J和M之外的任意字符起头的任意联系人名(与前一个例子相反):

SELECT   name,node_root,kn_id,shortname
FROM kn_node
WHERE NOT node_root LIKE '[^JM]%'
;

当然,也可以使用NOT操作符得出类似的结果。^的唯一优点是在使用多个WHERE子句时可以简化语法:

SELECT   name,node_root,kn_id,shortname
FROM kn_node
WHERE NOT node_root LIKE '[JM]%'
;

6.2 使用通配符的技巧

正如所见,SQL的通配符很有用。 但这种功能是有代价的,即通配符搜索一般比前面讨论的其他搜索要耗费更长的处理时间。 这里给出一些使用通配符时要记住的技巧。 ❑ 不要过度使用通配符。如果其他操作符能达到相同的目的,应该使用其他操作符。 ❑ 在确实需要使用通配符时,也尽量不要把它们用在搜索模式的开始处。把通配符置于开始处,搜索起来是最慢的。 ❑ 仔细注意通配符的位置。如果放错地方,可能不会返回想要的数据。 总之,通配符是一种极其重要和有用的搜索工具,以后我们经常会用到它。

6.3 小结

这一课介绍了什么是通配符,如何在WHERE子句中使用SQL通配符,还说明了通配符应该细心使用,不要使用过度。

6.4 挑战题

1.编写SQL语句,从Products表中检索产品名称(prod_name)和描述(prod_desc),仅返回描述中包含toy一词的产品。2.反过来再来一次。编写SQL语句,从Products表中检索产品名称(prod_name)和描述(prod_desc),仅返回描述中未出现toy一词的产品。这次,按产品名称对结果进行排序。3.编写SQL语句,从Products表中检索产品名称(prod_name)和描述(prod_desc),仅返回描述中同时出现toy和carrots的产品。有好几种方法可以执行此操作,但对于这个挑战题,请使用AND和两个LIKE比较。4.来个比较棘手的。我没有特别向你展示这个语法,而是想看看你根据目前已学的知识是否可以找到答案。编写SQL语句,从Products表中检索产品名称(prod_name)和描述(prod_desc),仅返回在描述中以先后顺序同时出现toy和carrots的产品。提示:只需要用带有三个%符号的LIKE即可。

第7课 创建计算字段

7.1 计算字段

存储在数据库表中的数据一般不是应用程序所需要的格式,下面举几个例子。 ❑ 需要显示公司名,同时还需要显示公司的地址,但这两个信息存储在不同的表列中。 ❑ 城市、州和邮政编码存储在不同的列中(应该这样), 但邮件标签打印程序需要把它们作为一个有恰当格式的字段检索出来。 ❑ 列数据是大小写混合的,但报表程序需要把所有数据按大写表示出来。 ❑ 物品订单表存储物品的价格和数量,不存储每个物品的总价格(用价格乘以数量即可)。 但为打印发票,需要物品的总价格。 ❑ 需要根据表数据进行诸如总数、平均数的计算。在上述每个例子中,存储在表中的数据都不是应用程序所需要的。 我们需要直接从数据库中检索出转换、计算或格式化过的数据,而不是检索出数据, 然后再在客户端应用程序中重新格式化。这就是计算字段可以派上用场的地方了。

与前几课介绍的列不同,计算字段并不实际存在于数据库表中。计算字段是运行时在SELECT语句内创建的。

字段(field)

基本上与列(column)的意思相同,经常互换使用,不过数据库列一般称为列,而字段这个术语通常在计算字段这种场合下使用。

需要特别注意,只有数据库知道SELECT语句中哪些列是实际的表列,哪些列是计算字段。从客户端(如应用程序)来看,计算字段的数据与其他列的数据的返回方式相同。

提示:客户端与服务器的格式

在SQL语句内可完成的许多转换和格式化工作都可以直接在客户端应用程序内完成。但一般来说,在数据库服务器上完成这些操作比在客户端中完成要快得多。

7.2 拼接字段

为了说明如何使用计算字段,我们来举一个简单例子,创建由两列组成的标题。 Vendors表包含供应商名和地址信息。假如要生成一个供应商报表,需要在格式化的名称(位置)中列出供应商的位置。 此报表需要一个值,而表中数据存储在两个列vend_name和vend_country中。 此外,需要用括号将vend_country括起来,这些东西都没有存储在数据库表中。 这个返回供应商名称和地址的SELECT语句很简单,但我们是如何创建这个组合值的呢? 拼接(concatenate)将值联结到一起(将一个值附加到另一个值)构成单个值。 解决办法是把两个列拼接起来。 在SQL中的SELECT语句中,可使用一个特殊的操作符来拼接两个列。 根据你所使用的DBMS,此操作符可用加号(+)或两个竖杠(||)表示。 在MySQL和MariaDB中,必须使用特殊的函数。

说明:是+还是||?

SQL Server使用+号。DB2、Oracle、PostgreSQL和SQLite使用||。详细请参阅具体的DBMS文档。下面是使用加号的例子(多数DBMS使用这种语法):

SELECT  vend_name + '(' + vend_country + ')'
FROM vendors
order by vend_name

下面是相同的语句,但使用的是|| 语法:

SELECT  vend_name || '(' || vend_country || ')'
from vendors
order by vend_name

下面是使用MySQL或MariaDB时需要使用的语句:

SELECT  CONCAT(node_root,'(',kn_id,')')
FROM kn_node

许多数据库(不是所有)保存填充为列宽的文本值,而实际上你要的结果不需要这些空格。为正确返回格式化的数据,必须去掉这些空格。这可以使用SQL的RTRIM()函数来完成

RTRIM()函数去掉值右边的所有空格。通过使用RTRIM(),各个列都进行了整理。说明:TRIM函数大多数DBMS都支持RTRIM()(正如刚才所见,它去掉字符串右边的空格)、LTRIM()(去掉字符串左边的空格)以及TRIM()(去掉字符串左右两边的空格)。

SQL支持列别名。别名(alias)是一个字段或值的替换名。别名用AS关键字赋予。

SELECT  node_root AS nr
FROM kn_node
;

说明:AS通常可选在很多DBMS中,AS关键字是可选的,不过最好使用它,这被视为一条最佳实践。 提示:别名的其他用途别名还有其他用途。常见的用途包括在实际的表列名包含不合法的字符(如空格)时重新命名它,在原来的名字含混或容易误解时扩充它。注意:别名别名的名字既可以是一个单词,也可以是一个字符串。如果是后者,字符串应该括在引号中。虽然这种做法是合法的,但不建议这么去做。多单词的名字可读性高,不过会给客户端应用带来各种问题。因此,别名最常见的使用是将多个单词的列名重命名为一个单词的名字。 说明:导出列别名有时也称为导出列(derived column),不管怎么叫,它们所代表的是相同的东西。

7.3 执行算术计算

计算字段的另一常见用途是对检索出的数据进行算术计算。

SELECT  (kn_id * node_id )AS id
FROM kn_node
;

提示:如何测试计算SELECT语句为测试、检验函数和计算提供了很好的方法。虽然SELECT通常用于从表中检索数据,但是省略了FROM子句后就是简单地访问和处理表达式,例如SELECT 3*2;将返回6,SELECT Trim(' abc ');将返回abc,SELECT Curdate();使用Curdate()函数返回当前日期和时间。现在你明白了,可以根据需要使用SELECT语句进行检验。

7.4 小结

这一课介绍了计算字段以及如何创建计算字段。我们用例子说明了计算字段在字符串拼接和算术计算中的用途。此外,还讲述了如何创建和使用别名,以便应用程序能引用计算字段。

第8课 使用函数处理数据

与SQL语句不一样,SQL函数不是可移植的。这意味着为特定SQL实现编写的代码在其他实现中可能不能用。可移植(portable)所编写的代码可以在多个系统上运行。

8.2 使用函数大多数SQL实现支持以下类型的函数。

❑ 用于处理文本字符串(如删除或填充值,转换值为大写或小写)的文本函数。 ❑ 用于在数值数据上进行算术操作(如返回绝对值,进行代数运算)的数值函数。 ❑ 用于处理日期和时间值并从这些值中提取特定成分(如返回两个日期之差,检查日期有效性)的日期和时间函数。 ❑ 用于生成美观好懂的输出内容的格式化函数(如用语言形式表达出日期,用货币符号和千分位表示金额)。 ❑ 返回DBMS正使用的特殊信息(如返回用户登录信息)的系统函数。我们在上一课看到函数用于SELECT后面的列名,但函数的作用不仅于此。它还可以作为SELECT语句的其他成分,如在WHERE子句中使用,在其他SQL语句中使用等,后面会做更多的介绍。

8.2.1 文本处理函数

在上一课,我们已经看过一个文本处理函数的例子,其中使用RTRIM()函数来去除列值右边的空格。 下面是另一个例子,这次使用的是UPPER()函数: UPPER()将文本转换为大写,

提示:大写,小写,大小写混合此时你应该已经知道SQL函数不区分大小写,因此upper(),UPPER(), Upper()都可以,substr(), SUBSTR(), SubStr()也都行。随你的喜好,不过注意保持风格一致,不要变来变去,否则你写的程序代码就不好读了。

DATEPART()函数,顾名思义,此函数返回日期的某一部分。DATEPART()函数有两个参数,它们分别是返回的成分和从中返回成分的日期。在此例子中,DATEPART()只从order_date列中返回年份

EXTRACT()函数用来提取日期的成分,year表示提取哪个部分,

第22课 高级SQL特性

这一课介绍SQL所涉及的几个高级数据处理特性:约束、索引和触发器。