SQL必知必会(第4版) Ben Forta
引言 代码行前的箭头(➥)表示代码太长,上一行容纳不下。在➥符号后输入的所有字符都应该是前一行的内容。 第1课 了解SQL 理解数据库概念是掌握SQL的重要前提, 数据库是一个以某种有组织的方式存储的数据集合。 描述表的这组信息就是所谓的模式(schema),模式可以用来描述数据库中特定的表,也可以用来描述整个数据库(和其中表的关系)。 正确地将数据分解为多个列极为重要。 数据类型(datatype)定义了列可以存储哪些数据种类。 数据类型所允许的数据的类型。每个表列都有相应的数据类型,它限制(或允许)该列中存储的数据。 表中的任何列都可以作为主键,只要它满足以下条件:
□ 任意两行都不具有相同的主键值;
□ 每一行都必须具有一个主键值(主键列不允许NULL值);
□ 主键列中的值不允许修改或更新;
□ 主键值不能重用(如果某行从表中删除,它的主键不能赋给以后的新行)。 SQL(发音为字母S-Q-L或sequel)是Structured Query Language(结构化查询语言)的缩写。SQL是一种专门用来与数据库沟通的语言。 标准SQL由ANSI标准委员会管理,从而称为ANSI SQL。所有主要的DBMS,即使有自己的扩展,也都支持ANSI SQL。 第2课 检索数据 为了使用SELECT检索表数据,必须至少给出两条信息——想选择什么,以及从什么地方选择。 SELECT prod_name
FROM Products; 如果没有明确排序查询结果(下一课介绍),则返回的数据没有特定的顺序。 在选择多个列时,一定要在列名之间加上逗号,但最后一个列名后不加。如果在最后一个列名后加了逗号,将出现错误。 如果给定一个通配符(*),则返回表中所有列。 一般而言,除非你确实需要表中的每一列,否则最好别使用*通配符。虽然使用通配符能让你自己省事,不用明确列出所需列,但检索不需要的列通常会降低检索和应用程序的性能。 办法就是使用DISTINCT关键字,顾名思义,它指示数据库只返回不同的值。 DISTINCT关键字作用于所有的列,不仅仅是跟在其后的那一列。 第一个被检索的行是第0行,而不是第1行。因此,LIMIT 1 OFFSET 1会检索第2行,而不是第1行。 注释使用--(两个连字符)嵌在行内。-- 之后的文本就是注释 第3课 排序检索数据 ORDER BY子句取一个或多个列的名字,据此对输出进行排序。 在指定一条ORDER BY子句时,应该保证它是SELECT语句中最后一条子句。如果它不是最后的子句,将会出现错误消息。 除了能用列名指出排序顺序外,ORDER BY还支持按相对列位置进行排序。 为了进行降序排序,必须指定DESC关键字。 SELECT prod_id, prod_price, prod_nameFROM ProductsORDER BY prod_price DESC, prod_name; DESC关键字只应用到直接位于其前面的列名。 这个子句必须是SELECT语句中的最后一条子句 第4课 过滤数据 只检索所需数据需要指定搜索条件(search criteria),搜索条件也称为过滤条件(filter condition)。 在同时使用ORDER BY和WHERE子句时,应该让ORDER BY位于WHERE之后,否则将会产生错误 单引号用来限定字符串。如果将值与字符串类型的列进行比较,就需要限定引号。用来与数值列进行比较的值不用引号。 !=和<>通常可以互换。但是,并非所有DBMS都支持这两种不等于操作符。例如,Microsoft Access支持<>而不支持!=。如果有疑问,请参阅相应的DBMS文档。 在使用BETWEEN时,必须指定两个值——所需范围的低端值和高端值。这两个值必须用AND关键字分隔。BETWEEN匹配范围中所有的值,包括指定的开始值和结束值。 过滤数据时,一定要验证被过滤列中含NULL的行确实出现在返回的数据中。 第5课 高级数据过滤 操作符(operator)用来联结或改变WHERE子句中的子句的关键字,也称为逻辑操作符(logical operator)。 AND用在WHERE子句中的关键字,用来指示检索满足所有给定条件的行。 许多DBMS在OR WHERE子句的第一个条件得到满足的情况下,就不再计算第二个条件了(在第一个条件满足时,不管第二个条件是否满足,相应的行都将被检索出来)。 SQL(像多数语言一样)在处理OR操作符前,优先处理AND操作符。 IN操作符用来指定条件范围,范围中的每个条件都可以进行匹配。IN取一组由逗号分隔、括在圆括号中的合法值。 IN操作符完成了与OR相同的功能 IN的最大优点是可以包含其他SELECT语句,能够更动态地建立WHERE子句。 但在更复杂的子句中,NOT是非常有用的。例如,在与IN操作符联合使用时,NOT可以非常简单地找出与条件列表不匹配的行。 第6课 用通配符进行过滤 利用通配符,可以创建比较特定数据的搜索模式。 通配符搜索只能用于文本字段(字符串),非文本数据类型字段不能使用通配符搜索。 如果使用的是Microsoft Access,需要使用*而不是%。 另一个有用的通配符是下划线(_)。下划线的用途与%一样,但它只匹配单个字符,而不是多个字符。 8 inch teddy bear产品没有匹配,因为搜索模式要求匹配两个通配符而不是一个。 方括号([])通配符用来指定一个字符集,它必须匹配指定位置(通配符的位置)的一个字符。 SELECT cust_contact
FROM Customers
WHERE cust_contact LIKE '[JM]%'
ORDER BY cust_contact; 此通配符可以用前缀字符^(脱字号)来否定。 第7课 创建计算字段 存储在数据库表中的数据一般不是应用程序所需要的格式 计算字段并不实际存在于数据库表中。计算字段是运行时在SELECT语句内创建的。 在SQL中的SELECT语句中,可使用一个特殊的操作符来拼接两个列。根据你所使用的DBMS,此操作符可用加号(+)或两个竖杠(||)表示。在MySQL和MariaDB中,必须使用特殊的函数。 SELECT Concat(vend_name, ' (', vend_country, ')')
FROM Vendors
ORDER BY vend_name; 为正确返回格式化的数据,必须去掉这些空格。这可以使用SQL的RTRIM()函数来完成 大多数DBMS都支持RTRIM()(正如刚才所见,它去掉字符串右边的空格)、LTRIM()(去掉字符串左边的空格)以及TRIM()(去掉字符串左右两边的空格)。 在很多DBMS中,AS关键字是可选的,不过最好使用它,这被视为一条最佳实践。 SELECT prod_id, quantity, item_price, quantity*item_price AS expanded_priceFROM OrderItemsWHERE order_num = 20008; 输出中显示的expanded_price列是一个计算字段,此计算为quantity*item_price。客户端应用现在可以使用这个新计算列,就像使用其他列一样 SELECT语句为测试、检验函数和计算提供了很好的方法。虽然SELECT通常用于从表中检索数据,但是省略了FROM子句后就是简单地访问和处理表达式,例如SELECT 3 * 2;将返回6, SELECT Trim(' abc ');将返回abc, SELECT Now();使用Now()函数返回当前日期和时间。现在你明白了,可以根据需要使用SELECT语句进行检验。 第8课 使用函数处理数据 与SQL语句不一样,SQL函数不是可移植的。 如果你决定使用函数,应该保证做好代码注释,以便以后你(或其他人)能确切地知道所编写的SQL代码的含义。 大多数SQL实现支持以下类型的函数。□ 用于处理文本字符串(如删除或填充值,转换值为大写或小写)的文本函数。□ 用于在数值数据上进行算术操作(如返回绝对值,进行代数运算)的数值函数。□ 用于处理日期和时间值并从这些值中提取特定成分(如返回两个日期之差,检查日期有效性)的日期和时间函数。 UPPER()将文本转换为大写 SOUNDEX是一个将任何文本串转换为描述其语音表示的字母数字模式的算法。SOUNDEX考虑了类似的发音字符和音节,使得能对字符串进行发音比较而不是字母比较。 日期和时间值以特殊的格式存储,以便能快速和有效地排序或过滤,并且节省物理存储空间。 DATEPART()函数,顾名思义,此函数返回日期的某一部分。DATEPART()函数有两个参数,它们分别是返回的成分和从中返回成分的日期。 MySQL和MariaDB用户可使用名为YEAR()的函数从日期中提取年份: 表8-3 常用数值处理函数 第9课 汇总数据 □ 确定表中行数(或者满足某个条件或包含某个特定值的行数);□ 获得表中某些行的和;□ 找出表列(或所有行或某些特定的行)的最大值、最小值、平均值。 表9-1 SQL聚集函数[插图] AVG()可用来返回所有列的平均值,也可以用来返回特定列或行的平均值。 使用COUNT(*)对表中行的数目进行计数,不管表列中包含的是空值(NULL)还是非空值。
□ 使用COUNT(column)对特定列中具有值的行进行计数,忽略NULL值。 如果指定列名,则COUNT()函数会忽略指定列的值为空的行,但如果COUNT()函数中用的是星号(*),则不忽略。 在用于文本数据时,MAX()返回按该列排序后的最后一行。 在用于文本数据时,MIN()返回该列排序后最前面的行。 SUM()函数忽略列值为NULL的行。 只包含不同的值,指定DISTINCT参数。 如果指定列名,则DISTINCT只能用于COUNT()。DISTINCT不能用于COUNT(*)。类似地,DISTINCT必须使用列名,不能用于计算或表达式。 在指定别名以包含某个聚集函数的结果时,不应该使用表中实际的列名。虽然这样做也算合法,但许多SQL实现不支持,可能会产生模糊的错误消息。 这些函数很高效,它们返回结果一般比你在自己的客户端应用程序中计算要快得多。 第10课 分组数据 GROUPBY子句和HAVING子句。 GROUP BY子句必须出现在WHERE子句之后,ORDER BY子句之前。 唯一的差别是,WHERE过滤行,而HAVING过滤分组。 使用HAVING时应该结合GROUP BY子句,而WHERE子句用于标准的行级过滤。 表10-1 ORDER BY与GROUP BY 一般在使用GROUP BY子句时,应该也给出ORDER BY子句。这是保证数据正确排序的唯一方法。千万不要仅依赖GROUP BY排序数据。 Microsoft Access不允许按别名排序,因此这个例子在Access中将失败。解决方法是用实际的计算或字段位置替换items(在ORDER BY子句中),即ORDER BY COUNT(*), order_num或ORDER BY 2, order_num。 表10-2 SELECT子句及其顺序 第11课 使用子查询 SQL还允许创建子查询(subquery),即嵌套在其他查询中的查询 可以把一条SELECT语句返回的结果用于另一条SELECT语句的WHERE子句。 在SELECT语句中,子查询总是从内向外处理。 把子查询分解为多行并进行适当的缩进,能极大地简化子查询的使用。 作为子查询的SELECT语句只能查询单个列。企图检索多个列将返回错误。 使用子查询的另一方法是创建计算字段。 SELECT cust_name, cust_state, (SELECT COUNT(*) FROM Orders WHERE Orders.cust_id = Customers.cust_id) AS ordersFROM CustomersORDER BY cust_name; 用一个句点分隔表名和列名,在有可能混淆列名时必须使用这种语法。 如果在SELECT语句中操作多个表,就应使用完全限定列名来避免歧义。 子查询常用于WHERE子句的IN操作符中,以及用来填充计算列。 第12课 联结表 SQL最强大的功能之一就是能在数据查询的执行中联结(join)表。 关系表的设计就是要把信息分解成多个表,一类数据一个表。各表通过某些共同的值互相关联(所以才叫关系数据库)。 可伸缩(scale)能够适应不断增加的工作量而不失败。设计良好的数据库或应用程序称为可伸缩性好(scale well)。 联结是一种机制,用来在一条SELECT语句中关联表,因此称为联结。 SELECT vend_name, prod_name, prod_price
FROM Vendors, Products
WHERE Vendors.vend_id = Products.vend_id; 笛卡儿积(cartesian product)由没有联结条件的表关系返回的结果为笛卡儿积。检索出的行的数目将是第一个表中的行数乘以第二个表中的行数。 目前为止使用的联结称为等值联结(equijoin),它基于两个表之间的相等测试。这种联结也称为内联结(inner join)。 SELECT prod_name, vend_name, prod_price, quantity
FROM OrderItems, Products, Vendors
WHERE Products.vend_id = Vendors.vend_id
AND OrderItems.prod_id = Products.prod_id
AND order_num = 20007; 不要联结不必要的表。联结的表越多,性能下降越厉害。 联结是SQL中一个最重要、最强大的特性,有效地使用联结需要对关系数据库设计有基本的了解。 第13课 创建高级联结 SQL除了可以对列名和计算字段使用别名,还允许给表名起别名。这样做有两个主要理由:□ 缩短SQL语句;□ 允许在一条SELECT语句中多次使用相同的表。 表别名只在查询执行中使用。与列别名不一样,表别名不返回到客户端。 自联结(self-join)、自然联结(natural join)和外联结(outer join)。 使用表别名的一个主要原因是能在一条SELECT语句中不止一次引用相同的表 SELECT c1.cust_id, c1.cust_name, c1.cust_contact
FROM Customers AS c1, Customers AS c2
WHERE c1.cust_name = c2.cust_name
AND c2.cust_contact = 'Jim Jones'; 自然联结排除多次出现,使每一列只返回一次。 联结包含了那些在相关表中没有关联行的行。这种联结称为外联结。 SELECT Customers.cust_id, Orders.order_num
FROM Customers LEFT OUTER JOIN Orders
ON Customers.cust_id = Orders.cust_id; 在使用OUTER JOIN语法时,必须使用RIGHT或LEFT关键字指定包括其所有行的表(RIGHT指出的是OUTER JOIN右边的表,而LEFT指出的是OUTER JOIN左边的表)。 SQLite支持LEFT OUTER JOIN,但不支持RIGHT OUTER JOIN。 全外联结(full outer join),它检索两个表中的所有行并关联那些可以关联的行。与左外联结或右外联结包含一个表的不关联的行不同, 应该总是提供联结条件,否则会得出笛卡儿积。 第14课 组合查询 主要有两种情况需要使用组合查询:
□ 在一个查询中从不同的表返回结构数据;
□ 对一个表执行多个查询,按一个查询返回数据。 可用UNION操作符来组合数条SQL查询。利用UNION,可给出多条SELECT语句,将它们的结果组合成一个结果集。 SELECT cust_name, cust_contact, cust_emailFROM CustomersWHERE cust_state IN ('IL', 'IN', 'MI')UNIONSELECT cust_name, cust_contact, cust_emailFROM CustomersWHERE cust_name = 'Fun4All'; UNION指示DBMS执行这两条SELECT语句,并把输出组合成一个查询结果集。 在这个简单的例子中,使用UNION可能比使用WHERE子句更为复杂。但对于较复杂的过滤条件,或者从多个表(而不是一个表)中检索数据的情形,使用UNION可能会使处理更简单。 UNION中的每个查询必须包含相同的列、表达式或聚集函数(不过,各个列不需要以相同的次序列出)。 UNION从查询结果集中自动去除了重复的行 使用UNION ALL, DBMS不取消重复的行。 在用UNION组合查询时,只能使用一条ORDER BY子句,它必须位于最后一条SELECT语句之后。 虽然ORDER BY子句似乎只是最后一条SELECT语句的组成部分,但实际上DBMS将用它来排序所有SELECT语句返回的所有结果。 不管结果中有无重复。使用UNION可极大地简化复杂的WHERE子句,简化从多个表中检索数据的工作。 第15课 插入数据 插入有几种方式:□ 插入完整的行;□ 插入行的一部分;□ 插入某些查询的结果。 使用INSERT语句可能需要客户端/服务器DBMS中的特定安全权限。在你试图使用INSERT前,应该保证自己有足够的安全权限。 各列必须以它们在表定义中出现的次序填充。 因为提供了列名,VALUES必须以其指定的次序匹配指定的列名,不一定按各列出现在表中的实际次序。其优点是,即使表的结构改变,这条INSERT语句仍然能正确工作。 不要使用没有明确给出列的INSERT语句。给出列能使SQL代码继续发挥作用,即使表结构发生了变化。 如果表的定义允许,则可以在INSERT操作中省略某些列。省略的列必须满足以下某个条件。□ 该列定义为允许NULL值(无值或空值)。□ 在表定义中给出默认值。这表示如果不给出值,将使用默认值。 INSERT通常只插入一行。要插入多行,必须执行多个INSERT语句。INSERTSELECT是个例外,它可以用一条INSERT插入多行,不管SELECT语句返回多少行,都将被INSERT插入。 说明:INSERT SELECT与SELECT INTO它们之间的一个重要差别是前者插入数据,而后者导出数据。 第16课 更新和删除数据 □ 更新表中的特定行;□ 更新表中的所有行。 在使用UPDATE时一定要细心。因为稍不注意,就会更新表中的所有行。 基本的UPDATE语句由三部分组成,分别是:□ 要更新的表;□ 列名和它们的新值;□ 确定要更新哪些行的过滤条件。 在更新多个列时,只需要使用一条SET命令,每个“列=值”对之间用逗号分隔(最后一列之后不用逗号)。 UPDATE语句中可以使用子查询,使得能用SELECT语句检索出的数据更新列数据。 有的SQL实现支持在UPDATE语句中使用FROM子句,用一个表的数据更新另一个表的行。 要删除某个列的值,可设置它为NULL(假如表定义允许NULL值)。 有两种使用DELETE的方式:□ 从表中删除特定的行;□ 从表中删除所有行。 DELETE FROM Customers
WHERE cust_id = '1000000006'; 如果想从表中删除所有行,不要使用DELETE。可使用TRUNCATE TABLE语句,它完成相同的工作,而速度更快(因为不记录数据的变动)。 在UPDATE或DELETE语句使用WHERE子句前,应该先用SELECT进行测试,保证它过滤的是正确的记录,以防编写的WHERE子句不正确。 第17课 创建和操纵表 用程序创建表,可以使用SQL的CREATE TABLE语句。 利用CREATE TABLE创建表,必须给出下列信息:□ 新表的名字,在关键字CREATE TABLE之后给出;□ 表列的名字和定义,用逗号分隔;□ 有的DBMS还要求指定表的位置。 在创建新的表时,指定的表名必须不存在,否则会出错。防止意外覆盖已有的表,SQL要求首先手工删除该表(请参阅后面的内容),然后再重建它,而不是简单地用创建表语句覆盖它。 NULL值就是没有值或缺值。允许NULL值的列也允许在插入行时不给出该列的值。不允许NULL值的列不接受没有列值的行,换句话说,在插入或更新行时,该列必须有值。 NULL为默认设置,如果不指定NOT NULL,就认为指定的是NULL。 主键是其值唯一标识表中每一行的列。只有不允许NULL值的列可作为主键,允许NULL值的列不能作为唯一标识。 默认值在CREATE TABLE语句的列定义中用关键字DEFAULT指定。 默认值经常用于日期或时间戳列 更新表定义,可以使用ALTER TABLE语句。 使用ALTER TABLE要极为小心,应该在进行改动前做完整的备份(表结构和数据的备份)。数据库表的更改不能撤销,如果增加了不需要的列,也许无法删除它们。类似地,如果删除了不应该删除的列,可能会丢失该列中的所有数据。 删除表没有确认,也不能撤销,执行这条语句将永久删除该表。 第18课 使用视图 视图是虚拟的表。与包含数据的表不一样,视图只包含使用时动态检索数据的查询。 ProductCustomers是一个视图,它不包含任何列或数据,包含的是一个查询(与上面用以正确联结表的查询相同)。 重要的是,要知道视图仅仅是用来查看存储在别处数据的一种设施。视图本身不包含数据,因此返回的数据是从其他表中检索出来的。在添加或更改这些表中的数据时,视图将返回改变过的数据。 许多DBMS禁止在视图查询中使用ORDER BY子句 视图用CREATE VIEW语句来创建。与CREATE TABLE一样,CREATE VIEW只能用于创建不存在的视图。 CREATE VIEW ProductCustomers ASSELECT cust_name, cust_contact, prod_idFROM Customers, Orders, OrderItemsWHERE Customers.cust_id = Orders.cust_id AND OrderItems.order_num = Orders.order_num; 视图极大地简化了复杂SQL语句的使用。利用视图,可一次性编写基础的SQL,然后根据需要多次使用。 视图的另一常见用途是重新格式化检索出的数据。 从视图检索数据时如果使用了一条WHERE子句,则两组子句(一组在视图中,另一组是传递给视图的)将自动组合。 第19课 使用存储过程 存储过程就是为以后使用而保存的一条或多条SQL语句。可将其视为批文件,虽然它们的作用不仅限于批处理。 使用存储过程有三个主要的好处,即简单、安全、高性能 执行存储过程的SQL语句很简单,即EXECUTE。EXECUTE接受存储过程名和需要传递给它的任何参数。 SQL Server中所有局部变量名都以@起头 第20课 管理事务处理 使用事务处理(transaction processing),通过确保成批的SQL操作要么完全执行,要么完全不执行,来维护数据库的完整性。 事务处理是一种机制,用来管理必须成批执行的SQL操作,保证数据库不包含不完整的操作结果。利用事务处理,可以保证一组操作不会中途停止,它们要么完全执行,要么完全不执行(除非明确指示)。如果没有错误发生,整组语句提交给(写到)数据库表;如果发生错误,则进行回退(撤销),将数据库恢复到某个已知且安全的状态。 事务处理用来管理INSERT、UPDATE和DELETE语句。不能回退SELECT语句(回退SELECT语句也没有必要),也不能回退CREATE或DROP操作。 管理事务的关键在于将SQL语句组分解为逻辑块,并明确规定数据何时应该回退,何时不应该回退。 COMMIT用于保存更改,ROLLBACK用于撤销 SQL的ROLLBACK命令用来回退(撤销)SQL语句 一般的SQL语句都是针对数据库表直接执行和编写的。这就是所谓的隐式提交(implicit commit),即提交(写或保存)操作是自动进行的。 使用简单的ROLLBACK和COMMIT语句,就可以写入或撤销整个事务。但是,只对简单的事务才能这样做,复杂的事务可能需要部分提交或回退。 要支持回退部分事务,必须在事务处理块中的合适位置放置占位符。这样,如果需要回退,可以回退到某个占位符。 可以在SQL代码中设置任意多的保留点,越多越好。为什么呢?因为保留点越多,你就越能灵活地进行回退。 第21课 使用游标 游标(cursor)是一个存储在DBMS服务器上的数据库查询,它不是一条SELECT语句,而是被该语句检索出来的结果集。 游标主要用于交互式应用,其中用户需要滚动屏幕上的数据,并对数据进行浏览或做出更改。 使用DECLARE语句创建游标 使用OPEN CURSOR语句打开游标 CLOSE语句用来关闭游标。一旦游标关闭,如果不再次打开,将不能使用。第二次使用它时不需要再声明,只需用OPEN打开它即可。 第22课 高级SQL特性 约束(constraint)
管理如何插入或处理数据库数据的规则。 主键是一种特殊的约束,用来保证一列(或一组列)中的值是唯一的,而且永不改动。 外键是表中的一列,其值必须列在另一表的主键中。 ADD CONSTRAINT CHECK (gender LIKE '[MF]') 索引用来排序数据以加快搜索和排序操作的速度。 索引改善检索操作的性能,但降低了数据插入、修改和删除的性能。在执行这些操作时,DBMS必须动态地更新索引。 触发器是特殊的存储过程,它在特定的数据库活动发生时自动执行。触发器可以与特定表上的INSERT、UPDATE和DELETE操作(或组合)相关联。 触发器内的代码具有以下数据的访问权:□ INSERT操作中的所有新数据;□ UPDATE操作中的所有新数据和旧数据;□ DELETE操作中删除的数据。 一般来说,约束的处理比触发器快,因此在可能的时候,应该尽量使用约束。 安全性使用SQL的GRANT和REVOKE语句来管理 约束是实施引用完整性的重要部分,索引可改善数据检索的性能,触发器可以用来执行运行前后的处理,安全选项可用来管理数据访问。