MySQL必知必会学习笔记

什么是数据库

数据库(database) 保存有组织的数据的容器(通常是一个文件或一组文件)。
数据库软件应称为DBMS(数据库管理系统)。数据库是通过DBMS创建和操纵的容器。数据库可以是保存在硬设备上的文件,但也可以不是。在很大程度上说,数据库究竟是文件还是别的什么东西并不重要,因为你并不直接访问数据库;你使用的是DBMS,它替你访问数据库。

表(table) 某种特定类型数据的结构化清单。

虽然在相同数据库中不能两次使用相同的表名,但在不同的数据库中却可以使用相同的表名。

模式(schema) 关于数据库和表的布局及特性的信息。

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

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

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

行(row) 表中的一个记录。是记录还是行? 你可能听到用户在提到行(row)时称其为
数据库记录(record)。在很大程度上,这两个术语是可以互相替代的,但从技术上说,行才是正确的术语。

主键(primary key)①一一列(或一组列),其值能够唯一区分表中每个行。
表中的任何列都可以作为主键,只要它满足以下条件:
 任意两行都不具有相同的主键值;
 每个行都必须具有一个主键值(主键列不允许NULL值)。

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

SQL(发音为字母S-Q-L或sequel)是结构化查询语言(Structured Query Language)的缩写。SQL是一种专门用来与数据库通信的语言。

设计SQL的目的是很好地完成一项任务,即提供一种从数据库中读写数据的简单有效的方法。

SQL尽管看上去很简单,但它实际上是一种强有力的语言,灵活使用其语言元素,可以进行非常复杂和高级的数据库操作。

MySQL简介

MySQL受到的唯一真正的批评是它并不总是支持其他DBMS提供的功能和特性。然而,这一点也正在逐步得到改善,MySQL的各个新版本正不断增加新特性、新功能。

客户机—服务器软件

DBMS可分为两类:一类为基于共享文件系统的DBMS,另一类为基于客户机—服务器的DBMS。前者(包括诸如Microsoft Access和FileMaker)用于桌面用途,通常不用于高端或更关键的应用。
MySQL、Oracle以及Microsoft SQL Server等数据库是基于客户机—服务器的数据库。客户机—服务器应用分为两个不同的部分。服务器部分是负责所有数据访问和处理的一个软件。这个软件运行在称为数据库服务器的计算机上。

与数据文件打交道的只有服务器软件。关于数据、数据添加、删除和数据更新的所有请求都由服务器软件完成。这些请求或更改来自运行客户机软件的计算机。客户机是与用户打交道的软件。例如,如果你请求一个按字母顺序列出的产品表,则客户机软件通过网络提交该请求给服务器软件。服务器软件处理这个请求,根据需要过滤、丢弃和排序数据;然后把结果送回到你的客户机软件。

所有这些活动对用户都是透明的。数据存储在别的地方,或者数据库服务器为你完成这个处理这一事实是隐藏的。你不需要直接访问数据文件。事实上,多数网络的建立使用户不具有对数据的访问权,甚至不具有对存储数据的驱动器的访问权。

使用MySQL

SHOW DATABASES;返回可用数据库的一个列表。
SHOW TABLES;返回当前选择的数据库内可用表的列表。
SHOW也可以用来显示表列:
SHOW COLUMNS FROM customers;

DESCRIBE customers;是SHOW COLUMNS FROM customers;的一种快捷方式。

其他SHOW语句还有:

 SHOW STATUS,用于显示广泛的服务器状态信息;
 SHOW CREATE DATABASE和SHOW CREATE TABLE,分别用来显示创建特定数据库或表的MySQL语句;
 SHOW GRANTS,用来显示授予用户(所有用户或特定用户)的安全权限;
 SHOW ERRORS和SHOW WARNINGS,用来显示服务器错误或警告消息
HELP SHOW;显示允许的SHOW语句。

检索数据

在处理SQL语句时,其中所有空格都被忽略。SQL语句可以在一行上给出,也可以分成许多行。多数SQL开发人员认为将SQL语句分成多行更容易阅读和调试。
DISTINCT关键字,顾名思义,此关键字指示MySQL只返回不同的值。

SELECT DISTINCT ven_id 
FROM  products;

为了返回第一行或前几行,可使用LIMIT子句。

SELECT prod_name
FROM products
LIMIT 5;

LIMIT 5指示MySQL返回不多于5行。
为得出下一个5行,可指定要检索的开始行和行数,如下所示:

SELECT prod_name
FROM products
LIMIT 5,5;

LIMIT 5, 5指示MySQL返回从行5开始的5行。第一个数为开始位置,第二个数为要检索的行数。

行0 检索出来的第一行为行0而不是行1。因此,LIMIT 1, 1将检索出第二行而不是第一行。

在行数不够时 LIMIT中指定要检索的行数为检索的最大行数。如果没有足够的行(例如,给出LIMIT 10, 5,但只有13
行),MySQL将只返回它能返回的那么多行。

SQL也可使用完全限定的名字来引用列(同时使用表名和列字)。

SELECT products.prod_name
FROM products;

这里指定了一个完全限定的列名。表名也可以是完全限定的,如下所示:

SELECT products.prod_name
FROM crashcource.products;

第5章 排序检索数据

子句(clause) SQL语句由子句构成,有些子句是必需的,而有的是可选的。一个子句通常由一个关键字和所提供的数据组成。

ORDER BY子句取一个或多个列的名字,据此对输出进行排序。

通过非选择列进行排序

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

按多个列排序,只要指定列名,列名之间用逗号分开即可。

下面的代码检索3个列,并按其中两个列对结果进行排序——首先按价格,然后再按名称排序。

SELECT prod_id,prod_price,prod_name
FROM products
ORDER BY prod_price,prod_name;

如果prod_price列中所有的值都是唯一的,则不会按prod_name排序。

数据排序不限于升序排序(从A到Z)。这只是默认的排序顺序,还可以降序(从Z到A)顺序排序。

DESC关键字只应用到直接位于其前面的列名。
在多个列上降序排序 如果想在多个列上进行降序排序,必须对每个列指定DESC关键字。

与DESC相反的关键字是ASC(ASCENDING),在升序排序时可以指定它。但实际上,ASC没有多大用处,因为升序是默认的。

使用ORDER BY和LIMIT的组合,能够找出一个列中最高或最低的值。

SELECT prod_price
FROM products
ORDER BY prod_price DESC
LIMIT 1;

结果:

----------
prod_price
----------
55.00
----------

第6章 过 滤 数 据

criteria 英/kraɪ’tɪəriə/

搜索条件 ( search criteria ) , 搜索条件也称为过滤条件( filter
condition)。

WHERE子句,它确定一个列是
否包含特定的值。MySQL支持表6-1列出的所有条件操作符。

操 作 符 说 明
= 等于
<> 不等于
!= 不等于
< 小于
<= 小于等于
> 大于
>= 大于等于
BETWEEN 在指定的两个值之间

MySQL在执行匹配时默认不区分大小写。
单引号用来限定字符串。如果将值与串类型的列进行比较,则需要限定引号。用来与数值列进行比较的值不用引号。

SELECT prod_name,prod_price FROM products WHERE prod_price BETWEEN 5 AND 10;
在使用BETWEEN时,必须指定两个值——所需范围的低端值和高端值。这两个值必须用AND关键字分隔。BETWEEN匹配范围中所有的值,包括指定的开始值和结束值。

6.2.4 空值检查

在创建表时,表设计人员可以指定其中的列是否可以不包含值。在一个列不包含值时,称其为包含空值NULL。

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

SELECT语句有一个特殊的WHERE子句,可用来检查具有NULL值的列。这个WHERE子句就是IS NULL子句。

第 7 章 数 据 过 滤

AND 用在WHERE子句中的关键字,用来指示检索满足所有给定条件的行。

一个关键字AND的语句,把两个过滤条件组合在一起。还可以添加多个过滤条件,每添加一条就要使用一个AND。

WHERE可包含任意数目的AND和OR操作符。允许两者结合以进行复杂和高级的过滤。

SQL(像多数语言一样)在处理OR操作符前,优先处理AND操作符。

在WHERE子句中使用圆括号 任何时候使用具有AND和OR操作符的WHERE子句,都应该使用圆括号明确地分组操作符。不要过分依赖默认计算次序,即使它确实是你想要的东西也是如此。使用圆括号没有什么坏处,它能消除歧义。

IN操作符用来指定条件范围,范围中的每个条件都可以进行匹配。IN取合法值的由逗号分隔的清单,全都括在圆括号中。

SELECT prod_name,prod_price 
FROM products 
WHERE vend_id IN (1002,1003)
ORDER BY prod_name;

IN操作符完成与OR相同的功能:

SELECT prod_name,prod_price 
FROM products 
WHERE vend_id = 1002 OR vend_id = 1003
ORDER BY prod_name;

为什么要使用IN操作符?其优点具体如下。

  1. 在使用长的合法选项清单时,IN操作符的语法更清楚且更直观。
  2. 在使用IN时,计算的次序更容易管理(因为使用的操作符更少)。
  3. IN操作符一般比OR操作符清单执行更快。
  4. IN的最大优点是可以包含其他SELECT语句,使得能够更动态地建立WHERE子句。

IN WHERE子句中用来指定要匹配值的清单的关键字,功能与OR相当。

NOT操作符有且只有一个功能,那就是否定它之后所跟的任何条件。

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

MySQL 支 持 使 用 NOT 对 IN 、 BETWEEN 和EXISTS子句取反,这与多数其他DBMS允许使用NOT对各种条件
取反有很大的差别。

第 8 章 用通配符进行过滤

8.1 LIKE操作符

通配符(wildcard) 用来匹配值的一部分的特殊字符。
搜索模式(search pattern)① 由字面值、通配符或两者组合构成的搜索条件。
为在搜索子句中使用通配符,必须使用LIKE操作符。LIKE指示MySQL,后跟的搜索模式利用通配符匹配而不是直接相等匹配进行比较。

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

8.1.1 百分号(%)通配符

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

找出所有以词jet起头的产品:

SELECT prod_id,prod_name FROM products WHERE prod_name LIKE 'jet%';

区分大小写 根据MySQL的配置方式,搜索可以是区分大小写的。如果区分大小写,'jet%'与JetPack 1000将不匹配。

通配符可在搜索模式中任意位置使用,并且可以使用多个通配符。

SELECT prod_id,prod_name FROM products WHERE prod_name LIKE '%anvil%';

通配符也可以出现在搜索模式的中间,虽然这样做不太有用。

找出以s起头以e结尾的所有产品:

SELECT prod_name FROM products WHERE prod_name LIKE 's%e';

除了一个或多个字符外,%还能匹配0个字符。%代表搜索模式中给定位置的0个、1个或多个字符。

注意尾空格 尾空格可能会干扰通配符匹配。例如,在保存词anvil 时 , 如 果 它 后 面 有 一 个 或 多 个 空 格 , 则 子 句 WHEREprod_name LIKE '%anvil’将不会匹配它们,因为在最后的l后有多余的字符。解决这个问题的一个简单的办法是在搜索模式最后附加一个%。一个更好的办法是使用函数(第11章将会介绍)去掉首尾空格。

注意NULL
虽然似乎%通配符可以匹配任何东西,但有一个例外,即NULL。即使是WHERE prod_name LIKE '%'也不能匹配用值NULL作为产品名的行。

8.1.2 下划线(_)通配符

下划线只匹配单个字符而不是多个字符。

SELECT prod_id,prod_name FROM products WHERE prod_name LIKE '_ ton anvil';

+--------------+-----------------+
prod_id           prod_name
+--------------+-----------------+
ANV01               .5 ton anvil
ANV02				1 ton anvil
ANV03               2 ton anvil
+--------------+-----------------+

与%能匹配0个字符不一样,_总是匹配一个字符,不能多也不能少。

使用通配符的技巧

MySQL的通配符很有用。但这种功能是有代价的:通配符搜索的处理一般要比前面讨论的其他搜索所花时间更长。

  1. 不要过度使用通配符。如果其他操作符能达到相同的目的,应该使用其他操作符。
  2. 在确实需要使用通配符时,除非绝对有必要,否则不要把它们用在搜索模式的开始处。把通配符置于搜索模式的开始处,搜索起来是最慢的。
  3. 仔细注意通配符的位置。如果放错地方,可能不会返回想要的数据。

通配符应该细心使用,不要过度使用。

第 9 章 用正则表达式进行搜索

正则表达式是用来匹配文本的特殊的串(字符集合)。如果你想从一个文本文件中提取电话号码,可以使用正则表达式。如果你需要查找名字中间有数字的所有文件,可以使用一个正则表达式。如果你想在一个文本块中找到所有重复的单词,可以使用一个正则表达式。如果你想替换一个页面中的所有URL为这些URL的实际HTML链接,也可以使用一个正则表达式(对于最后这个例子,或者是两个正则表达式)。

MySQL用WHERE子句对正则表达式提供了初步的支持,允许你指定正则表达式,过滤SELECT检索出的数据。

9.2.1 基本字符匹配

检索列prod_name包含文本1000的所有行:

SELECT prod_name
FROM products
WHERE prod_name REGEXP '1000' 
ORDER BY prod_name;

它告诉MySQL:REGEXP后所跟的东西作为正则表达式(与文字正文1000匹配的一个正则表达式)处理。

正则表达式确实没有带来太多好处(可能还会降低性能),不过,请考虑下面的例子:

SELECT prod_name
FROM products
WHERE prod_name REGEXP '.000' 
ORDER BY prod_name;

+--------------+
prod_name           
+--------------+
JetPack 1000  
JetPack 2000
+--------------+

这里使用了正则表达式.000。

.是正则表达式语言中一个特殊的字符。它表示匹配任意一个字符,因此,1000和2000都匹配且返回。

这个特殊的例子也可以用LIKE和通配符来完成。

LIKE与REGEXP

在LIKE和REGEXP之间有一个重要的差别。

SELECT prod_name
FROM products
WHERE prod_name LIKE '1000' 
ORDER BY prod_name;

SELECT prod_name
FROM products
WHERE prod_name REGEXP '1000' 
ORDER BY prod_name;

如果执行上述两条语句,会发现第一条语句不返回数据,而第二条语句返回一行。

  1. LIKE匹配整个列。如果被匹配的文本在列值中出现,LIKE将不会找到它,相应的行也不被返回(除非使用通配符)。而REGEXP在列值内进行匹配,如果被匹配的文本在列值中出现,REGEXP将会找到它,相应的行将被返回。这是一个非常重要的差别。
  2. REGEXP能不能用来匹配整个列值(从而起与LIKE相同的作用)?答案是肯定的,使用^和$定位符(anchor)即可。

匹配不区分大小写 MySQL中的正则表达式匹配(自版本3.23.4后)不区分大小写(即,大写和小写都匹配)。为区分大小写,可使用BINARY关键字,如

WHERE prod_name REGEXP BINARY 'JetPack .000';

9.2.2 进行OR匹配

为搜索两个串之一(或者为这个串,或者为另一个串),使用|,

SELECT prod_name
FROM products
WHERE prod_name REGEXP '1000|2000' 
ORDER BY prod_name;

+--------------+
prod_name           
+--------------+
JetPack 1000  
JetPack 2000
+--------------+

|为正则表达式的OR操作符。它表示匹配其中之一。

使用|从功能上类似于在SELECT语句中使用OR语句,多个OR条件可并入单个正则表达式。

9.2.3 匹配几个字符之一

匹配任何单一字符。但是,如果你只想匹配特定的字符,怎么办?可通过指定一组用**[和]**括起来的字符来完成。

SELECT prod_name
FROM products
WHERE prod_name REGEXP '[123] Ton' 
ORDER BY prod_name;

+--------------+
prod_name           
+--------------+
1 ton anvil  
2 ton anvil 
+--------------+

使用了正则表达式[123] Ton。[123]定义一组字符,它的意思是匹配1或2或3,因此,1 ton和2 ton都匹配且返回(没有3 ton)。

**[]是另一种形式的OR语句。**事实上,正则表达式[123]Ton为[1|2|3]Ton的缩写,也可以使用后者。

字符集合也可以被否定,即,它们将匹配除指定字符外的任何东西。为否定一个字符集,在集合的开始处放置一个即可。因此,[123]匹配字符1、2或3,但[123]却匹配除这些字符外的任何东西。

9.2.4 匹配范围

集合可用来定义要匹配的一个或多个字符。例如,下面的集合将匹配数字0到9:

[0123456789]

为简化这种类型的集合,可使用-来定义一个范围。下面的式子功能上等同于上述数字列表:

[0-9]

范围不限于完整的集合,[1-3]和[6-9]也是合法的范围。此外,范围不一定只是数值的,[a-z]匹配任意字母字符。

举一个例子:

SELECT prod_name
FROM products
WHERE prod_name REGEXP '[1-5] Ton' 
ORDER BY prod_name;

+--------------+
prod_name           
+--------------+
.5 ton anvil 
1 ton anvil  
2 ton anvil 
+--------------+

[1-5]定义了一个范围,这个表达式意思是匹配1到5,因此返回3个匹配行。由于5 ton匹配,所以返回.5 ton。

9.2.5 匹配特殊字符

正则表达式语言由具有特定含义的特殊字符构成。我们已经看到.、[]、|和-等,还有其他一些字符。

SELECT prod_name
FROM products
WHERE prod_name REGEXP '.' 
ORDER BY prod_name;

.匹配任意字符,因此每个行都被检索出来。

为了匹配特殊字符,必须用\为前导。\-表示查找-,\.表示查找.。

SELECT prod_name
FROM products
WHERE prod_name REGEXP '\\.' 
ORDER BY prod_name;

这才是期望的输出。

这种处理就是所谓的转义(escaping),正则表达式内具有特殊意义的所有字符都必须以这种方式转义。这包括.、|、[]以及迄今为止使用过的其他特殊字符。

\也用来引用元字符(具有特殊含义的字符),如表9-1所列。

表9-1 空白元字符

元 字 符 说 明
\\f 换页
\\n 换行
\\r 回车
\\t 制表
\\v 纵向制表

匹配\ 为了匹配反斜杠(\)字符本身,需要使用\\。

\或\?

多数正则表达式实现使用单个反斜杠转义特殊字符,以便能使用这些字符本身。但MySQL要求两个反斜杠(MySQL自己解释一个,正则表达式库解释另一个)。

9.2.6 匹配字符类

存在找出你自己经常使用的数字、所有字母字符或所有数字字母字符等的匹配。为更方便工作,可以使用预定义的字符集,称为字符类(character class)。

表9-2 字符类

+--------------------------------------------------------------
  类                说明
+--------------------------------------------------------------
[:alnum:] 任意字母和数字(同[a-zA-Z0-9])
[:alpha:] 任意字符(同[a-zA-Z])
[:blank:] 空格和制表(同[\\t])
[:cntrl:] ASCII控制字符(ASCII 0到31和127)
[:digit:] 任意数字(同[0-9])
[:graph:] 与[:print:]相同,但不包括空格
[:print:] 任意可打印字符
[:lower:] 任意小写字母(同[a-z])
[:upper:] 任意大写字母(同[A-Z])
[:punct:] 既不在[:alnum:]又不在[:cntrl:]中的任意字符
[:space:] 包括空格在内的任意空白字符(同[\\f\\n\\r\\t\\v])
[:xdigit:] 任意十六进制数字(同[a-fA-F0-9])
+--------------------------------------------------------------

9.2.7 匹配多个实例

有时需要对匹配的数目进行更强的控制。例如,你可能需要寻找所有的数,不管数中包含多少数字,或者你可能想寻找一个单词并且还能够适应一个尾随的s(如果存在),等等。

可以用表9-3列出的正则表达式重复元字符来完成。

表9-3 重复元字符

+--------------------------------------------------------------
  元字符        说明
+--------------------------------------------------------------
 *          0个或多个匹配
 +          1个或多个匹配(等于{1,})
 ?          0个或1个匹配(等于{0,1})
 {n}        指定数目的匹配
 {n,}       不少于指定数目的匹配
 {n,m}      匹配数目的范围(m不超过255)
+--------------------------------------------------------------

举几个例子:

SELECT prod_name
FROM products
WHERE prod_name REGEXP '\\([0-9] sticks?\\)'
ORDER BY prod_name;

正则表达式\([0-9] sticks?\)需要解说一下。\(匹配(,[0-9]匹配任意数字(这个例子中为1和5),sticks?匹配stick和sticks(s后的?使s可选,因为?匹配它前面的任何字符的0次或1次出现),\)匹配)。没有?,匹配stick和sticks会非常困难。

匹配连在一起的4位数字:

SELECT prod_name
FROM products
WHERE prod_name REGEXP '[[:digit:]]{4}'
ORDER BY prod_name;

如前所述,[:digit:]匹配任意数字,因而它为数字的一个集合。{4}确切地要求它前面的字符(任意数字)出现4次,所以[[:digit:]]{4}匹配连在一起的任意4位数字。

在使用正则表达式时,编写某个特殊的表达式几乎总是有不止一种方法。上面的例子也可以如下编写:

SELECT prod_name
FROM products
WHERE prod_name REGEXP '[0-9][0-9][0-9][0-9]'
ORDER BY prod_name;

9.2.8 定位符

目前为止的所有例子都是匹配一个串中任意位置的文本。为了匹配特定位置的文本,需要使用表9-4列出的定位符。

表9-4 定位元字符

+--------------------------------------------------------------
  元字符        说明
+--------------------------------------------------------------
 ^          文本的开始
 $          文本的结尾
 [[:<:]]    词的开始
 [[:>:]]    词的结尾
+--------------------------------------------------------------

想找出以一个数(包括以小数点开始的数)开始的所有产品,怎么办?

简单搜索[0-9\.](或[[:digit:]\.])不行,因为它将在文本内任意位置查找匹配。解决办法是使用^定位符,如下所示:

SELECT prod_name 
FROM products 
WHERE prod_name REGEXP '^[0-9\\.]'
ORDER BY prod_name;

+--------------+
prod_name           
+--------------+
.5 ton anvil 
1 ton anvil  
2 ton anvil 
+--------------+

匹配串的开始。因此,[0-9\.]只在.或任意数字为串中第一个字符时才匹配它们。
^的双重用途 ^有两种用法。在集合中(用[和]定义),用它来否定该集合,否则,用来指串的开始处。
[123]匹配字符1、2或3,但[^123]却匹配除这些字符外的任何东西。

使REGEXP起类似LIKE的作用 本章前面说过,LIKE和REGEXP的不同在于,LIKE匹配整个串而REGEXP匹配子串。利用定位符,通过用^开始每个表达式,用$结束每个表达式,可以使REGEXP的作用与LIKE一样。

简单的正则表达式测试

可以在不使用数据库表的情况下用SELECT来测试正则表达式。REGEXP检查总是返回0(没有匹配)或1(匹配)。可以用带文字串的REGEXP来测试表达式,并试
验它们。相应的语法如下:

SELECT 'hello' REGEXP '[0-9]';

这个例子显然将返回0(因为文本hello中没有数字)。

第 10 章 创建计算字段

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

拼接(concatenate) 将值联结到一起构成单个值。

在MySQL的SELECT语句中,可使用Concat()函数来拼接两个列。

MySQL的不同之处多数DBMS使用+或||来实现拼接,MySQL则使用Concat()函数来实现。当把SQL语句转换成MySQL语句时一定要把这个区别铭记在心。

SELECT Concat(vend_name,' (',vend_country,')')
FROM vendors
ORDER BY vend_name;

输出:

+----------------------------------+
Concat(vend_name,' (',vend_country,')')           
+----------------------------------+
ACME (USA)
Anvils R US (USA)  
Furball Inc. (USA) 
Jet Set (England)
Jouets Et Ours (France)
LT Suppliers (USA)
+----------------------------------+

Concat()拼接串,即把多个串连接起来形成一个较长的串。

Concat()需要一个或多个指定的串,各个串之间用逗号分隔。

上面的SELECT语句连接以下4个元素:

  1. 存储在vend_name列中的名字;
  2. 包含一个空格和一个左圆括号的串;
  3. 存储在vend_country列中的国家;
  4. 包含一个右圆括号的串。

从上述输出中可以看到,SELECT语句返回包含上述4个元素的单个列**(计算字段)**。

删除数据右侧多余的空格来整理数据,这可以使用MySQL的RTrim()函数来完成,如下所示:

SELECT Concat(RTrim(vend_name),' (',RTrim(vend_country),')')
FROM vendors
ORDER BY vend_name;

RTrim()函数去掉值右边的所有空格。通过使用RTrim(),各个列都进行了整理。

Trim函数 MySQL除了支持RTrim()(去掉串右边的空格),还支持LTrim()(去掉串左边的空格)以及Trim()(去掉串左右两边的空格)

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

SELECT Concat(vend_name,' (',vend_country,')') AS vend_title
FROM vendors
ORDER BY vend_name;

+----------------------------------+
vend_title           
+----------------------------------+
ACME (USA)
Anvils R US (USA)  
Furball Inc. (USA) 
Jet Set (England)
Jouets Et Ours (France)
LT Suppliers (USA)
+----------------------------------+

现在列名为vend_title,任何客户机应用都可以按名引用这个列,就像它是一个实际的表列一样。

别名的其他用途 别名还有其他用途。常见的用途包括在实际的表列名包含不符合规定的字符(如空格)时重新命名它,在原来的名字含混或容易误解时扩充它,等等。

导出列 别名有时也称为导出列(derived column),不管称为什么,它们所代表的都是相同的东西。

10.3 执行算术计算

举一个例子,orders表包含收到的所有订单,orderitems表包含每个订单中的各项物品。下面的SQL语句检索订单号20005中的所有物品:

SELECT prod_id,quantity,item_price 
FROM orderitems
WHERE order_num=20005;

输出:

+-------+---------+-----------+
prod_id   quantity  item_price 
+-------+---------+-----------+
ANV01     10        5.99
ANV02      3        9.99
TNT2       5        10.00
FB         1        10.00
+-------+---------+-----------+

如下汇总物品的价格(单价乘以订购数量):

SELECT prod_id,quantity,item_price,quantity*item_price AS expanded_price 
FROM orderitems
WHERE order_num=20005;

expanded_price列为一个计算字段。

基本算术操作符:
+ - * /
加减乘除

圆括号可用来区分优先顺序。

如何测试计算

SELECT提供了测试和试验函数与计算的一个很好的办法。虽然SELECT通常用来从表中检索数据,但可以省略FROM子句以便简单地访问和处理表达式。例如,SELECT 3*2;将返回6,SELECT Trim(‘abc’);将返回abc,而SELECT Now()利用Now()函数返回当前日期和时间。通过这些例子,可以明白如何根据需要使用SELECT进行试验。

第 11 章 使用数据处理函数

本章介绍什么是函数,MySQL支持何种函数,以及如何使用这些函数。

11.1 函数

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

  1. 用于处理文本串(如删除或填充值,转换值为大写或小写)的文本函数。
  2. 用于在数值数据上进行算术操作(如返回绝对值,进行代数运算)的数值函数。
  3. 用于处理日期和时间值并从这些值中提取特定成分(例如,返回两个日期之差,检查日期有效性等)的日期和时间函数。
  4. 返回DBMS正使用的特殊信息(如返回用户登录信息,检查版本细节)的系统函数。

11.2.1 文本处理函数

使用RTrim()函数来去除列值右边的空格。Upper()将文本转换为大写。

SELECT vend_name,Upper(vend_name) AS vend_name_upcase 
FROM vendors 
ORDER BY vend_name;

输出:

+-------------+-----------------------+
vend_name      vend_name_upcase  
+-------------+-----------------------+
ACME            ACME 
Anvils R Us     ANVILS R US  
Furball Inc.    FURBALL INC.
Jet Set         JET SET
Jouets Et Ours  JOUETS ET OURS
LT Suppliers    LT SUPPLIERS
+-------------+-----------------------+

表11-1 常用的文本处理函数

+-------------+-----------------------+
 函   数          说明  
+-------------+-----------------------+
Left()            返回串左边的字符 
Length()          返回串的长度 
Locate()          找出串的一个子串
Lower()           将串转换为小写
LTrim()           去掉串左边的空格
Right()           返回串右边的字符
RTrim             去掉串右边的空格
Soundex()         返回串的SOUNDEX值
SubString()       返回子串的字符
Upper()           将串转换为大写
+-------------+-----------------------+

SOUNDEX需要做进一步的解释。
SOUNDEX是一个将任何文本串转换为描述其语音表示的字母数字模式的算法。SOUNDEX考虑了类似的发音字符和音节,使得能对串进行发音比较而不是字母比较。

下面给出一个使用Soundex()函数的例子。

customers表中有一个顾客Coyote Inc.,其联系名为Y.Lee。但假如这是输入错误,假如此联系名实际应该是Y.Lie,怎么办?显然,按正确的联系名搜索不会返回数据,如下所示:

SELECT cust_name,cust_contact 
FROM customers 
WHERE cust_contact = 'Y.Lie';

输出:

+-------------+-----------------------+
cust_name       cust_contact  
+-------------+-----------------------+

在试一下使用Soundex()函数进行搜索,它匹配所有发音类似于Y.Lie的联系名:

SELECT cust_name,cust_contact 
FROM customers 
WHERE Soundex(cust_contact) = Soundex('Y.Lie');

输出:

+-------------+-----------------------+
cust_name       cust_contact  
+-------------+-----------------------+
Coyote Inc.      Y Lee 
+-------------+-----------------------+

在这个例子中,WHERE子句使用Soundex()函数来转换cust_contact 列 值 和 搜 索 串 为 它 们 的 SOUNDEX 值 。 因 为 Y.Lee 和Y.Lie发音相似,所以它们的SOUNDEX值匹配,因此WHERE子句正确地过滤出了所需的数据。

11.2.2 日期和时间处理函数

日期和时间采用相应的数据类型和特殊的格式存储,以便能快速和有效地排序或过滤,并且节省物理存储空间。

一般,应用程序不使用用来存储日期和时间的格式,因此日期和时间函数总是被用来读取、统计和处理这些值。由于这个原因,日期和时间函数在MySQL语言中具有重要的作用。

表11-2 常用日期和时间处理函数

+-------------+-----------------------+
 函   数          说明  
+-------------+-----------------------+
AddDate()       增加一个日期(天、周等) 
AddTime()       增加一个时间(时、分等) 
CurDate()       返回当前日期
CurTime()       返回当前时间
Date()          返回日期时间的日期部分
DateDiff()      计算两个日期之差
Date_Add()      高度灵活的日期运算函数
Date_Format()   返回一个格式化的日期或时间串
Day()      		返回一个日期的天数部分
DayOfWeek()     对于一个日期,返回对应的星期几
Hour()          返回一个时间的小时部分
Minute()  		返回一个时间的分钟部分
Mouth()  		返回一个日期的月份部分
Now()  			返回当前日期和时间
Second()  		返回一个时间的秒部分
Time()  		返回一个日期时间的时间部分
Year()  		返回一个日期的年份部
+-------------+-----------------------+

用日期进行过滤需要注意一些别的问题和使用特殊的MySQL函数。
首先需要注意的是MySQL使用的日期格式。无论你什么时候指定一个日期,不管是插入或更新表值还是用WHERE子句进行过滤,日期必须为格式yyyy-mm-dd。

因此,2005年9月1日,给出为2005-09-01。虽然其他的日期格式可能也行,但这是首选的日期格式,因为它排除了多义性(如,04/05/06是2006年5月4日或2006年4月5日或2004年5月6日或……)

应该总是使用4位数字的年份 支持2位数字的年份,MySQL处理00-69为2000-2069,处理70-99为1970-1999。虽然它们可能是打算要的年份,但使用完整的4位数字年份更可靠,因为MySQL不必做出任何假定。

基本的日期比较应该很简单:

SELECT cust_id,order_num 
FROM orders
WHERE order_date = '2005-09-01';

但是,使用WHERE order_date = '2005-09-01’可靠吗?
order_date的数据类型为datetime。这种类型存储日期及时间值。样例表中的值全都具有时间值00:00:00,但实际中很可能并不总是这样。如果用当前日期和时间存储订单日期(因此你不仅知道订单日期,还知道下订单当天 的时间),怎么办?比如,存储的order_date值为2005-09-01 11:30:05,则WHERE order_date = 2005-09-01’失败。即使给出具有该日期的一行,也不会把它检索出来,因为WHERE匹配失败。

解决办法是指示MySQL仅将给出的日期与列中的日期部分进行比较,而不是将给出的日期与整个列值进行比较。为此,必须使用Date()函数。Date(order_date)指示MySQL仅提取列的日期部分,更可靠的SELECT语句为:

SELECT cust_id,order_num 
FROM orders
WHERE Date(order_date) = '2005-09-01';

如果你想检索出2005年9月下的所有订单,怎么办?简单的相等测试不行,因为它也要匹配月份中的天数。有几种解决办法,其中之一如下所示:

SELECT cust_id,order_num 
FROM orders
WHERE Date(order_date) BETWEEN '2005-09-01' AND '2005-09-30';

还有另外一种办法(一种不需要记住每个月中有多少天或不需要操心闰年2月的办法):

SELECT cust_id,order_num 
FROM orders
WHERE Year(order_date) =2005 AND Month(order_date) = 9;

another:

SELECT cust_id,order_num 
FROM orders
WHERE order_date LIKE '2005-09-%';

11.2.3 数值处理函数

数值处理函数仅处理数值数据。这些函数一般主要用于代数、三角或几何运算,因此没有串或日期—时间处理函数的使用那么频繁。

在主要DBMS的函数中,数值函数是最一致最统一的函数。

表11-3 常用数值处理函数

+-------------+-----------------------+
 函   数          说明  
+-------------+-----------------------+
Abs()       返回一个数的绝对值 
Cos()       返回一个角度的余弦 
Exp()       返回一个数的指数值
Mod()       返回除操作的余数
Pi()        返回圆周率
Rand()      返回一个随机数
Sin()       返回一个角度的正弦
Sqrt()   	返回一个数的平方根
Tan()      	返回一个角度的正切
+-------------+-----------------------+

第 12 章 汇 总 数 据

12.1 聚集函数

这种类型的检索例子有以下几种:

  1. 确定表中行数(或者满足某个条件或包含某个特定值的行数)。
  2. 获得表中行组的和。
  3. 找出表列(或所有行或某些特定的行)的最大值、最小值和平均值。

上述例子都需要对表中数据(而不是实际数据本身)汇总。因此,返回实际表数据是对时间和处理资源的一种浪费(更不用说带宽了)。重复一遍,实际想要的是汇总信息。

聚集函数(aggregate function) 运行在行组上,计算和返回单个值的函数。

为方便这种类型的检索,MySQL给出了5个聚集函数,见表12-1。

+-------------+-----------------------+
 函   数          说明  
+-------------+-----------------------+
AVG()          返回某列的平均值 
COUNT()        返回某列的行数 
MAX()          返回某列的最大值
MIN()          返回某列的最小值
SUM()          返回某列值之和
+-------------+-----------------------+

**AVG()**可用来返回所有列的平均值,也可以用来返回特定列或行的平均值。

SELECT AVG(prod_price) AS avg_price FROM products;
SELECT AVG(prod_price) AS avg_price FROM products WHERE vend_id = 1003;

只用于单个列 AVG()只能用来确定特定数值列的平均值,而且列名必须作为函数参数给出。为了获得多个列的平均值,必须使用多个AVG()函数。

NULL值 AVG()函数忽略列值为NULL的行。

COUNT()函数进行计数。可利用COUNT()确定表中行的数目或符合特定条件的行的数目。

COUNT()函数有两种使用方式:

  1. 使用COUNT(*)对表中行的数目进行计数,不管表列中包含的是空值(NULL)还是非空值。
  2. 使用COUNT(column)对特定列中具有值的行进行计数,忽略NULL值。

返回customers表中客户的总数:

SELECT COUNT(*) AS num_cust FROM customers;

此例子中,利用COUNT(*)对所有行计数,不管行中各列有什么值。

只对具有电子邮件地址的客户计数:

SELECT COUNT(cust_email) AS num_cust FROM customers;

这条SELECT语句使用COUNT(cust_email)对cust_email列中有值的行进行计数。

NULL值 如果指定列名,则指定列的值为空的行被COUNT()函数忽略,但如果COUNT()函数中用的是星号(*),则不忽略。

MAX()返回指定列中的最大值。MAX()要求指定列名。

NULL值 MAX()函数忽略列值为NULL的行。

MIN()返回指定列的最小值。与MAX()一样,MIN()要求指定列名。

NULL值 MIN()函数忽略列值为NULL的行。

SUM()用来返回指定列值的和(总计)。

如下检索所订购物品的总数(所有quantity值之和):

SELECT SUM(quantity) AS items_ordered FROM orderitems WHERE order_num = 20005;

SUM()也可以用来合计计算值。在下面的例子中,合计每项物品的item_price*quantity,得出总的订单金额:

SELECT SUM(item_price*quantity) AS total_price FROM orderitems WHERE order_num = 20005;

NULL值 SUM()函数忽略列值为NULL的行。

12.2 聚集不同值

聚集函数的DISTINCT的使用,已经被添加到MySQL 5.0.3中。下面所述内容在MySQL 4.x中不能正常运行。

以上5个聚集函数都可以如下使用:

  1. 对所有的行执行计算,指定ALL参数或不给参数(因为ALL是默认行为);
  2. 只包含不同的值,指定DISTINCT参数。

下面的例子使用AVG()函数返回特定供应商提供的产品的平均价格。

SELECT AVG(DISTINCT prod_price) AS avg_price FROM products WHERE vend_id = 1003;

注意 如果指定列名,则DISTINCT只能用于COUNT()。DISTINCT不能用于COUNT(*),因此不允许使用COUNT(DISTINCT),否则会产生错误 。类似地,DISTINCT必须使用列名,不能用于计算或表达式。

将DISTINCT用于MIN()和MAX() 虽然DISTINCT从技术上可用于MIN()和MAX(),但这样做实际上没有价值。一个列中的最小值和最大值不管是否包含不同值都是相同的。

12.3 组合聚集函数

SELECT语句可根据需要包含多个聚集函数。

SELECT COUNT(*) AS num_items,
	   MIN(prod_price) AS price_min,
	   MAX(prod_price) AS price_max,
	   AVG(prod_price) AS price_avg
FROM products;

取别名 在指定别名以包含某个聚集函数的结果时,不应该使用表中实际的列名。虽然这样做并非不合法,但使用唯一的名字会使你的SQL更易于理解和使用(以及将来容易排除故障)

第 13 章 分 组 数 据

如何分组数据,以便能汇总表内容的子集。这涉及两个新SELECT语句子句,分别是GROUP BY子句和HAVING子句。

目前为止的所有计算都是在表的所有数据或匹配特定的WHERE子句的数据上进行的。提示一下,下面的例子返回供应商1003提供的产品数目:

SELECT COUNT(*) AS num_prods
FROM products 
WHERE vend_id = 1003;

但如果要返回每个供应商提供的产品数目怎么办?或者返回只提供单项产品的供应商所提供的产品,或返回提供10个以上产品的供应商怎么办?这就是分组显身手的时候了。分组允许把数据分为多个逻辑组,以便能对每个组进行聚集计算。

13.2 创建分组

分组是在SELECT语句的GROUP BY子句中建立的。

SELECT vend_id,COUNT(*) AS num_prods
FROM products 
GROUP BY vend_id;

上面的SELECT语句指定了两个列,vend_id包含产品供应商的ID,num_prods为计算字段(用COUNT(*)函数建立)。GROUP BY子句指示MySQL按vend_id排序并分组数据。这导致对每个vend_id而不是整个表计算num_prods一次。

因为使用了GROUP BY,就不必指定要计算和估值的每个组了。系统会自动完成。GROUP BY子句指示MySQL分组数据,然后对每个组而不是整个结果集进行聚集。

在具体使用GROUP BY子句前,需要知道一些重要的规定。

  1. GROUP BY子句可以包含任意数目的列。这使得能对分组进行嵌套,为数据分组提供更细致的控制。
  2. 如果在GROUP BY子句中嵌套了分组,数据将在最后规定的分组上进行汇总。换句话说,在建立分组时,指定的所有列都一起计算(所以不能从个别的列取回数据)。
  3. GROUP BY子句中列出的每个列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在SELECT中使用表达式,则必须在GROUP BY子句中指定相同的表达式。不能使用别名。
  4. 除聚集计算语句外,SELECT语句中的每个列都必须在GROUP BY子句中给出。
  5. 如果分组列中具有NULL值,则NULL将作为一个分组返回。如果列中有多行NULL值,它们将分为一组。
  6. GROUP BY子句必须出现在WHERE子句之后,ORDER BY子句之前。

使用ROLLUP
使用WITH ROLLUP关键字,可以得到每个分组以及每个分组汇总级别(针对每个分组)的值,如下所示:

SELECT vend_id,COUNT(*) AS num_prods
FROM products 
GROUP BY vend_id WITH ROLLUP;

13.3 过滤分组

除了能用GROUP BY分组数据外,MySQL还允许过滤分组,规定包括哪些分组,排除哪些分组。
例如,可能想要列出至少有两个订单的所有顾客。为得出这种数据,必须基于完整的分组而不是个别的行进行过滤。

我们已经看到了WHERE子句的作用(第6章中引入)。但是,在这个例子中WHERE不能完成任务,因为WHERE过滤指定的是行而不是分组。事实上,WHERE没有分组的概念。

那么,不使用WHERE使用什么呢?MySQL为此目的提供了另外的子句,那就是HAVING子句。HAVING非常类似于WHERE。事实上,目前为止所学过的所有类型的WHERE子句都可以用HAVING来替代。唯一的差别是WHERE过滤行,而HAVING过滤分组。

HAVING支持所有WHERE操作符
在第6章和第7章中,我们学习了WHERE子句的条件(包括通配符条件和带多个操作符的子句)。所学过的有关WHERE的所有这些技术和选项都适用于HAVING。它们的句法是相同的,只是关键字有差别。

怎么过滤分组呢?请看以下的例子:

SELECT cust_id,COUNT(*) AS orders
FROM orders 
GROUP BY cust_id 
HAVING COUNT(*) >= 2;

这里WHERE子句不起作用,因为过滤是基于分组聚集值而不是特定行值的。

HAVING和WHERE的差别
这里有另一种理解方法,WHERE在数据分组前进行过滤,HAVING在数据分组后进行过滤。这是一个重要的区别,WHERE排除的行不包括在分组中。这可能会改变计算值,从而影响HAVING子句中基于这些值过滤掉的分组。

同时使用WHERE和HAVING子句的需要,请看下面的例子,它列出具有2个(含)以上、价格为10(含)以上的产品的供应商:

SELECT vend_id,COUNT(*) AS num_prods
FROM products
WHERE prod_price >= 10 
GROUP BY vend_id 
HAVING COUNT(*) >= 2;

输出:

+-------------+-----------------------+
vend_id         num_prods  
+-------------+-----------------------+
1003            4
1005            2 
+-------------+-----------------------+

这条语句中,第一行是使用了聚集函数的基本SELECT,它与前面的例子很相像。WHERE子句过滤所有prod_price至少为10的行。然后按vend_id分组数据,HAVING子句过滤计数为2或2以上的分组。如果没有WHERE子句,将会多检索出两行(供应商1002,销售的所有产品价格都在10以下;供应商1001,销售3个产品,但只有一个产品的价格大于等于10):

SELECT vend_id,COUNT(*) AS num_prods
FROM products
GROUP BY vend_id 
HAVING COUNT(*) >= 2;

输出:

+-------------+-----------------------+
vend_id         num_prods  
+-------------+-----------------------+
1001            3
1002            2
1003            7
1005            2 
+-------------+-----------------------+

13.4 分组和排序

GROUP BY和ORDER BY经常完成相同的工作,但它们是非常不同的。表13-1汇总了它们之间的差别。

表13-1 ORDER BY与GROUP BY

+---------------------------+-------------------------------------+
 ORDER BY                     GROUP BY  
+---------------------------+-------------------------------------+
排序产生的输出                分组行。但输出可能不是分组的顺序 
任意列都可以使用(甚至        只可能使用选择列或表达式列,而且必须使用每个选择
非选择的列也可以使用)			列表达式
不一定需要                    如果与聚集函数一起使用列(或表达式),则必须使用    
+---------------------------+-------------------------------------+

表13-1中列出的第一项差别极为重要。我们经常发现用GROUP BY分组的数据确实是以分组顺序输出的。但情况并不总是这样,它并不是SQL
规范所要求的。此外,用户也可能会要求以不同于分组的顺序排序。仅因为你以某种方式分组数据(获得特定的分组聚集值),并不表示你需要
以相同的方式排序输出。应该提供明确的ORDER BY子句,即使其效果等同于GROUP BY子句也是如此。

不要忘记ORDER BY

**一般在使用GROUP BY子句时,应该也给出ORDER BY子句。**这是保证数据正确排序的唯一方法。千万不要仅依赖GROUP BY排序数据。

下面的SELECT语句类似于前面那些例子。它检索总计订单价格大于等于50的订单的订单号和总计订单价格:

SELECT order_num,SUM(quantity * item_price) AS ordertotal
FROM orderitems 
GROUP BY order_num
HAVING SUM(quantity * item_price) >= 50;

输出:

+-------------+-----------------------+
order_num       ordertotal 
+-------------+-----------------------+
20005          149.87
20006          55.00
20007          1000.00
20008          125.00
+-------------+-----------------------+

按总计订单价格排序输出,需要添加ORDER BY子句,如下所示:

SELECT order_num,SUM(quantity * item_price) AS ordertotal
FROM orderitems 
GROUP BY order_num
HAVING SUM(quantity * item_price) >= 50
ORDER BY ordertotal;

输出:

+-------------+-----------------------+
order_num       ordertotal 
+-------------+-----------------------+
20006          55.00
20008          125.00
20005          149.87
20007          1000.00
+-------------+-----------------------+

在这个例子中,GROUP BY子句用来按订单号(order_num列)分组数据,以便SUM(*)函数能够返回总计订单价格。HAVING子句过滤数据,使得只返回总计订单价格大于等于50的订单。最后,用ORDER BY子句排序输出。

13.5 SELECT子句顺序

表13-2 SELECT子句及其顺序

+----------------+---------------------------+-----------------------+
子 句                说 明                       是否必须使用
+----------------+---------------------------+-----------------------+
SELECT             要返回的列或表达式          是
FROM               从中检索数据的表            仅在从表选择数据时使用
WHERE              行级过滤                    否
GROUP BY           分组说明                    仅在按组计算聚集时使用
HAVING             组级过滤                    否
ORDER BY           输出排序顺序                否
LIMIT              要检索的行数                否
+----------------+---------------------------+-----------------------+

第 14 章 使用子查询

14.1 子查询

迄今为止我们所看到的所有SELECT语句都是简单查询,即从单个数据库表中检索数据的单条语句。

SQL还允许创建子查询(subquery),即嵌套在其他查询中的查询。

14.2 利用子查询进行过滤

订单存储在两个表中。对于包含订单号、客户ID、订单日期的每个订单,orders表存储一行。各订单的物品存储在相关的orderitems表中。orders表不存储客户信息。它只存储客户的ID。实际的客户信息存储在customers表中。

现在,假如需要列出订购物品TNT2的所有客户,应该怎样检索?下面列出具体的步骤。

  1. 检索包含物品TNT2的所有订单的编号。
  2. 检索具有前一步骤列出的订单编号的所有客户的ID。
  3. 检索前一步骤返回的所有客户ID的客户信息。

上述每个步骤都可以单独作为一个查询来执行。可以把一条SELECT语句返回的结果用于另一条SELECT语句的WHERE子句。

也可以使用子查询来把3个查询组合成一条语句。

第一条SELECT语句的含义很明确,对于prod_id为TNT2的所有订单物品,它检索其order_num列。输出列出两个包含此物品的订单。

SELECT order_num
FROM orderitems
WHERE prod_id = 'TNT2';

输出:

+-------------+
order_num      
+-------------+       
20005          
20007          
+-------------+

下一步,查询具有订单20005和20007的客户ID,利用第7章介绍的IN子句,编写如下的SELECT语句:

SELECT cust_id
FROM orders
WHERE order_num IN (20005,20007);

输出:

+-------------+
cust_id      
+-------------+       
10001          
10004          
+-------------+

现在,把第一个查询(返回订单号的那一个)变为子查询组合两个查询。

SELECT cust_id
FROM orders
WHERE order_num IN (SELECT order_num
					FROM orderitems
					WHERE prod_id = 'TNT2');

输出:

+-------------+
cust_id      
+-------------+       
10001          
10004          
+-------------+

在SELECT语句中,子查询总是从内向外处理。

格式化SQL 包含子查询的SELECT语句难以阅读和调试,特别是它们较为复杂时更是如此。如上所示把子查询分解为多行并且适当地进行缩进,能极大地简化子查询的使用。

现在得到了订购物品TNT2的所有客户的ID。下一步是检索这些客户ID的客户信息。检索两列的SQL语句为:

SELECT cust_name,cust_contact 
FROM customers 
WHERE cust_id IN (10001,10004);

可以把其中的WHERE子句转换为子查询而不是硬编码这些客户ID:

SELECT cust_name,cust_contact 
FROM customers 
WHERE cust_id IN (	SELECT cust_id
					FROM orders
					WHERE order_num IN (SELECT order_num
										FROM orderitems
										WHERE prod_id = 'TNT2'));

输出:

+-------------+-----------------------+
cust_name      cust_contact
+-------------+-----------------------+
Coyote Inc.     Y Lee
Yosemite Place  Y Sam
+-------------+-----------------------+

为了执行上述SELECT语句,MySQL实际上必须执行3条SELECT语句。最里边的子查询返回订单号列表,此列表用于其外面的子查询的WHERE子句。外面的子查询返回客户ID列表,此客户ID列表用于最外层查询的WHERE子句。最外层查询确实返回所需的数据。

可见,在WHERE子句中使用子查询能够编写出功能很强并且很灵活的SQL语句。对于能嵌套的子查询的数目没有限制,不过在实际使用时由于性能的限制,不能嵌套太多的子查询。

虽然子查询一般与IN操作符结合使用,但也可以用于测试等于(=)、不等于(<>)等。

子查询和性能 这里给出的代码有效并获得所需的结果。但是,使用子查询并不总是执行这种类型的数据检索的最有效的方法。(联结表)

14.3 作为计算字段使用子查询

使用子查询的另一方法是创建计算字段。假如需要显示customers表中每个客户的订单总数。订单与相应的客户ID存储在orders表中。

为了执行这个操作,遵循下面的步骤。

  1. 从customers表中检索客户列表。
  2. 对于检索出的每个客户,统计其在orders表中的订单数目。

可使用SELECT COUNT(*)对表中的行进行计数,并且通过提供一条WHERE子句来过滤某个特定的客户ID,可仅对该客户的订单进行计数。例如,下面的代码对客户10001的订单进行计数:

SELECT COUNT(*) AS orders
FROM orders
WHERE cust_id = 10001;

为了对每个客户执行COUNT()计算,应该将COUNT()作为一个子查询。请看下面的代码:

SELECT	cust_name,
		cust_state,
		(SELECT COUNT(*)
		FROM orders
		WHERE orders.cust_id = customers.cust_id AS orders)
FROM customers 
ORDER BY cust_name;

输出:

+-------------+------------+-----------+
cust_name      cust_state    orders
+-------------+-----------------------+
Coyote Inc.     MI          2
E Fudd          IL          1
Mouse House     Oh          0
Wascals         IN          1
Yosemite Place  AZ          1
+-------------+-----------------------+

orders是一个计算字段,它是由圆括号中的子查询建立的。该子查询对检索出的每个客户执行一次。在此例子中,该子查询执行了5次,因为检索出了5个客户。

子查询中的WHERE子句与前面使用的WHERE子句稍有不同,因为它使用了完全限定列名(在第4章中首次提到)。下面的语句告诉SQL比较orders表中的cust_id与当前正从customers表中检索的cust_id:

WHERE orders.cust_id = customers.cust_id AS orders

相关子查询(correlated subquery) 涉及外部查询的子查询。

这种类型的子查询称为相关子查询。任何时候只要列名可能有多义性,就必须使用这种语法(表名和列名由一个句点分隔)。为什么这样?我们来看看如果不使用完全限定的列名会发生什么情况:

SELECT	cust_name,
		cust_state,
		(SELECT COUNT(*)
		FROM orders
		WHERE cust_id = cust_id AS orders)
FROM customers 
ORDER BY cust_name;

输出:

+-------------+------------+-----------+
cust_name      cust_state    orders
+-------------+-----------------------+
Coyote Inc.     MI          5
E Fudd          IL          5
Mouse House     Oh          5
Wascals         IN          5
Yosemite Place  AZ          5
+-------------+-----------------------+

显然,返回的结果不正确(请比较前面的结果),那么,为什么会这样呢?有两个cust_id列,一个在customers中,另一个在orders中,需要比较这两个列以正确地把订单与它们相应的顾客匹配。如果不完全限定列名,MySQL将假定你是对orders表中的cust_id进行自身比较。
而SELECT COUNT(*) FROM orders WHERE cust_id = cust_id;总是返回orders表中的订单总数(因为MySQL查看每个订单的cust_id是否与本身匹配,当然,它们总是匹配的)。

逐渐增加子查询来建立查询

用子查询测试和调试查询很有技巧性,特别是在这些语句的复杂性不断增加的情况下更是如此。用子查询建立(和测试)查询的最可靠的方法是逐渐进行,这与MySQL处理它们的方法非常相同。首先,建立和测试最内层的查询。然后,用硬编码数据建立和测试外层查询,并且仅在确认它正常后才嵌入子查询。这时,再次测试它。对于要增加的每个查询,重复这些步骤。这样做仅给构造查询增加了一点点时间,但节省了以后(找出查询为什么不正常)的大量时间,并且极大地提高了查询一开始就正常工作的可能性。

第 15 章 联 结 表

15.1 联结

SQL最强大的功能之一就是能在数据检索查询的执行中联结(join)表。

15.1.1 关系表

理解关系表的最好方法是来看一个现实世界中的例子。

假如有一个包含产品目录的数据库表,其中每种类别的物品占一行。对于每种物品要存储的信息包括产品描述和价格,以及生产该产品的供应商信息。

现在,假如有由同一供应商生产的多种物品,那么在何处存储供应商信息(如,供应商名、地址、联系方法等)呢?将这些数据与产品信息分开存储的理由如下。

  1. 因为同一供应商生产的每个产品的供应商信息都是相同的,对每个产品重复此信息既浪费时间又浪费存储空间。
  2. 如果供应商信息改变(例如,供应商搬家或电话号码变动),只需改动一次即可。
  3. 如果有重复数据(即每种产品都存储供应商信息),很难保证每次输入该数据的方式都相同。不一致的数据在报表中很难利用。

关键是,相同数据出现多次决不是一件好事,此因素是关系数据库设计的基础。关系表的设计就是要保证把信息分解成多个表,一类数据一个表。各表通过某些常用的值(即关系设计中的关系(relational))互相关联。

在这个例子中,可建立两个表,一个存储供应商信息,另一个存储产品信息。vendors表包含所有供应商信息,每个供应商占一行,每个供应商具有唯一的标识。此标识称为主键(primary key)(在第1章中首次提到),可以是供应商ID或任何其他唯一值。products表只存储产品信息,它除了存储供应商ID(vendors表的主键)外不存储其他供应商信息。vendors表的主键又叫作products的外键,它将vendors表与products表关联,利用供应商ID能从vendors表中找出相应供应商的详细信息。

外键(foreign key) 外键为某个表中的一列,它包含另一个表的主键值,定义了两个表之间的关系。

这样做的好处如下:

  1. 供应商信息不重复,从而不浪费时间和空间;
  2. 如果供应商信息变动,可以只更新vendors表中的单个记录,相关表中的数据不用改动;
  3. 由于数据无重复,显然数据是一致的,这使得处理数据更简单。
    总之,关系数据可以有效地存储和方便地处理。因此,关系数据库的可伸缩性远比非关系数据库要好。

可伸缩性(scale) 能够适应不断增加的工作量而不失败。设计良好的数据库或应用程序称之为可伸缩性好(scale well)

15.1.2 为什么要使用联结

分解数据为多个表能更有效地存储,更方便地处理,并且具有更大的可伸缩性。但这些好处是有代价的。

如果数据存储在多个表中,怎样用单条SELECT语句检索出数据?

答案是使用联结。简单地说,联结是一种机制,用来在一条SELECT语句中关联表,因此称之为联结。使用特殊的语法,可以联结多个表返回一组输出,联结在运行时关联表中正确的行。

**联结不是物理实体。**换句话说,它在实际的数据库表中不存在。联结由MySQL根据需要建立,它存在于查询的执行当中。

15.2 创建联结

联结的创建非常简单,规定要联结的所有表以及它们如何关联即可。请看下面的例子:

SELECT vend_name,prod_name,prod_price 
FROM vendors,products 
WHERE vendors.vend_id = products.vend_id
ORDER BY vend_name,product_name;

ELECT语句与前面所有语句一样指定
要检索的列。这里,最大的差别是所指定的两个列(prod_name和prod_price)在一个表中,而另一个列(vend_name)在另一个表中。现在来看FROM子句。与以前的SELECT语句不一样,这条语句的FROM子句列出了两个表,分别是vendors和products。它们就是这条SELECT语句联结的两个表的名字。这两个表用WHERE子句正确联结,WHERE子句
指示MySQL匹配vendors表中的vend_id和products表中的vend_id。

可 以 看 到 要 匹 配 的 两 个 列 以 vendors.vend_id 和 products.vend_id指定。这里需要这种完全限定列名,因为如果只给出vend_id,则MySQL不知道指的是哪一个(它们有两个,每个表中一个)。

完全限定列名 在引用的列可能出现二义性时,必须使用完全限定列名(用一个点分隔的表名和列名)。如果引用一个没有用表名限制的具有二义性的列名,MySQL将返回错误。

15.2.1 WHERE子句的重要性

请记住,在一条SELECT语句中联结几个表时,相应的关系是在运行中构造的。在数据库表的定义中不存在能指示MySQL如何对表进行联结的东西。你必须自己做这件事情。在联结两个表时,你实际上做的是将第一个表中的每一行与第二个表中的每一行配对。WHERE子句作为过滤条件,它只包含那些匹配给定条件(这里是联结条件)的行。没有WHERE子句,第一个表中的每个行将与第二个表中的每个行配对,而不管它们逻辑上是否可以配在一起。

笛卡儿积(cartesian product)

由没有联结条件的表关系返回的结果为笛卡儿积。检索出的行的数目将是第一个表中的行数乘以第二个表中的行数。

不要忘了WHERE子句 应该保证所有联结都有WHERE子句,否则MySQL将返回比想要的数据多得多的数据。同理,应该保证WHERE子句的正确性。不正确的过滤条件将导致MySQL返回
不正确的数据。

叉联结 有时我们会听到返回称为叉联结(cross join)的笛卡儿积的联结类型。

15.2.2 内部联结

目前为止所用的联结称为**等值联结(equijoin),它基于两个表之间的相等测试。这种联结也称为内部联结。**其实,对于这种联结可以使用稍微不同的语法来明确指定联结的类型。下面的SELECT语句返回与前面例子完全相同的数据:

SELECT vend_name,prod_name,prod_price 
FROM vendors INNER JOIN products 
ON vendors.vend_id = products.vend_id;

此语句中的SELECT与前面的SELECT语句相同,但FROM子句不同。这里,两个表之间的关系是FROM子句的组成部分,以INNER JOIN指定。在使用这种语法时,联结条件用特定的ON子句而不是WHERE子句给出。传递给ON的实际条件与传递给WHERE的相同。

使用哪种语法 ANSI SQL规范首选INNER JOIN语法。此外,尽管使用WHERE子句定义联结的确比较简单,但是使用明确的联结语法能够确保不会忘记联结条件,有时候这样做也能影响性能。

15.2.3 联结多个表

SQL对一条SELECT语句中可以联结的表的数目没有限制。创建联结的基本规则也相同。首先列出所有表,然后定义表之间的关系。例如:

SELECT prod_name,vend_name,prod_price,quantity 
FROM orderitems,products,vendors 
WHERE vendors.vend_id = products.vend_id 
	AND orderitems.prod_id = products.prod_id 
	AND order_num = 20005;

输出:

+----------------+--------------+------------+--------------+
prod_name         vend_name      prod_price    quantity
+----------------+--------------+------------+--------------+
.5 ton anvil      Anvils R US          5.99         10
1 ton anvil       Anvils R US           9.99         3
TNT (5 sticks)    ACME                 10.00         5
Bird seed         ACME                 10.00         1
+----------------+--------------+------------+--------------+

此例子显示编号为20005的订单中的物品。订单物品存储在orderitems表中。每个产品按其产品ID存储,它引用products表中的产品。这些产品通过供应商ID联结到vendors表中相应的供应商,供应商ID存储在每个产品的记录中。这里的FROM子句列出了3个表,而WHERE子句定义了这两个联结条件,而第三个联结条件用来过滤出订单20005中的物品。

性能考虑 MySQL在运行时关联指定的每个表以处理联结。这种处理可能是非常耗费资源的,因此应该仔细,不要联结不必要的表。联结的表越多,性能下降越厉害。

现在可以回顾一下第14章中的例子了。该例子如下所示,其SELECT语句返回订购产品TNT2的客户列表:

SELECT cust_name,cust_contact
FROM customers 
WHERE cust_id IN (SELECT cust_id 
				  FROM orders 
				  WHERE order_num IN(SELECT order_num 
									FROM orderitems 
									WHERE prod_id = 'TNT2'));

正如第14章所述,子查询并不总是执行复杂SELECT操作的最有效的方法,下面是使用联结的相同查询:

SELECT cust_name,cust_contact
FROM customers,orders,orderitems 
WHERE  customers.cust_id = orders.cust_id
	AND orderitems.order_num = orders.order_num
	AND prod_id = 'TNT2';

输出:

+-------------+-----------------------+
cust_name      cust_contact
+-------------+-----------------------+
Coyote Inc.     Y Lee
Yosemite Place  Y Sam
+-------------+-----------------------+

正如第14章所述,这个查询中返回数据需要使用3个表。但这里我们没有在嵌套子查询中使用它们,而是使用了两个联结。这里有3个WHERE子句条件。前两个关联联结中的表,后一个过滤产品TNT2的数据。

多做实验
为执行任一给定的SQL操作,一般存在不止一种方法。很少有绝对正确或绝对错误的方法。性能可能会受操作类型、表中数据量、是否存在索引或键以及其他一些条件的影响。因此,有必要对不同的选择机制进行实验,以找出最适合具体情况的方法。

第 16 章 创建高级联结

16.1 使用表别名

给列起别名的语法如下:

SELECT Concat(Rtrim(vend_name),'(',Rtrim(vend_country),')') AS vend_title 
FROM vendors 
ORDER BY vend_name;

别名除了用于列名和计算字段外,SQL还允许给表名起别名。这样做有两个主要理由:

  1. 缩短SQL语句;

  2. 允许在单条SELECT语句中多次使用相同的表。

    SELECT cust_name,cust_contact
    FROM customers AS c, orders AS o, ordertimes AS oi
    WHERE c.cust_id = o.cust_id
    AND oi.order_num = o.order_num
    AND prod_id = ‘TNT2’;
    以看到,FROM子句中3个表全都具有别名。在此例子中,表别名只用于WHERE子句。但是,表别名不仅能用于WHERE子句,它还可以用于SELECT的列表、ORDER BY子句以及语句的其他部分。
    应该注意,表别名只在查询执行中使用。与列别名不一样,表别名不返回到客户机。

16.2 使用不同类型的联结

迄今为止,我们使用的只是称为内部联结或等值联结(equijoin)的简单联结。现在来看3种其他联结,它们分别是自联结、自然联结和外部联结。

16.2.1 自联结

使用表别名的主要原因之一是能在单条SELECT语句中不止一次引用相同的表。

下面举一个例子。

假如你发现某物品(其ID为DTNTR)存在问题,因此想知道生产该物品的供应商生产的其他物品是否也存在这些问题。此查询要求首先找到生产ID为DTNTR的物品的供应商,然后找出这个供应商生产的其他物品。
下面是解决此问题的一种方法:

SELECT prod_id,prod_name 
FROM products 
WHERE vend_id = (SELECT vend_id 
				FROM products 
				WHERE prod_id = 'DTNTR');

输出:

+-------------+-----------------------+
 prod_id        prod_name
+-------------+-----------------------+
DTNTR          Detonator
FB             Bird seed
FC             Carrots
SAFE           Safe
SLING          Sling
TNT1           TNT(1 stick)
TNT2           TNT(5 sticks)
+-------------+-----------------------+

这是第一种解决方案,它使用了子查询。

内部的SELECT语句做了一个简单的检索 ,返回生产ID为 DTNTR 的物品供应商的vend_id。该ID用于外部查询的WHERE子句中,以便检索出这个供应商生
产的所有物品。

现在来看使用联结的相同查询:

SELECT p1.prod_id,p1.prod_name 
FROM products AS p1,products AS p2
WHERE p1.vend_id = p2.vend_id
AND p2.prod_id = 'DTNTR';

输出:

+-------------+-----------------------+
 prod_id        prod_name
+-------------+-----------------------+
DTNTR          Detonator
FB             Bird seed
FC             Carrots
SAFE           Safe
SLING          Sling
TNT1           TNT(1 stick)
TNT2           TNT(5 sticks)
+-------------+-----------------------+

此查询中需要的两个表实际上是相同的表,因此products表在FROM子句中出现了两次。虽然这是完全合法的,但对products的引用具有二义性,因为MySQL不知道你引用的是products表中的哪个实例。

为解决此问题,使用了表别名。products的第一次出现为别名p1,第二次出现为别名p2。现在可以将这些别名用作表名。例如,SELECT语句使用p1前缀明确地给出所需列的全名。如果不这样,MySQL将返回错误,因为分别存在两个名为prod_id、prod_name的列。MySQL不知道想要的是哪一个列(即使它们事实上是同一个列)。WHERE(通过匹配p1中的vend_id和p2中的vend_id)首先联结两个表,然后按第二个表中的prod_id过滤数据,返回所需的数据。

用自联结而不用子查询

自联结通常作为外部语句用来替代从相同表中检索数据时使用的子查询语句。虽然最终的结果是相同的,但有时候处理联结远比处理子查询快得多。应该试一下两种方法,以确定哪一种的性能更好。

16.2.2 自然联结

无论何时对表进行联结,应该至少有一个列出现在不止一个表中(被联结的列)

自然联结是这样一种联结,其中你只能选择那些唯一的列。这一般是通过对表使用通配符(SELECT *),对所有其他表的列使用明确的子集来完成的。下面举一个例子:

SELECT c.*,o.ordernum,o.order_date, oi.prod_id, oi.quantity,OI.item_price
FROM customers AS c,orders AS o,orderitems AS oi
WHERE c.cust_id = o.cust_id
	AND oi.order_num = o.order_num
	AND prod_id = 'FB';

在这个例子中,通配符只对第一个表使用。所有其他列明确列出,所以没有重复的列被检索出来。

迄今为止我们建立的每个内部联结都是自然联结,很可能我们永远都不会用到不是自然联结的内部联结。

16.2.3 外部联结

许多联结将一个表中的行与另一个表中的行相关联。但有时候会需要包含没有关联行的那些行。例如,可能需要使用联结来完成以下工作:

  1. 对每个客户下了多少订单进行计数,包括那些至今尚未下订单的客户;
  2. 列出所有产品以及订购数量,包括没有人订购的产品;
  3. 计算平均销售规模,包括那些至今尚未下订单的客户。

在上述例子中,联结包含了那些在相关表中没有关联行的行。这种类型的联结称为外部联结

下面的SELECT语句给出一个简单的内部联结。它检索所有客户及其订单:

SELECT customers.cust_id,orders.order_num
FROM customers INNER JOIN orders
ON customers.cust_id = orders.cust_id;

外部联结语法类似。为了检索所有客户,包括那些没有订单的客户,可如下进行:

SELECT customers.cust_id,orders.order_num
FROM customers LEFT OUTER JOIN orders
ON customers.cust_id = orders.cust_id;

输出:

+-------------+-----------------------+
 cust_id        order_num
+-------------+-----------------------+
10001          20005
10001          20009
10002          NULL
10003          20006
10004          20007
10005          20008
+-------------+-----------------------+

类似于上一章中所看到的内部联结,这条SELECT语句使用了关键字OUTER JOIN来指定联结的类型(而不是在WHERE子句中指定)。但是,与内部联结关联两个表中的行不同的是,外部联结还包括没有关联行的行。在使用OUTER JOIN语法时,必须使用RIGHT或LEFT关键字指定包括其所有行的表(RIGHT指出的是OUTER JOIN右边的表,而LEFT指出的是OUTER JOIN左边的表)。上面的例子使用LEFT OUTER JOIN从FROM子句的左边表(customers表)中选择所有行。

为了从右边的表中选择所有行,应该使用RIGHT OUTER JOIN,如下例所示:

SELECT customers.cust_id,order.order_num 
FROM customers RIGHT OUTER JOIN orders 
ON orders.cust_id = customers.cust_id;

没有=操作符 **
MySQL不支持简化字符
=和=*的使用,这两种操作符在其他DBMS中是很流行的。

外部联结的类型 存在两种基本的外部联结形式:左外部联结和右外部联结。它们之间的唯一差别是所关联的表的顺序不同。换句话说,左外部联结可通过颠倒FROM或WHERE子句中表的顺序转换为右外部联结。因此,两种类型的外部联结可互换使用,而究竟使用哪一种纯粹是根据方便而定。

16.3 使用带聚集函数的联结

聚集函数用来汇总数据。虽然至今为止聚集函数的所有例子只是从单个表汇总数据,但这些函数也可以与联结一起使用。

为说明这一点,请看一个例子。如果要检索所有客户及每个客户所下的订单数,下面使用了COUNT()函数的代码可完成此工作:

SELECT customers.cust_name,customers.cust_id,COUNT(orders.order_num) AS num_ord		
FROM customers INNER JOIN orders
	ON customers.cust_id = orders.cust_id 
GROUP BY customers.cust_id;

输出:

+----------------+--------------+------------+
cust_name         cust_id,       num_ord    
+----------------+--------------+------------+
Coyote Inc        10001           2
Wascals           10003           1
Yosemite Place    10004           1
E Fudd            10005           1
+----------------+--------------+------------+

此SELECT语句使用INNER JOIN将customers和orders表互相关联。GROUP BY 子 句 按 客 户 分 组 数 据 , 因 此 , 函 数 调 用 COUNT(orders.order_num)对每个客户的订单计数,将它作为num_ord返回。

聚集函数也可以方便地与其他联结一起使用。请看下面的例子:

SELECT customers.cust_name,customers.cust_id,COUNT(orders.order_num) AS num_ord		
FROM customers LEFT OUTER JOIN orders
	ON customers.cust_id = orders.cust_id 
GROUP BY customers.cust_id;

输出:

+----------------+--------------+------------+
cust_name         cust_id,       num_ord    
+----------------+--------------+------------+
Coyote Inc        10001           2
Mouse House       10002           0
Wascals           10003           1
Yosemite Place    10004           1
E Fudd            10005           1
+----------------+--------------+------------+

这个例子使用左外部联结来包含所有客户,甚至包含那些没有任何下订单的客户。结果显示也包含了客户Mouse House,它有0个订单。

16.4 使用联结和联结条件

汇总一下关于联结及其使用的某些要点。

  1. 注意所使用的联结类型。一般我们使用内部联结,但使用外部联结也是有效的。
  2. 保证使用正确的联结条件,否则将返回不正确的数据。
  3. 应该总是提供联结条件,否则会得出笛卡儿积。
  4. 在一个联结中可以包含多个表,甚至对于每个联结可以采用不同的联结类型。虽然这样做是合法的,一般也很有用,但应该在一起测试它们前,分别测试每个联结。这将使故障排除更为简单。

第 17 章 组 合 查 询

利用UNION操作符将多条SELECT语句组合成一个结果集。

17.1 组合查询

多数SQL查询都只包含从一个或多个表中返回数据的单条SELECT语句。MySQL也允许执行多个查询(多条SELECT语句),并将结果作为单个查询结果集返回。这些组合查询通常称为并( union )或复合查询(compound query)。

有两种基本情况,其中需要使用组合查询:

  1. 在单个查询中从不同的表返回类似结构的数据;
  2. 对单个表执行多个查询,按单个查询返回数据。

**组合查询和多个WHERE条件 **
多数情况下,组合相同表的两个查询完成的工作与具有多个WHERE子句条件的单条查询完成的工作相同。换句话说,任何具有多个WHERE子句的SELECT语句都可以作为一个组合查询给出,在以下段落中可以看到这一点。
这两种技术在不同的查询中性能也不同。因此,应该试一下这两种技术,以确定对特定的查询哪一种性能更好。

17.2 创建组合查询

利用UNION,可给出多条SELECT语句,将它们的结果组合成单个结果集。

17.2.1 使用UNION

UNION的使用很简单。所需做的只是给出每条SELECT语句,在各条语句之间放上关键字UNION。

举一个例子,假如需要价格小于等于5的所有物品的一个列表,而且还想包括供应商1001和1002生产的所有物品(不考虑价格)。当然,可以利用WHERE子句来完成此工作,不过这次我们将使用UNION。

正如所述,创建UNION涉及编写多条SELECT语句。首先来看单条语句:

SELECT vend_id,prod_id,prod_price 
FROM products
WHERE prod_price <= 5;

输出:

+----------------+--------------+------------+
vend_id           prod_id        prod_price    
+----------------+--------------+------------+
1003              FC             2.50
1002              FU1            3.42
1003              SLING          4.49
1003              TNT1           2.50
+----------------+--------------+------------+

输入

SELECT vend_id,prod_id,prod_price 
FROM products
WHERE vend_id IN (1001,1002);

输出:

+----------------+--------------+------------+
vend_id           prod_id        prod_price    
+----------------+--------------+------------+
1001              ANV01          5.99
1001              ANV02          9.99
1001              ANV03          14.99
1002              FU1            3.42
1002              OL1            8.99
+----------------+--------------+------------+

第一条SELECT检索价格不高于5的所有物品。第二条SELECT使用IN找出供应商1001和1002生产的所有物品。

为了组合这两条语句,按如下进行:

SELECT vend_id,prod_id,prod_price 
FROM products
WHERE prod_price <= 5 
UNION
SELECT vend_id,prod_id,prod_price 
FROM products
WHERE vend_id IN (1001,1002);

输出:

+----------------+--------------+------------+
vend_id           prod_id        prod_price    
+----------------+--------------+------------+
1003              FC             2.50
1002              FU1            3.42
1003              SLING          4.49
1003              TNT1           2.50
1001              ANV01          5.99
1001              ANV02          9.99
1001              ANV03          14.99
1002              OL1            8.99
+----------------+--------------+------------+

UNION指示MySQL执行两条SELECT语句,并把输出组合成单个查询结果集。

作为参考,这里给出使用多条WHERE子句而不是使用UNION的相同查询:

SELECT vend_id,prod_id,prod_price 
FROM products
WHERE prod_price <= 5 
	OR vend_id IN (1001,1002);

在这个简单的例子中,使用UNION可能比使用WHERE子句更为复杂。但对于更复杂的过滤条件,或者从多个表(而不是单个表)中检索数据的情形,使用UNION可能会使处理更简单。

17.2.2 UNION规则

并是非常容易使用的。但在进行并时有几条规则需要注意。

  1. UNION必须由两条或两条以上的SELECT语句组成,语句之间用关键字UNION分隔(因此,如果组合4条SELECT语句,将要使用3个UNION关键字)。
  2. UNION中的每个查询必须包含相同的列、表达式或聚集函数(不过各个列不需要以相同的次序列出)。
  3. 列数据类型必须兼容:类型不必完全相同,但必须是DBMS可以隐含地转换的类型(例如,不同的数值类型或不同的日期类型)。

17.2.3 包含或取消重复的行

UNION从查询结果集中自动去除了重复的行(换句话说,它的行为与单条SELECT语句中使用多个WHERE子句条件一样)。因为供应商1002生产的一种物品的价格也低于5,所以两条SELECT语句都返回该行。在使用UNION时,重复的行被自动取消。

这是UNION的默认行为,但是如果需要,可以改变它。

如果想返回所有匹配行,可使用UNION ALL而不是UNION。

SELECT vend_id,prod_id,prod_price 
FROM products
WHERE prod_price <= 5 
UNION ALL 
SELECT vend_id,prod_id,prod_price 
FROM products
WHERE vend_id IN (1001,1002);

输出:

+----------------+--------------+------------+
vend_id           prod_id        prod_price    
+----------------+--------------+------------+
1003              FC             2.50
1002              FU1            3.42
1003              SLING          4.49
1003              TNT1           2.50
1001              ANV01          5.99
1001              ANV02          9.99
1001              ANV03          14.99
1002              FU1            3.42
1002              OL1            8.99
+----------------+--------------+------------+

使用UNION ALL,MySQL不取消重复的行。

**UNION与WHERE **
本章开始时说过,UNION几乎总是完成与多个WHERE条件相同的工作。UNION ALL为UNION的一种形式,它完成
WHERE子句完成不了的工作。如果确实需要每个条件的匹配行全部出现(包括重复行),则必须使用UNION ALL而不是WHERE。

17.2.4 对组合查询结果排序

SELECT语句的输出用ORDER BY子句排序。**在用UNION组合查询时,只能使用一条ORDER BY子句,**它必须出现在最后一条SELECT语句之后。对于结果集,不存在用一种方式排序一部分,而又用另一种方式排序另一部分的情况,因此不允许使用多条ORDER BY子句。

SELECT vend_id,prod_id,prod_price 
FROM products
WHERE prod_price <= 5 
UNION 
SELECT vend_id,prod_id,prod_price 
FROM products
WHERE vend_id IN (1001,1002)
ORDER BY vend_id,prod_price;

输出:

+----------------+--------------+------------+
vend_id           prod_id        prod_price    
+----------------+--------------+------------+
1001              ANV01          5.99
1001              ANV02          9.99
1001              ANV03          14.99
1002              FU1            3.42
1002              OL1            8.99
1003              TNT1           2.50
1003              FC             2.50
1003              SLING          4.49
+----------------+--------------+------------+

UNION在最后一条SELECT语句后使用了ORDER BY子句。虽然ORDER BY子句似乎只是最后一条SELECT语句的组成部分,但实际上MySQL将用它来排序所有SELECT语句返回的所有结果。

利用UNION,可把多条查询的结果作为一条组合查询返回,不管它们的结果中包含还是不包含重复。使用UNION可极大地简化复杂的WHERE子句,简化从多个表中检索数据的工作。

第 18 章 全文本搜索

使用MySQL的全文本搜索功能进行高级的数据查询和选择。

18.1 理解全文本搜索

并非所有引擎都支持全文本搜索 正如第21章所述,MySQL支持几种基本的数据库引擎。并非所有的引擎都支持本书所描述的全文本搜索。两个最常使用的引擎为MyISAM和InnoDB,前者支持全文本搜索,而后者不支持。这就是为什么虽然本书中 创 建 的 多 数 样 例 表 使 用 InnoDB , 而 有 一 个 样 例 表(productnotes表)却使用MyISAM的原因。

第8章介绍了LIKE关键字,它利用通配操作符匹配文本(和部分文本)。使用LIKE,能够查找包含特殊值或部分值的行(不管这些值位于列内什么位置)。

在第9章中,用基于文本的搜索作为正则表达式匹配列值的更进一步的介绍。使用正则表达式,可以编写查找所需行的非常复杂的匹配模式。

虽然这些搜索机制非常有用,但存在几个重要的限制。

  1. 性能——通配符和正则表达式匹配通常要求MySQL尝试匹配表中所有行(而且这些搜索极少使用表索引)。因此,由于被搜索行数不断增加,这些搜索可能非常耗时。
  2. 明确控制——使用通配符和正则表达式匹配,很难(而且并不总是能)明确地控制匹配什么和不匹配什么。例如,指定一个词必须匹配,一个词必须不匹配,而一个词仅在第一个词确实匹配的情况下才可以匹配或者才可以不匹配。
  3. 智能化的结果——虽然基于通配符和正则表达式的搜索提供了非常灵活的搜索,但它们都不能提供一种智能化的选择结果的方法。例如,一个特殊词的搜索将会返回包含该词的所有行,而不区分包含单个匹配的行和包含多个匹配的行(按照可能是更好的匹配来排列它们)。类似,一个特殊词的搜索将不会找出不包含该词但包含其他相关词的行。

所有这些限制以及更多的限制都可以用全文本搜索来解决。

18.2 使用全文本搜索

为了进行全文本搜索,必须索引被搜索的列,而且要随着数据的改变不断地重新索引。在对表列进行适当设计后,MySQL会自动进行所有的索引和重新索引。

在索引之后,SELECT可与Match()和Against()一起使用以实际执行搜索。

18.2.1 启用全文本搜索支持

一般在创建表时启用全文本搜索。CREATE TABLE语句(第21章中介绍)接受FULLTEXT子句,它给出被索引列的一个逗号分隔的列表。

下面的CREATE语句演示了FULLTEXT子句的使用:

CREATE TABLE productnotes (
note_id    int      NOT NULL AUTO_INCREMENT,
prod_id    char(10) NOT NULL,
note_date  datetime NOT NULL,
note_text  text     NULL,
PRIMARY KEY(note_id),
FULLTEXT(note_text),
)ENGINE=MyISAM;

这些列中有一个名为note_text的列,为了进行全文本搜索,MySQL根据子句FULLTEXT(note_text)的指示对它进行索引。这里的FULLTEXT索引单个列,如果需要也可以指定多个列。

在定义之后,MySQL自动维护该索引。在增加、更新或删除行时,索引随之自动更新。
可以在创建表时指定FULLTEXT,或者在稍后指定(在这种情况下所有已有数据必须立即索引)。

**不要在导入数据时使用FULLTEXT **
更新索引要花时间,虽然不是很多,但毕竟要花时间。如果正在导入数据到一个新表,此时不应该启用FULLTEXT索引。应该首先导入所有数据,然后再修改表,定义FULLTEXT。这样有助于更快地导入数据(而且使索引数据的总时间小于在导入每行时分别进行索引所需的总时间)。

18.2.2 进行全文本搜索

在索引之后,使用两个函数Match()和Against()执行全文本搜索,其中Match()指定被搜索的列,Against()指定要使用的搜索表达式。

SELECT note_text
FROM productnotes
WHERE Match(note_text) Against('rabbit');

输出:

+--------------------------------------------------------------------------------------+
note_text      
+--------------------------------------------------------------------------------------+     
Customer complaint:rabbit has able to detect trap,food apparently less  effective now.
Quantaty varies,sold by the sack load. All guaranteed to be bright and orange,and suitable for use as rabbit bait.         
+--------------------------------------------------------------------------------------+

此SELECT语句检索单个列note_text。由于WHERE子句,一个全文本搜索被执行。Match(note_text)指示MySQL针对指定的列进行搜索,Against(‘rabbit’)指定词rabbit作为搜索文本。

由于有两行包含词rabbit,这两个行被返回。

使用完整的Match()说明

传递给Match()的值必须与FULLTEXT()定义中的相同。如果指定多个列,则必须列出它们(而且次序正确)。

搜索不区分大小写

除非使用BINARY方式(本章中没有介绍),否则全文本搜索不区分大小写。

刚才的搜索可以简单地用LIKE子句完成,如下所示:

SELECT note_text
FROM productnotes
WHERE note_text LIKE '%rabbit%';

输出:

+--------------------------------------------------------------------------------------+
note_text      
+--------------------------------------------------------------------------------------+     
Customer complaint:rabbit has able to detect trap,food apparently less  effective now.
Quantaty varies,sold by the sack load. All guaranteed to be bright and orange,and suitable for use as rabbit bait.         
+--------------------------------------------------------------------------------------+

这条SELECT语句同样检索出两行,但次序不同(虽然并不总是出现这种情况)。

全文本搜索的一个重要部分就是对结果排序。具有较高等级的行先返回(因为这些行很可能是你真正想要的行)。
为演示排序如何工作,请看以下例子:

SELECT note_text,
		Match(note_text) Against('rabbit') AS rank;
FROM productnotes

输出:

+----------------------------------------------+------------+
note_text                                       rank    
+----------------------------------------------+------------+
Customer complaint:rabbit has able to detect    1.640805
trap,food apparently less  effective now.

Matcher not include,recommend purchase of        0
matches or detonator (item DTNTR).

Quantaty varies,sold by the sack load. All 
guaranteed to be bright and orange,and          1.59055
suitable for use as rabbit bait.

Call from individual trapped in safe plummeting
to the ground,suggests an escape hatch be        0
added,Comment forwarded to vendor.

...                                             ...
+----------------------------------------------+------------+

在SELECT而不是WHERE子句中使用Match()和Against()。这使所有行都被返回(因为没有WHERE子句)。Match()和Against()
用来建立一个计算列(别名为rank),此列包含全文本搜索计算出的等级值。等级由MySQL根据行中词的数目、唯一词的数目、整个索引中词的总数以及包含该词的行的数目计算出来。

正如所见,不包含词rabbit的行等级为0(因此不被前一例子中的WHERE子句选择)。确实包含词rabbit的两个行每行都有一个等级值,文本中词靠前的行的等级值比词靠后的行的等级值高。

这个例子有助于说明全文本搜索如何排除行(排除那些等级为0的行),如何排序结果(按等级以降序排序)。

排序多个搜索项
如果指定多个搜索项,则包含多数匹配词的那些行将具有比包含较少词(或仅有一个匹配)的那些行高的等级值。

全文本搜索提供了简单LIKE搜索不能提供的功能。而且,由于数据是索引的,全文本搜索还相当快。

18.2.3 使用查询扩展

查询扩展用来设法放宽所返回的全文本搜索结果的范围。考虑下面的情况。你想找出所有提到anvils的注释。只有一个注释包含词anvils,但你还想找出可能与你的搜索有关的所有其他行,即使它们不包含词anvils。这也是查询扩展的一项任务。

在使用查询扩展时,MySQL对数据和索引进行两遍扫描来完成搜索:

  1. 首先,进行一个基本的全文本搜索,找出与搜索条件匹配的所有行;
  2. 其次,MySQL检查这些匹配行并选择所有有用的词(我们将会简要地解释MySQL如何断定什么有用,什么无用)。
  3. 再其次,MySQL再次进行全文本搜索,这次不仅使用原来的条件,而且还使用所有有用的词。
    利用查询扩展,能找出可能相关的结果,即使它们并不精确包含所查找的词。

下面举一个例子,首先进行一个简单的全文本搜索,没有查询扩展:

SELECT note_text
FROM productnotes
WHERE Match(note_text) Against('rabbit');

输出:

+-------------------------------------------------------+
note_text                                       
+-------------------------------------------------------+
Multiple customer returens,anvils failing to drop 
fast enough or falling backwards on purchaser. Recomend 
that customer considers using heavier anvils.
+-------------------------------------------------------+

只有一行包含词anvils,因此只返回一行。

下面是相同的搜索,这次使用查询扩展:

SELECT note_text 
FROM productnotes
WHERE Match(note_text) Against('anvils' WITH QUERY EXPANSION);

输出:

+------------------------------------------------------------------+
note_text                                       
+------------------------------------------------------------------+
Multiple customer returens,anvils failing to drop fast 
enough or falling backwards on purchaser. Recomend that 
customer considers using heavier anvils.
Customer complaint:Sticks not individually wrapped,too easy
to mistakenly detonate all at once.Recommend individual 
wrapping.
Customer complaint:Not heavy enough to generate flying stars
around headof victim.If being purchased for dropping,
recommend ANV02 or ANV03 instead.
Please note that no returns will be accepted if safe opened using
explosives.
Customer complaint:rabbit has been able to detect trap,food
apparently less effective now.
Customer complaint:Circular hole in safe floor can apparently
be easily cut with handsaw.
Matches not inculded,recommend purchase of matches or detonator
(item DTNTR).
+------------------------------------------------------------------+

这次返回了7行。第一行包含词anvils,因此等级最高。第二行与anvils无关,但因为它包含第一行中的两个词(customer和recommend),所以也被检索出来。第3行也包含这两个相同的词,但它们在文本中的位置更靠后且分开得更远,因此也包含这一行,但等级为第三。第三行确实也没有涉及anvils(按它们的产品名)。

查询扩展极大地增加了返回的行数,但这样做也增加了你实际上并不想要的行的数目。

行越多越好

表中的行越多(这些行中的文本就越多),使用查询扩展返回的结果越好。

18.2.4 布尔文本搜索

MySQL支持全文本搜索的另外一种形式,称为布尔方式(boolean mode)。以布尔方式,可以提供关于如下内容的细节:

  1. 要匹配的词;
  2. 要排斥的词(如果某行包含这个词,则不返回该行,即使它包含
  3. 其他指定的词也是如此);
  4. 排列提示(指定某些词比其他词更重要,更重要的词等级更高);
  5. 表达式分组;
  6. 另外一些内容。

即使没有FULLTEXT索引也可以使用 布尔方式不同于迄今为止使用的全文本搜索语法的地方在于,即使没有定义FULLTEXT索引,也可以使用它。但这是一种非常缓慢的操作(其性能将随着数据量的增加而降低)。

为演示IN BOOLEAN MODE的作用,举一个简单的例子:

SELECT note_text 
FROM productnotes
WHERE Match(note_text) Against('heavy' IN BOOLEAN MODE);

输出:

+------------------------------------------------------------------+
note_text                                       
+------------------------------------------------------------------+
Item is extremely heavy.Designed for dropping,not recommended for use
with sling,ropes,pulleys,or tightropes.
Customer complaint:Not heavy enough to generate flying stars
around headof victim.If being purchased for dropping,
recommend ANV02 or ANV03 instead.
+------------------------------------------------------------------+

此全文本搜索检索包含词heavy的所有行(有两行)。其中使用了关键字IN BOOLEAN MODE,但实际上没有指定布尔操作符,因此,其结果与没有指定布尔方式的结果相同。

为了匹配包含heavy但不包含任意以rope开始的词的行,可使用以下查询:

SELECT note_text 
FROM productnotes
WHERE Match(note_text) Against('heavy -rope*' IN BOOLEAN MODE);

输出:

+------------------------------------------------------------------+
note_text                                       
+------------------------------------------------------------------+
Customer complaint:Not heavy enough to generate flying stars
around headof victim.If being purchased for dropping,
recommend ANV02 or ANV03 instead.
+------------------------------------------------------------------+

这次只返回一行。这一次仍然匹配词heavy,但-rope明确地指示MySQL排除包含rope (任何以 rope 开始 的词,包括ropes)的行。

我们已经看到了两个全文本搜索布尔操作符-和*,-排除一个词,而*是截断操作符(可想象为用于词尾的一个通配符)。表18-1列出支持的所有布尔操作符。

			表18-1 全文本布尔操作符
+---------+----------------------------------------------------------+
布尔操作符           说 明
+---------+----------------------------------------------------------+
	+       包含,词必须存在
	-       排除,词必须不出现
	>       包含,而且增加等级值
	<       包含,且减少等级值
	()      把词组成子表达式(允许这些子表达式作为一个组被包含、排除、排列等)
	~       取消一个词的排序值
	*       词尾的通配符
	""      定义一个短语(与单个词的列表不一样,它匹配整个短语以便包含或排除这个短语)
+---------+----------------------------------------------------------+

下面举几个例子,说明某些操作符如何使用:

SELECT note_text 
FROM productnotes
WHERE Match(note_text) Against('+rabbit +bait' IN BOOLEAN MODE);

这个搜索匹配包含词rabbit和bait的行。

SELECT note_text 
FROM productnotes
WHERE Match(note_text) Against('rabbit bait' IN BOOLEAN MODE);

没有指定操作符,这个搜索匹配包含rabbit和bait中的至少一个词的行。

SELECT note_text 
FROM productnotes
WHERE Match(note_text) Against('"rabbit bait"' IN BOOLEAN MODE);

这个搜索匹配短语rabbit bait而不是匹配两个词rabbit和bait。

SELECT note_text 
FROM productnotes
WHERE Match(note_text) Against('>rabbit <carrot' IN BOOLEAN MODE);

匹配rabbit和carrot,增加前者的等级,降低后者的等级。

SELECT note_text 
FROM productnotes
WHERE Match(note_text) Against('+safe +(<combination)' IN BOOLEAN MODE);

这个搜索匹配词safe和combination,降低后者的等级。

排列而不排序 在布尔方式中,不按等级值降序排序返回的行。

18.2.5 全文本搜索的使用说明

  1. 在索引全文本数据时,短词被忽略且从索引中排除。短词定义为那些具有3个或3个以下字符的词(如果需要,这个数目可以更改)。
  2. MySQL带有一个内建的非用词(stopword)列表,这些词在索引全文本数据时总是被忽略。如果需要,可以覆盖这个列表(请参阅MySQL文档以了解如何完成此工作)
  3. 许多词出现的频率很高,搜索它们没有用处(返回太多的结果)。因此,MySQL规定了一条50%规则,如果一个词出现在50%以上的行中,则将它作为一个非用词忽略。50%规则不用于IN BOOLEAN MODE。
  4. 如果表中的行数少于3行,则全文本搜索不返回结果(因为每个词或者不出现,或者至少出现在50%的行中)。
  5. 忽略词中的单引号。例如,don’t索引为dont。
  6. 不具有词分隔符(包括日语和汉语)的语言不能恰当地返回全文本搜索结果。
  7. 如前所述,仅在MyISAM数据库引擎中支持全文本搜索。

18.3 小结

本章介绍了为什么要使用全文本搜索,以及如何使用MySQL的Match()和Against()函数进行全文本搜索。我们还学习了查询扩展(它能增加找到相关匹配的机会)和如何使用布尔方式进行更细致的查找控制。

第 19 章 插 入 数 据

19.1 数据插入

INSERT是用来插入(或添加)行到数据库表的。插入可
以用几种方式使用:

  1. 插入完整的行;
  2. 插入行的一部分;
  3. 插入多行;
  4. 插入某些查询的结果。

插入及系统安全 可针对每个表或每个用户,利用MySQL的安全机制禁止使用INSERT语句,这将在第28章介绍。

19.2 插入完整的行

把数据插入表中的最简单的方法是使用基本的INSERT语法,它要求定表名和被插入到新行中的值。下面举一个例子:

INSERT INTO Customers
VALUES(NULL,
	'Pep E. LaPew',
	'100 Main Street',
	'Los Angeles',
	'CA',
	'90046',
	'USA',
	NULL,
	NULL);

没有输出 INSERT语句一般不会产生输出。

此例子插入一个新客户到customers表。存储到每个表列中的数据在VALUES子句中给出,对每个列必须提供一个值。如果某
个列没有值(如上面的cust_contact和cust_email列),应该使用NULL值(假定表允许对该列指定空值)。**各个列必须以它们在表定义中出现的次序填充。**第一列cust_id也为NULL。这是因为每次插入一个新行时,该列由MySQL自动增量。你不想给出一个值(这是MySQL的工作),又不能省略此列(如前所述,必须给出每个列),所以指定一个NULL值(它被MySQL忽略,MySQL在这里插入下一个可用的cust_id值)。

虽然这种语法很简单,但并不安全,应该尽量避免使用。

虽然这种语法很简单,但并不安全,应该尽量避免使用。上面的SQL语句高度依赖于表中列的定义次序,并且还依赖于其次序容易获得的信息。即使可得到这种次序信息,也不能保证下一次表结构变动后各个列保持完全相同的次序。因此,编写依赖于特定列次序的SQL语句是很不安全的。如果这样做,有时难免会出问题。

编写INSERT语句的更安全(不过更烦琐)的方法如下:

INSERT INTO Customers(cust_name,
	cust_address,
	cust_city,
	cust_state,
	cust_zip,
	cust_country,
	cust_contact,
	cust_email)
VALUES('Pep E. LaPew',
	'100 Main Street',
	'Los Angeles',
	'CA',
	'90046',
	'USA',
	NULL,
	NULL);

在插入行时,MySQL将用VALUES列表中的相应值填入列表中的对应项。VALUES中的第一个值对应于第一个指定的列名。第二个值对应于第二个列名,如此等等。

因为提供了列名,VALUES必须以其指定的次序匹配指定的列名,不一定按各个列出现在实际表中的次序。其优点是,即使表的结构改变,此INSERT语句仍然能正确工作。你会发现cust_id的NULL值是不必要的,cust_id列并没有出现在列表中,所以不需要任何值。

下面的INSERT语句填充所有列(与前面的一样),但以一种不同的次序填充。因为给出了列名,所以插入结果仍然正确:

INSERT INTO Customers(cust_name,
	cust_contact,
	cust_email,
	cust_address,
	cust_city,
	cust_state,
	cust_zip,
	cust_country)
VALUES('Pep E. LaPew',
	NULL,
	NULL,
	'100 Main Street',
	'Los Angeles',
	'CA',
	'90046',
	'USA');

总是使用列的列表 一般不要使用没有明确给出列的列表的INSERT语句。使用列的列表能使SQL代码继续发挥作用,即使表结构发生了变化。

仔 细地给出值 不管使用哪种INSERT语法,都必须给出VALUES的正确数目。如果不提供列名,则必须给每个表列提供一个值。如果提供列名,则必须对每个列出的列给出一个值。如果不这样,将产生一条错误消息,相应的行插入不成功。

省略列 如果表的定义允许,则可以在INSERT操作中省略某些列。省略的列必须满足以下某个条件。

  1. 该列定义为允许NULL值(无值或空值)。
  2. 在表定义中给出默认值。这表示如果不给出值,将使用默认值。

如果对表中不允许NULL值且没有默认值的列不给出值,则MySQL将产生一条错误消息,并且相应的行插入不成功。

提高整体性能 数据库经常被多个客户访问,对处理什么请求以及用什么次序处理进行管理是MySQL的任务。INSERT操作可能很耗时(特别是有很多索引需要更新时),而且它可能降低等待处理的SELECT语句的性能。如果数据检索是最重要的(通常是这样),则你可以通过在INSERT和INTO之间添加关键字LOW_PRIORITY,指示MySQL降低INSERT语句的优先级,如下所示:

INSERT LOW PRIORITY INTO

顺便说一下,这也适用于下一章介绍的UPDATE和DELETE语句。

19.3 插入多个行

使用多条INSERT语句,一次提交它们,每条语句用一个分号结束,如下所示:

INSERT INTO Customers(cust_name,
	cust_contact,
	cust_email,
	cust_address,
	cust_city,
	cust_state,
	cust_zip,
	cust_country)
VALUES('Pep E. LaPew',
	NULL,
	NULL,
	'100 Main Street',
	'Los Angeles',
	'CA',
	'90046',
	'USA');
INSERT INTO Customers(cust_name,
	cust_contact,
	cust_email,
	cust_address,
	cust_city,
	cust_state,
	cust_zip,
	cust_country)
VALUES('M. Martian',
	NULL,
	NULL,
	'42 Galaxy Way',
	'New York',
	'NY',
	'11213',
	'USA');

或者,只要每条INSERT语句中的列名(和次序)相同,可以如下组合各语句:

INSERT INTO Customers(cust_name,
	cust_contact,
	cust_email,
	cust_address,
	cust_city,
	cust_state,
	cust_zip,
	cust_country)
VALUES('Pep E. LaPew',
	NULL,
	NULL,
	'100 Main Street',
	'Los Angeles',
	'CA',
	'90046',
	'USA'),
VALUES('M. Martian',
	NULL,
	NULL,
	'42 Galaxy Way',
	'New York',
	'NY',
	'11213',
	'USA');

其中单条INSERT语句有多组值,每组值用一对圆括号括起来,用逗号分隔。

提高INSERT的性能 此技术可以提高数据库处理的性能,因为MySQL用单条INSERT语句处理多个插入比使用多条INSERT语句快。

19.4 插入检索出的数据

INSERT一般用来给表插入一个指定列值的行。但是,INSERT还存在另一种形式,可以利用它将一条SELECT语句的结果插入表中。这就是所谓的INSERT SELECT,顾名思义,它是由一条INSERT语句和一条SELECT语句组成的。

假如你想从另一表中合并客户列表到你的customers表。不需要每次读取一行,然后再将它用INSERT插入,可以如下进行:

新例子的说明 这个例子把一个名为custnew的表中的数据导入customers表中。为了试验这个例子,应该首先创建和填充custnew表。custnew表的结构与附录B中描述的customers表的相同。在填充custnew时,不应该使用已经在customers中使用过的cust_id值(如果主键值重复,后续的INSERT操作将会失败)或仅省略这列值让MySQL在导入数据的过程中产生新值。

INSERT INTO Customers(cust_id,
	cust_contact,
	cust_email,
	cust_name,
	cust_address,
	cust_city,
	cust_state,
	cust_zip,
	cust_country)
SELECT cust_id,
	cust_contact,
	cust_email,
	cust_name,
	cust_address,
	cust_city,
	cust_state,
	cust_zip,
	cust_country
FROM custnew;

这个例子使用INSERT SELECT从custnew中将所有数据导入customers。SELECT语句从custnew检索出要插入的值,而不是列出它们。SELECT中列出的每个列对应于customers表名后所跟的列表中的每个列。这条语句将插入多少行有赖于custnew表中有多少行。如果这个表为空,则没有行被插入(也不产生错误,因为操作仍然是合法的)。如果这个表确实含有数据,则所有数据将被插入到customers。

这个例子导入了cust_id(假设你能够确保cust_id的值不重复)。你也可以简单地省略这列(从INSERT和SELECT中),这样MySQL就会生成
新值。

INSERT SELECT中的列名

为简单起见,这个例子在INSERT和SELECT语句中使用了相同的列名。但是,不一定要求列名匹配。事实上,MySQL甚至不关心SELECT返回的列名。它使用的是列的位置,因此SELECT中的第一列(不管其列名)将用来填充表列中指定的第一个列,第二列将用来填充表列中指定的第二个列,如此等等。这对于从使用不同列名的表中导入数据是非常有用的。

INSERT SELECT中SELECT语句可包含WHERE子句以过滤插入的数据。

第 20 章 更新和删除数据

更新(修改)表中的数据,可使用UPDATE语句。可采用两种方式使用UPDATE:

  1. 更新表中特定行;
  2. 更新表中所有行。

不要省略WHERE子句 在使用UPDATE时一定要注意细心。因为稍不注意,就会更新表中所有行。在使用这条语句前,请完整地阅读本节。

UPDATE与安全 可以限制和控制UPDATE语句的使用,更多内容请参见第28章。

基本的UPDATE语句由3部分组成,分别是:

  1. 要更新的表;
  2. 列名和它们的新值
  3. 确定要更新行的过滤条件。

举一个简单例子。客户10005现在有了电子邮件地址,因此他的记录需要更新,语句如下:

UPDATE customers
SET cust_email = 'elmer@fudd.com'
WHERE cust_id = 10005;

要更新的表的名字为customers。SET命令用来将新值赋给被更新的列。如这里所示,SET子句设置cust_email列为指定的值。

UPDATE语句以WHERE子句结束,它告诉MySQL更新哪一行。没有WHERE子句,MySQL将会用这个电子邮件地址更新customers表中所有行,这不是我们所希望的。

更新多个列的语法稍有不同:

UPDATE customers
SET cust_email = 'elmer@fudd.com',
	cust_name = 'The Fudds'
WHERE cust_id = 10005;

在更新多个列时,只需要使用单个SET命令,每个“列=值”对之间用逗号分隔(最后一列之后不用逗号)。

在UPDATE语句中使用子查询 UPDATE语句中可以使用子查询,使得能用SELECT语句检索出的数据更新列数据。关于子
查询及使用的更多内容,请参阅第14章。

IGNORE关键字

如果用UPDATE语句更新多行,并且在更新这些行中的一行或多行时出一个现错误,则整个UPDATE操作被取消(错误发生前更新的所有行被恢复到它们原来的值)。为即使是发生错误,也继续进行更新,可使用IGNORE关键字,如下所示:

UPDATE IGNORE customers…

为了删除某个列的值,可设置它为NULL(假如表定义允许NULL值)。如下进行:

UPDATE customers
SET cust_email = NULL
WHERE cust_id = 10005;

20.2 删除数据

从一个表中删除(去掉)数据,使用DELETE语句。可以两种方式使用DELETE:

  1. 从表中删除特定的行;
  2. 从表中删除所有行。

不要省略WHERE子句 在使用DELETE时一定要注意细心。因为稍不注意,就会错误地删除表中所有行。

下面的语句从customers表中删除一行:

DELETE FROM customers
WHERE cust_id = 10006;

如果省略WHERE子句,它将删除表中每个客户。

DELETE不需要列名或通配符。DELETE删除整行而不是删除列。为了删除指定的列,请使用UPDATE语句。

删除表的内容而不是表

DELETE语句从表中删除行,甚至是删除表中所有行。但是,DELETE不删除表本身。

更快的删除

如果想从表中删除所有行,不要使用DELETE。可使用TRUNCATE TABLE语句,它完成相同的工作,但速度更快(TRUNCATE实际是删除原来的表并重新创建一个表,而不是逐行删除表中的数据)。

20.3 更新和删除的指导原则

如果执行UPDATE而不带WHERE子句,则表中每个行都将用新值更新。类似地,如果执行DELETE语句而不带WHERE子句,表的所有数据都将被删除。

许多SQL程序员使用UPDATE或DELETE时所遵循的习惯:

  1. 除非确实打算更新和删除每一行,否则绝对不要使用不带WHERE子句的UPDATE或DELETE语句。
  2. 保证每个表都有主键(如果忘记这个内容,请参阅第15章),尽可能像WHERE子句那样使用它(可以指定各主键、多个值或值的范围)。
  3. 在对UPDATE或DELETE语句使用WHERE子句前,应该先用SELECT进行测试,保证它过滤的是正确的记录,以防编写的WHERE子句不正确。
  4. 使用强制实施引用完整性的数据库(关于这个内容,请参阅第15章),这样MySQL将不允许删除具有与其他表相关联的数据的行。

第 21 章 创建和操纵表

21.1 创建表

MySQL不仅用于表数据操纵,而且还可以用来执行数据库和表的所有操作,包括表本身的创建和处理。
一般有两种创建表的方法:

  1. 使用具有交互式创建和管理表的工具(如第2章讨论的工具);
  2. 表也可以直接用MySQL语句操纵。

21.1.1 表创建基础

为利用CREATE TABLE创建表,必须给出下列信息:

  1. 新表的名字,在关键字CREATE TABLE之后给出;
  2. 表列的名字和定义,用逗号分隔。
    CREATE TABLE语句也可能会包括其他关键字或选项,但至少要包括表的名字和列的细节。

创建本书中所用的customers表:

CREATE TABLE customers
(
	cust_id	int	NOT NULL AUTO_INCREAMENT,
	cust_name  char(50) NOT NULL,
	cust_address char(50) NULL,
	cust_city  char(50) NULL,
	cust_state  char(50) NULL,
	cust_zip  char(10)  NULL,
	cust_country  char(50)  NULL,
	cust_contact  char(50)  NULL,
	cust_email  char(255)  NULL,
	PRIMARY KEY (cust_id)
)ENGINE=InnoDB;

表名紧跟在CREATE TABLE关键字后面。实际的表定义(所有列)括在圆括号之中。各列之间用逗号分隔。这个表由9列组成。每列的定义以列名(它在表中必须是唯一的)开始,后跟列的数据类型表的主键可以在创建表时用PRIMARY KEY关键字指定。这里,列cust_id指定作为主键列。整条语句由右圆括号后的分号结束。

语句格式化

MySQL语句中忽略空格。语句可以在一个长行上输入,也可以分成许多行。它们的作用都相同。这允许你以最适合自己的方式安排语句的格式。前面的CREATE TABLE语句就是语句格式化的一个很好的例子,它被安排在多个行上,其中的列定义进行了恰当的缩进,以便阅读和编辑。以何种缩进格式安排SQL语句没有规定,但我强烈推荐采用某种缩进格式。

处理现有的表

在创建新表时,指定的表名必须不存在,否则将出错。如果要防止意外覆盖已有的表,SQL要求首先手工删除该表(请参阅后面的小节),然后再重建它,而不是简单地用创建表语句覆盖它。如果你仅想在一个表不存在时创建它,应该在表名后给出IF NOT EXISTS。这样做不检查已有表的模式是否与你打算创建的表模式相匹配。它只是查看表名是否存在,并且仅在表名不存在时创建它。

21.1.2 使用NULL值

NULL值就是没有值或缺值。允许NULL值的列也允许在插入行时不给出该列的值。不允许NULL值的列不接受该列没有值的行,换句话说,在插入或更新行时,该列必须有值。

创建本书中所用的orders表:

CREATE TABLE orders
(
	order_num	int	NOT NULL AUTO_INCREAMENT,
	order_date  datetime NOT NULL,
	cust_id int NOT NULL,
	PRIMARY KEY (order_num)
)ENGINE=InnoDB;

orders包含3个列,分别是订单号、订单日期和客户ID。所有3个列都需要,因此每个列的定义都含有关键字NOT NULL。这将会阻止插入没有值的列。如果试图插入没有值的列,将返回错误,且插入失败。

创建本书中使用的vendors表:

CREATE TABLE vendors
(
	vend_id	int	NOT NULL AUTO_INCREAMENT,
	vend_name  char(50) NOT NULL,
	vend_address  char(50) NULL,
	vend_city  char(50) NULL,
	vend_state  char(5) NULL,
	vend_zip  char(10) NULL,
	vend_country  char(50) NULL,
	PRIMARY KEY (vend_id)
)ENGINE=InnoDB;

供应商ID和供应商名字列是必需的,因此指定为NOT NULL。其余5个列全都允许NULL值,所以不指定NOT NULL。NULL为默认设置,如果不指定NOT NULL,则认为指定的是NULL。

理解NULL

不要把NULL值与空串相混淆。NULL值是没有值,它不是空串。如果指定’'(两个单引号,其间没有字符),这
在NOT NULL列中是允许的。空串是一个有效的值,它不是无值。NULL值用关键字NULL而不是空串指定。

21.1.3 主键再介绍

主键值必须唯一。即,表中的每个行必须具有唯一的主键值。如果主键使用单个列,则它的值必须唯一。如果使用多个列,则这些列的组合值必须唯一。

创建由多个列组成的主键,应该以逗号分隔的列表给出各列名,如下所示:

CREATE TABLE orderitems
(
	order_num  int NOT NULL,
	order_item  int NOT NULL,
	prod_id  char(10) NOT NULL,
	quantity  int  NOT NULL,
	item_price  decimal(8,2) NOT NULL,
	PRIMARY KEY (order_num,order_item)
)ENGINE=InnoDB;

orderitems表包含orders表中每个订单的细节。每个订单有多项物品,但每个订单任何时候都只有1个第一项物品,1个第二项物品,如此等等。因此,订单号(order_num列)和订单物品(order_item列)的组合是唯一的,从而适合作为主键,主键可以在创建表时定义(如这里所示),或者在创建表之后定义。

主键和NULL值

第1章介绍过,主键为其值唯一标识表中每个行的列。主键中只能使用不允许NULL值的列。允许NULL值的列不能作为唯一标识。

21.1.4 使用AUTO_INCREMENT

customers表中的顾客由列cust_id唯一标识,每个顾客有一个唯一编号。类似,orders表中的每个订单有一个唯一的订单号,这个订单号存储在列order_num中。增加一个新顾客或新订单时,需要一个新的顾客ID或订单号。这些编号可以任意,只
要它们是唯一的即可。

当然,你可以使用SELECT语句得出最大的数(使用第12章介绍的Max()函数),然后对它加1。但这样做并不可靠(你需要找出一种办法来保证,在你执行SELECT和INSERT两条语句之间没有其他人插入行,对于多用户应用,这种情况是很有可能出现的),而且效率也不高(执行额外的MySQL操作肯定不是理想的办法)。这就是AUTO_INCREMENT发挥作用的时候了。

AUTO_INCREMENT告诉MySQL,本列每当增加一行时自动增量。每次执行一个INSERT操作时,MySQL自动对该列增量。

每个表只允许一个AUTO_INCREMENT列,而且它必须被索引(如,通过使它成为主键)。

覆盖 AUTO_INCREMENT

如 果 一 个 列 被 指 定 为 AUTO_INCREMENT,则它需要使用特殊的值吗?你可以简单地在INSERT语句中指定一个值,只要它是唯一的(至今尚未使用过)即可,该值将被用来替代自动生成的值。后续的增量将开始使用该手工插入的值。

确定AUTO_INCREMENT值

让MySQL生成(通过自动增量)主键的一个缺点是你不知道这些值都是谁。考虑这个场景:你正在增加一个新订单。这要求在orders表中创建一行,然后在orderitms表中对订购的每项物品创建一行。order_num在orderitems表中与订单细节一起存储。这就是为什么orders表和orderitems表为相互关联的表的原因。这显然要求你在插入orders行之后,插入orderitems行
之前知道生成的order_num。

那么,如何在使用AUTO_INCREMENT列时获得这个值呢?可使用last_insert_id()函数获得这个值,如下所示:

SELECT last_insert_id()

此语句返回最后一个AUTO_INCREMENT值,然后可以将它用于后续的MySQL语句。

21.1.5 指定默认值

在插入行时没有给出值,MySQL允许指定此时使用的默认值。默认值用CREATE TABLE语句的列定义中的DEFAULT关键字指定。

CREATE TABLE orderitems
(
	order_num  int NOT NULL,
	order_item  int NOT NULL,
	prod_id  char(10) NOT NULL,
	quantity  int  NOT NULL  DEFAULT 1,
	item_price  decimal(8,2) NOT NULL,
	PRIMARY KEY (order_num,order_item)
)ENGINE=InnoDB;

quantity列包含订单中每项物品的数量。在此例子中,给该列的描述添加文本DEFAULT 1指示MySQL,在未给出数量的情况下使用数量1。

21.1.6 引擎类型

与其他DBMS一样,MySQL有一个具体管理和处理数据的内部引擎。在你使用CREATE TABLE语句时,该引擎具体创建表,而在你使用SELECT语句或进行其他数据库处理时,该引擎在内部处理你的请求。多数时候,此引擎都隐藏在DBMS内,不需要过多关注它。

但MySQL与其他DBMS不一样,它具有多种引擎。它打包多个引擎,这些引擎都隐藏在MySQL服务器内,全都能执行CREATE TABLE和SELECT等命令。

为什么要发行多种引擎呢?因为它们具有各自不同的功能和特性,为不同的任务选择正确的引擎能获得良好的功能和灵活性。

如果省略ENGINE=语句,则使用默认引擎(很可能是MyISAM),多数SQL语句都会默认使用它。

几个需要知道的引擎:

  1. InnoDB是一个可靠的事务处理引擎(参见第26章),它不支持全文本搜索;
  2. MEMORY在功能等同于MyISAM,但由于数据存储在内存(不是磁盘)中,速度很快(特别适合于临时表);
  3. MyISAM是一个性能极高的引擎,它支持全文本搜索(参见第18章),但不支持事务处理。

引擎类型可以混用。除productnotes表使用MyISAM外,本书中的样例表都使用InnoDB。原因是作者希望支持事务处理(因此,使用InnoDB),但也需要在productnotes中支持全文本搜索(因此,使用MyISAM)

外键不能跨引擎

混用引擎类型有一个大缺陷。外键(用于强制实施引用完整性,如第1章所述)不能跨引擎,即使用一个引擎的表不能引用具有使用不同引擎的表的外键。

21.2 更新表

用ALTER TABLE语句。但是,理想状态下,当表中存储数据以后,该表就不应该再被更新。在表的设计过程中需要花费大量时间来考虑,以便后期不对该表进行大的改动。
为了使用ALTER TABLE更改表结构,必须给出下面的信息:

  1. 在ALTER TABLE之后给出要更改的表名(该表必须存在,否则将出错);
  2. 所做更改的列表。

给表添加一个列:

ALTER TABLE vendors
ADD vend_phone CHAR(20);

这条语句给vendors表增加一个名为vend_phone的列,必须明确其数据类型。

删除刚刚添加的列,可以这样做:

ALTER TABLE vendors
DROP COLUMN vend_phone;

ALTER TABLE的一种常见用途是定义外键。下面是用来定义本书中的表所用的外键的代码:

ALTER TABLE orderitems
ADD CONSTRAINT fk_orderitems_orders
FOREIGN KEY (order_num) REFERENCES orders (order_num);

ALTER TABLE orderitems
ADD CONSTRAINT fk_orderitems_products
FOREIGN KEY (prod_id) REFERENCES products (prod_id);

ALTER TABLE orders
ADD CONSTRAINT fk_orders_customers 
FOREIGN KEY (cust_id) REFERENCES customers (cust_id);

ALTER TABLE products
ADD CONSTRAINT fk_products_vendors 
FOREIGN KEY (vend_id) REFERENCES vendors (vend_id);

由于要更改4个不同的表,使用了4条ALTER TABLE语句。为了对单个表进行多个更改,可以使用单条ALTER TABLE语句,每个更改用逗号分隔。

复杂的表结构更改一般需要手动删除过程,它涉及以下步骤:

  1. 用新的列布局创建一个新表;
  2. 使用INSERT SELECT语句(关于这条语句的详细介绍,请参阅第19章)从旧表复制数据到新表。如果有必要,可使用转换函数和计算字段;
  3. 检验包含所需数据的新表;
  4. 重命名旧表(如果确定,可以删除它);
  5. 用旧表原来的名字重命名新表;
  6. 根据需要,重新创建触发器、存储过程、索引和外键。
小心使用ALTER TABLE

使用ALTER TABLE要极为小心,应该在进行改动前做一个完整的备份(模式和数据的备份)。数据库表的更改不能撤销,如果增加了不需要的列,可能不能删除它们。类似地,如果删除了不应该删除的列,可能会丢失该列中的所有数据。

21.3 删除表

删除表(删除整个表而不是其内容)非常简单,使用DROP TABLE语句即可:

DROP TABLE customers2;

删除customers 2表(假设它存在)。删除表没有确认,也不能撤销,执行这条语句将永久删除该表。

21.4 重命名表

RENAME TABLE语句可以重命名一个表:

RENAME TABLE customers2 TO customers;

RENAME TABLE所做的仅是重命名一个表。可以使用下面的语句对多个表重命名:

RENAME TABLE backup_customers TO customers,
			backup_vendors TO vendors,
			backup_products TO products;

21.5 小结

本章介绍了几条新SQL语句。CREATE TABLE用来创建新表,ALTERTABLE用来更改表列(或其他诸如约束或索引等对象),而DROP TABLE用来完整地删除一个表。这些语句必须小心使用,并且应在做了备份后使用。本章还介绍了数据库引擎、定义主键和外键,以及其他重要的表和列选项。

第 22 章 使 用 视 图

22.1 视图

视图是虚拟的表。与包含数据的表不一样,视图只包含使用时动态检索数据的查询。

理解视图的最好方法是看一个例子。第15章中用下面的SELECT语句从3个表中检索数据:

SELECT cust_name,cust_contact
FROM customers,orders,orderitems
WHERE customers.cust_id = orders.cust_id
	AND orderitems.order_num = orders.order_num
	AND prod_id = 'TNT2';

此查询用来检索订购了某个特定产品的客户。

任何需要这个数据的人都必须理解相关表的结构,并且知道如何创建查询和对表进行联结。为了检索其他产品(或多个产品)的相同数据,必须修改最后的WHERE子句。现在,假如可以把整个查询包装成一个名为productcustomers的虚拟表,则可以如下轻松地检索出相同的数据:

SELECT cust_name,cust_contact
FROM productcustomers
WHERE prod_id = 'TNT2';

这就是视图的作用。productcustomers是一个视图,作为视图,它不包含表中应该有的任何列或数据,它包含的是一个SQL查询(与上面用以正确联结表的相同的查询)。

22.1.1 为什么使用视图

下面是视图的一些常见应用。

  1. 重用SQL语句。
  2. 简化复杂的SQL操作。在编写查询后,可以方便地重用它而不必知道它的基本查询细节。
  3. 使用表的组成部分而不是整个表。
  4. 保护数据。可以给用户授予表的特定部分的访问权限而不是整个表的访问权限。
  5. 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。

在视图创建之后,可以用与表基本相同的方式利用它们。可以对视图执行SELECT操作,过滤和排序数据,将视图联结到其他视图或表,甚至能添加和更新数据(添加和更新数据存在某些限制。关于这个内容稍
后还要做进一步的介绍)。

重要的是知道视图仅仅是用来查看存储在别处的数据的一种设施。视图本身不包含数据,因此它们返回的数据是从其他表中检索出来的。在添加或更改这些表中的数据时,视图将返回改变过的数据。

性能问题

因为视图不包含数据,所以每次使用视图时,都必须处理查询执行时所需的任一个检索。如果你用多个联结和过滤创建了复杂的视图或者嵌套了视图,可能会发现性能下降得很厉害。因此,在部署使用了大量视图的应用前,应该进行测试。

22.1.2 视图的规则和限制

下面是关于视图创建和使用的一些最常见的规则和限制。

  1. 与表一样,视图必须唯一命名(不能给视图取与别的视图或表相同的名字)。
  2. 对于可以创建的视图数目没有限制。
  3. 为了创建视图,必须具有足够的访问权限。这些限制通常由数据库管理人员授予。
  4. 视图可以嵌套,即可以利用从其他视图中检索数据的查询来构造一个视图。
  5. ORDER BY可以用在视图中,但如果从该视图检索数据SELECT中也含有ORDER BY,那么该视图中的ORDER BY将被覆盖。
  6. 视图不能索引,也不能有关联的触发器或默认值。
  7. 视图可以和表一起使用。例如,编写一条联结表和视图的SELECT语句。

22.2 使用视图

视图的创建。

  1. 视图用CREATE VIEW语句来创建。
  2. 使用SHOW CREATE VIEW viewname;来查看创建视图的语句。
  3. 用DROP删除视图,其语法为DROP VIEW viewname;。
  4. 更新视图时,可以先用DROP再用CREATE,也可以直接用CREATE OR REPLACE VIEW。如果要更新的视图不存在,则第2条更新语句会创建一个视图;如果要更新的视图存在,则第2条更新语句会替换原有视图。

22.2.1 利用视图简化复杂的联结

视图的最常见的应用之一是隐藏复杂的SQL,这通常都会涉及联结。请看下面的例子:

CREATE VIEW productcustomers AS
SELECT cust_name,cust_contact,prod_id
FROM customers,orders,orderitems
WHERE customers.cust_id = orders.cust_id
	AND orderitems.order_num = orders.order_num;

这条语句创建一个名为productcustomers的视图,它联结三个表,以返回已订购了任意产品的所有客户的列表。如果执行
SELECT * FROM productcustomers,将列出订购了任意产品的客户。

为检索订购了产品TNT2的客户,可如下进行:

SELECT cust_name,cust_contact
FROM productcustomers
WHERE prod_id = 'TNT2';

输出:

+---------------+-----------------+
cust_name         cust_contact
+---------------+-----------------+
Coyote Inc.         Y Lee
Yosemite Place      Y Sam
+---------------+-----------------+

这条语句通过WHERE子句从视图中检索特定数据。在MySQL处理此查询时,它将指定的WHERE子句添加到视图查询中的已有WHERE子句中,以便正确过滤数据。

视图极大地简化了复杂SQL语句的使用。利用视图,可一次性编写基础的SQL,然后根据需要多次使用。

创建可重用的视图

创建不受特定数据限制的视图是一种好办法。例如,上面创建的视图返回生产所有产品的客户而不仅仅是 生产TNT2的客户。扩展视图的范围不仅使得它能被
重用,而且甚至更有用。这样做不需要创建和维护多个类似视图。

22.2.2 用视图重新格式化检索出的数据

视图的另一常见用途是重新格式化检索出的数据。下面的SELECT语句(来自第10章)在单个组合计算列中返回供应商名和位置:

SELECT Concat(RTrim(vend_name),'(',RTrim(vend_country),')')
	AS vend_title
FROM vendors
ORDER BY vend_name;

输出:

+---------------+
vend_title        
+---------------+
ACME(USA)
Anvila R Us(USA)        
Furball Inc.(USA)
Jet Set(England)
Jouets Et Ours(France)
LT Supplies(USA)
+---------------+

现在,假如经常需要这个格式的结果。不必在每次需要时执行联结,创建一个视图,每次需要时使用它即可。为把此语句转换为视图,可按
如下进行:

CREATE VIEW vendorlocations AS
SELECT Concat(RTrim(vend_name),'(',RTrim(vend_country),')')
	AS vend_title
FROM vendors
ORDER BY vend_name;

这条语句使用与以前的SELECT语句相同的查询创建视图。为了检索出以创建所有邮件标签的数据,可如下进行:

SELECT * FROM vendorlocations;

输出:

+---------------+
vend_title        
+---------------+
ACME(USA)
Anvila R Us(USA)        
Furball Inc.(USA)
Jet Set(England)
Jouets Et Ours(France)
LT Supplies(USA)
+---------------+

22.2.3 用视图过滤不想要的数据

视图对于应用普 的 WHERE子句也很有用。例如,可以定义customeremaillist视图,它过滤没有电子邮件地址的客户。为此目的,可使用下面的语句:

CREATE VIEW customeremaillist AS 
SELECT cust_id,cust_name,cust_email
FROM customers
WHERE cust_email IS NOT NULL;

这里的WHERE子句过滤了cust_email列中具有NULL值的那些行,使他们不被检索出来。

可以像使用其他表一样使用视图customeremaillist。

SELECT * FROM customeremaillist;

输出:

+------------+-----------------+--------------------+
 cust_id       cust_name        cust_email
+------------+-----------------+--------------------+
 10001         Coyote Inc.      ylee@coyote.com
 10003         Wascals          rabbit@wascally.com
 10004         Yosemite Place   sam@yosemite.com
+------------+-----------------+--------------------+

WHERE子句与WHERE子句 如果从视图检索数据时使用了一条WHERE子句,则两组子句(一组在视图中,另一组是传递给视图的)将自动组合。

22.2.4 使用视图与计算字段

视图对于简化计算字段的使用特别有用。下面是第10章中介绍的一条SELECT语句。它检索某个特定订单中的物品,计算每种物品的总价格:

SELECT prod_id,
	quantity,
	item_price,
	quantity*item_price AS expanded_price
FROM orderitems
WHERE order_num = 20005;

输出:

+------+---------+---------+--------------+
prod_id quantity item_price expanded_price
+------+---------+---------+--------------+
ANV01    10      5.99           59.90
ANV02    3       9.99           29.97
TNT2     5       10.00          50.00
FB       1       10.00          10.00
+------+---------+---------+--------------+

为将其转换为一个视图,如下进行:

CREATE VIEW orderitemsexpanded AS
SELECT order_num,
	prod_id,
	quantity,
	item_price,
	quantity*item_price AS expanded_price
FROM orderitems;

为检索订单20005的详细内容(上面的输出),如下进行:

SELECT * 
FROM orderitemsexpanded 
WHERE order_num = 20005;

输出:

+---------+--------+---------+---------+--------------+
order_num	prod_id quantity item_price expanded_price
+---------+--------+---------+---------+--------------+
20005		ANV01    10      5.99           59.90
20005		ANV02    3       9.99           29.97
20005		TNT2     5       10.00          50.00
20005		FB       1       10.00          10.00
+---------+--------+---------+---------+--------------+

视图非常容易创建,而且很好使用。正确使用,视图可极大地简化复杂的数据处理。

22.2.5 更新视图

视图的数据能否更新?答案视情况而定。

通常,视图是可更新的(即,可以对它们使用INSERT、UPDATE和DELETE)。更新一个视图将更新其基表(可以回忆一下,视图本身没有数
据)。如果你对视图增加或删除行,实际上是对其基表增加或删除行。

但是,并非所有视图都是可更新的。基本上可以说,如果MySQL不能正确地确定被更新的基数据,则不允许更新(包括插入和删除)。这实
际上意味着,如果视图定义中有以下操作,则不能进行视图的更新:

  • 分组(使用GROUP BY和HAVING);
  • 联结;
  • 子查询;
  • 并;
  • 聚集函数(Min()、Count()、Sum()等);
  • DISTINCT;
  • 导出(计算)列。
    本章许多例子中的视图都是不可更新的。这听上去好像是一个严重的限制,但实际上不是,因为视图主要用于数据检索。

**将视图用于检索 **
一般,应该将视图用于检索(SELECT语句)而不用于更新(INSERT、UPDATE和DELETE)。

第 23 章 使用存储过程

存储过程简单来说,就是为以后的使用而保存的一条或多条MySQL语句的集合。

23.3 使用存储过程

23.3.1 执行存储过程

MySQL称存储过程的执行为调用,执行存储过程的语句为CALL。CALL接受存储过程的名字以及需要传递给它的任意参数。

CALL productpricing(@pricelow,
					@pricehigh,
					@priceaverage);

该句执行名为productpricing的存储过程,它计算并返回产品的最低、最高和平均价格。

23.3.2 创建存储过程

一个例子——一个返回产品平均价格的存储过程。以下是其代码:

CREATE PROCEDURE productpricing()
BEGIN 
	SELECT Avg(prod_price) AS priceaverage
	FROM products;
END;

此存储过程名为productpricing,用CREATE PROCEDURE productpricing()语句定义。如果存储过程接受参数,它们将在()中列举出来。此存储过程没有参数,但后跟的()仍然需要。
BEGIN和END语句用来限定存储过程体,过程体本身仅是一个简单的SELECT语句。

**备注:**PROCEDURE 英 [prəˈsiːdʒə®] n. (商业、法律或政治上的)程序; 步骤;

在MySQL处理这段代码时,它创建一个新的存储过程productpricing。没有返回数据,因为这段代码并未调用存储过程,这里只是为以后使用而创建它。

mysql命令行客户机的分隔符

默认的MySQL语句分隔符为;。
mysql命令行实用程序也使用;作为语句分隔符。如果命令行实用程序要解释存储过程自身内的;字符,则它们最终不会成为存储过程的成分,这会使存储过程中的SQL出现句法错误。

解决办法是临时更改命令行实用程序的语句分隔符,如下所示:

DELIMITER //

CREATE PROCEDURE productpricing()
BEGIN
	SELECT Avg(prod_price) AS priceaverage
	FROM products;
END //

DELIMITER ;

DELIMITER //告诉命令行实用程序使用//作为新的语句结束分隔符,可以看到标志存储过程结束的END定义为END //而不是END ;。这样,存储过程体内的;仍然保持不动,并且正确地传递给数据库引擎。最后,为恢复为原来的语句分隔符,可使用DELIMITER ;。除\符号外,任何字符都可以用作语句分隔符。

如何使用名为productpricing这个存储过程?如下所示:

CALL productpricing();

输出:

+-----------------+
| priceaverage    |
+-----------------+
|   16.133571     |
+-----------------+

CALL productpricing();执行刚创建的存储过程并显示返回的结果。因为存储过程实际上是一种函数,所以存储过程名后需要有()符号(即使不传递参数也需要)。

23.3.3 删除存储过程

存储过程在创建之后,被保存在服务器上以供使用,直至被删除。

删除刚创建的存储过程,可使用以下语句:

DROP PROCEDURE productpricing;

删除刚创建的存储过程。请注意没有使用后面的(),只给出存储过程名。

仅当存在时删除

如果指定的过程不存在,则DROP PROCEDURE将产生一个错误。当过程存在想删除它时(如果过程不存在也不产生错误)可使用DROP PROCEDURE IF EXISTS。

23.3.4 使用参数

productpricing只是一个简单的存储过程,它简单地显示SELECT语句的结果。一般,存储过程并不显示结果,而是把结果返回给你指定的变量。

以下是productpricing的修改版本:(如果不先删除此存储过程,则不能再次创建它):

CREATE PROCEDURE productpricing(
	OUT pl DECIMAL(8,2),
	OUT ph DECIMAL(8,2),
	OUT pa DECIMAL(8,2)
)
BEGIN
	SELECT Min(prod_price)
	INTO pl
	FROM products;
	SELECT Max(prod_price)
	INTO ph
	FROM products;
	SELECT Avg(prod_price)
	INTO pa
	FROM products;
END;

此存储过程接受3个参数:pl存储产品最低价格,ph存储产品最高价格,pa存储产品平均价格。每个参数必须具有指定的类
型,这里使用十进制值。关键字OUT指出相应的参数用来从存储过程传出一个值(返回给调用者)。MySQL支持IN(传递给存储过程)、OUT(从存
储过程传出,如这里所用)和INOUT(对存储过程传入和传出)类型的参数。存储过程的代码位于BEGIN和END语句内,如前所见,它们是一系列
SELECT语句,用来检索值,然后保存到相应的变量(通过指定INTO关键字)。

参数的数据类型

存储过程的参数允许的数据类型与表中使用的数据类型相同。附录D列出了这些类型。
注意,记录集不是允许的类型,因此,不能通过一个参数返回多个行和列。这就是前面的例子为什么要使用3个参数(和3条SELECT语句)的原因。

为调用此修改过的存储过程,必须指定3个变量名,如下所示:

CALL productpricing(@pricelow,
					@pricehigh,
					@priceaverage);

由于此存储过程要求3个参数,因此必须正好传递3个参数,不多也不少。所以,这条CALL语句给出3个参数。它们是存储过程将保存结果的3个变量的名字。

变量名 所有MySQL变量都必须以@开始。

在调用时,这条语句并不显示任何数据。它返回以后可以显示(或在其他处理中使用)的变量。
为了显示检索出的产品平均价格,可如下进行:

SELECT @priceaverage;

输出:

+-----------------+
| @priceaverage    |
+-----------------+
|   16.133571428   |
+-----------------+

为了获得3个值,可使用以下语句:

SELECT @pricehigh,@pricelow,@priceaverage;

输出:

+-----------------+------------------+-------------------+
| @pricehigh      | @pricelow        |   @priceaverage   |
+-----------------+------------------+-------------------+
| 55.00           | 2.50             |   16.133571428    |
+-----------------+------------------+-------------------+

另外一个例子,这次使用IN和OUT参数。ordertotal接受订单号并返回该订单的合计:

CREATE PROCEDURE ordertotal(
	IN onumber INT,
	OUT ototal DECIMAL(8,2)
)
BEGIN
	SELECT Sum(item_price*quantity)
	FROM orderitems
	WHERE order_num = onumber
	INTO ototal;
END;

onumber定义为IN,因为订单号被传入存储过程。ototal定义为OUT,因为要从存储过程返回合计。SELECT语句使用这两个参数,WHERE子句使用onumber选择正确的行,INTO使用ototal存储计算
出来的合计。

为调用这个新存储过程,可使用以下语句:

CALL ordertotal(20005,@total);

必须给ordertotal传递两个参数;第一个参数为订单号,第二个参数为包含计算出来的合计的变量名。

为了显示此合计,可如下进行:

SELECT @total;

输出:

+-----------+
| @total    |
+-----------+
| 149.87    |
+-----------+

@total已由ordertotal的CALL语句填写,SELECT显示它包含的值。

为了得到另一个订单的合计显示,需要再次调用存储过程,然后重新显示变量:

CALL ordertotal(20009,@total);
SELECT @tatal;

23.3.5 建立智能存储过程

迄今为止使用的所有存储过程基本上都是封装MySQL简单的SELECT语句。虽然它们全都是有效的存储过程例子,但它们所能完成的工作你直接用这些被封装的语句就能完成(如果说它们还能带来更多的东西,那就是使事情更复杂)。只有在存储过程内包含业务规则和智能处理时,它们的威力才真正显现出来。

考虑这个场景。你需要获得与以前一样的订单合计,但需要对合计增加营业税,不过只针对某些顾客(或许是你所在州中那些顾客)。那么,你需要做下面几件事情:

1.获得合计(与以前一样);
1.把营业税有条件地添加到合计;
1.返回合计(带或不带税)

存储过程的完整工作如下:

-- Name:ordertotal
-- Parameters: onumber = order number
--             taxable = 0 if not taxable,1 if taxable
--             ototal = order total variable

CREATE PROCEDURE ordertotal(
	IN onumber INT,
	IN taxable BOOLEAN,
	OUT ototal DECIMAL(8,2)
)COMMENT 'Obtain order total,optionally adding tax'
BEGIN
	--Declare variale for total
	DECLARE total DECIMAL(8,2)
	--Declare tax percentage
	DECLARE taxrate INT DEFAULT 6;

	--GET the order total
	SELECT Sum(item_price*quantity)
	FROM orderitems
	WHERE order_num = onumber
	INTO total;
	
	--Is this taxable?
	IF taxable THEN
		--Yes, so add taxrate to the total
		SELECT total+(total/100*taxrate) INTO total;
	END IF;
		--And finally,save to out variable
		SELECT total INTO ototal;

END;

此存储过程有很大的变动。首先,增加了注释(前面放置–)

在存储过程复杂性增加时,这样做特别重要。添加了另外一个参数taxable,它是一个布尔值(如果要增加税则为真,否则为假)。在存储过程体中,用DECLARE语句定义了两个局部变量。

DECLARE要求指定变量名和数据类型,它也支持可选的默认值(这个例子中的taxrate的默认被设置为6%)。

SELECT语句已经改变,因此其结果存储到total(局部变量)而不是ototal。IF语句检查taxable是否为真,如果为真,则用另一SELECT语句增加营业税到局部变量total。最后,用另一SELECT语句将total(它增加或许不增加营业税)保存到ototal。

COMMENT关键字 本例子中的存储过程在CREATE PROCEDURE语句中包含了一个COMMENT值。它不是必需的,但如果给出,将在SHOW PROCEDURE STATUS的结果中显示。
这显然是一个更高级,功能更强的存储过程。为试验它,请用以下两条语句:

CALL ordertotal(20005,0,@total);
SELECT @total;

输出:

+-----------+
| @total    |
+-----------+
| 149.87    |
+-----------+

CALL ordertotal(20005,1,@total);
SELECT @total;

输出:

+-------------+
| @total      |
+-------------+
|158.862200000|
+-------------+

BOOLEAN值指定为1表示真,指定为0表示假(实际上,非零值都考虑为真,只有0被视为假)。通过给中间的参数指定0或1,可以有条件地将营业税加到订单合计上。

23.3.6 检查存储过程

为显示用来创建一个存储过程的CREATE语句,使用SHOW CREATE PROCEDURE语句:

SHOW CREATE PROCEDURE ordertotal;

为了获得包括何时、由谁创建等详细信息的存储过程列表,使用SHOW PROCEDURE STATUS。

限制过程状态结果

SHOW PROCEDURE STATUS列出所有存储过程。为限制其输出,可使用LIKE指定一个过滤模式,例如:

SHOW PROCEDURE STATUS LIKE 'ordertotal';

第 24 章 使 用 游 标

24.1 游标

MySQL检索操作返回一组称为结果集的行。这组返回的行都是与SQL语句相匹配的行(零行或多行)。使用简单的SELECT语句,例如,没有办法得到第一行、下一行或前10行,也不存在每次一行地处理所有行的简单方法(相对于成批地处理它们)。

有时,需要在检索出来的行中前进或后退一行或多行。这就是使用游标的原因。游标(cursor)是一个存储在MySQL服务器上的数据库查询,它不是一条SELECT语句,而是被该语句检索出来的结果集。在存储了游标之后,应用程序可以根据需要滚动或浏览其中的数据。

游标主要用于交互式应用,其中用户需要滚动屏幕上的数据,并对数据进行浏览或做出更改。

只能用于存储过程 不像多数DBMS,MySQL游标只能用于存储过程(和函数)。

24.2 使用游标

使用游标涉及几个明确的步骤。

  1. 在能够使用游标前,必须声明(定义)它。这个过程实际上没有检索数据,它只是定义要使用的SELECT语句。
  2. 一旦声明后,必须打开游标以供使用。这个过程用前面定义的SELECT语句把数据实际检索出来。
  3. 对于填有数据的游标,根据需要取出(检索)各行。
  4. 在结束游标使用时,必须关闭游标

在声明游标后,可根据需要频繁地打开和关闭游标。在游标打开后,可根据需要频繁地执行取操作。

24.2.1 创建游标

游标用DECLARE语句创建(参见第23章)。DECLARE命名游标,并定义相应的SELECT语句,根据需要带WHERE和其他子句。例如,下面的语句定义了名为ordernumbers的游标,使用了可以检索所有订单的SELECT语句。

CREATE PROCEDURE processorders()
BEGIN 
	DECLARE ordernumbers CURSOR
	FOR
	SELECT order_num FROM orders;
END;

这个存储过程并没有做很多事情,DECLARE语句用来定义和命名游标,这里为ordernumbers。 存储过程处理完成后,游标就消失(因为它局限于存储过程)。

在定义游标之后,可以打开它。

24.2.2 打开和关闭游标

游标用OPEN CURSOR语句来打开:

OPEN ordernumbers;

在处理OPEN语句时执行查询,存储检索出的数据以供浏览和滚动。

游标处理完成后,应当使用如下语句关闭游标:

CLOSE ordernumbers;

CLOSE释放游标使用的所有内部内存和资源,因此在每个游标不再需要时都应该关闭。

在一个游标关闭后,如果没有重新打开,则不能使用它。但是,使用声明过的游标不需要再次声明,用OPEN语句打开它就可以了。

隐含关闭
如果你不明确关闭游标,MySQL将会在到达END语句时自动关闭它。

下面是前面例子的修改版本:

CREATE PROCEDURE processorders()
BEGIN
	--Declare the cursor
	DECLARE ordernumbers CURSOR
	FOR
	SELECT order_num FROM orders;
	
	--Open the cursor
	OPEN ordernumbers;
	
	--Close the cursor
	CLOSE ordernumbers;
END;

这个存储过程声明、打开和关闭一个游标。但对检索出的数据什么也没做。

24.2.3 使用游标数据

在一个游标被打开后,可以使用FETCH语句分别访问它的每一行。FETCH指定检索什么数据(所需的列),检索出来的数据存储在什么地方。它还向前移动游标中的内部行指针,使下一条FETCH语句检索下一行(不重复读取同一行)。

第一个例子从游标中检索单个行(第一行):

CREATE PROCEDURE processorders()
BEGIN

	--Declare local variables
	DECLARE o INT;

	--Declare the cursor
	DECLARE ordernumbers CURSOR
	FOR
	SELECT order_num FROM orders;
	
	--Open the cursor
	OPEN ordernumbers;

	--GET order number
	FETCH ordernumbers INTO o;

	--CLOSE the cursor
	CLOSE ordernumbers;

END;

其中FETCH用来检索当前行的order_num列(将自动从第一行开始)到一个名为o的局部声明的变量中。对检索出的数据不做任何处理。

在下一个例子中,循环检索数据,从第一行到最后一行:

CREATE PROCEDURE processorders()
BEGIN

	--Declare local variables
	DECLARE done BOOLEAN DEFAULT 0;
	DECLARE o INT;

	--Declare the cursor
	DECLARE ordernumbers CURSOR
	FOR
	SELECT order_num FROM orders;
	
	--Declare continue handler
	DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done=1;

	--Open the cursor
	OPEN ordernumbers;

	--Loop through all rows
	REPEAT

		--GET order number
		FETCH ordernumbers INTO o;

	--End of loop
	UNTIL done END REPEAT;

	--CLOSE the cursor
	CLOSE ordernumbers;

END;

与前一个例子一样,这个例子使用FETCH检索当前order_num到声明的名为o的变量中。但与前一个例子不一样的是,这个例子中的FETCH是在REPEAT内,因此它反复执行直到done为真(由UNTIL done END REPEAT;规定)。为使它起作用,用一个DEFAULT 0(假,不结束)定义变量done。那么,done怎样才能在结束时被设置为真呢?答案是用以下语句:

DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done=1;

这条语句定义了一个CONTINUE HANDLER,它是在条件出现时被执行的代码。这里,它指出当SQLSTATE '02000’出现时,SET done=1。SQLSTATE '02000’是一个未找到条件,当REPEAT由于没有更多的行供循环而不能继续时,出现这个条件。

MySQL的错误代码 关于MySQL 5使用的MySQL错误代码列表,请参阅http://dev.mysql.com/doc/mysql/en/error-handling.html。

DECLARE语句的次序

DECLARE语句的发布存在特定的次序。
用DECLARE语句定义的局部变量必须在定义任意游标或句柄之前定义,而句柄必须在游标之后定义。不遵守此顺序将产生错误消息。

如 果 调 用 这 个 存 储 过 程 , 它 将 定 义 几 个 变 量 和 一 个 CONTINUE HANDLER,定义并打开一个游标,重复读取所有行,然后关闭游标。如果一切正常,你可以在循环内放入任意需要的处理(在FETCH语句之后,循环结束之前)。

为了把这些内容组织起来,下面给出我们的游标存储过程样例的更进一步修改的版本,这次对取出的数据进行某种实际的处理:

CREATE PROCEDURE processorders()
BEGIN

	--Declare local variables
	DECLARE done BOOLEAN DEFAULT 0;
	DECLARE o INT;
	DECLARE t DECIMAL(8,2);

	--Declare the cursor
	DECLARE ordernumbers CURSOR
	FOR
	SELECT order_num FROM orders;
	
	--Declare continue handler
	DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done=1;

	--Create a table to store the results
	CREATE TABLE IF NOT EXISTS ordertotals
		(order_num INT,total DECIMAL(8,2));

	--Open the cursor
	OPEN ordernumbers;

	--Loop through all rows
	REPEAT

		--GET order number
		FETCH ordernumbers INTO o;

		--Get the total for this order
		CALL ordertotal(o,1,t);

		--Insert order and total into ordertotals
		INSERT INTO ordertotals(order_num,total)
		VALUES(o,t)

	--End of loop
	UNTIL done END REPEAT;

	--CLOSE the cursor
	CLOSE ordernumbers;

END;

在这个例子中,我们增加了另一个名为t的变量(存储每个订单的合计)。此存储过程还在运行中创建了一个新表(如果它不存在的话),名为ordertotals。这个表将保存存储过程生成的结果。FETCH像以前一样取每个order_num,然后用CALL执行另一个存储过程(我们在
前一章中创建)来计算每个订单的带税的合计(结果存储到t)。最后,用INSERT保存每个订单的订单号和合计。

此存储过程不返回数据,但它能够创建和填充另一个表,可以用一条简单的SELECT语句查看该表:

SELECT * 
FROM ordertotals;

输出:

+----------+----------+
|order_num |total     |
+----------+----------+
|20005     | 158.86   |
|20006     |  59.30   |
|20007     |1060.00   |
|20008     | 132.50   |
|20009     |  40.78   |
+----------+----------+

这样,我们就得到了存储过程、游标、逐行处理以及存储过程调用其他存储过程的一个完整的工作样例。

第 25 章 使用触发器

25.1 触发器

MySQL语句在需要时被执行,存储过程也是如此。但是,如果你想要某条语句(或某些语句)在事件发生时自动执行,怎么办呢?例如:

  1. 每当增加一个顾客到某个数据库表时,都检查其电话号码格式是否正确,州的缩写是否为大写;
  2. 每当订购一个产品时,都从库存数量中减去订购的数量;
  3. 无论何时删除一行,都在某个存档表中保留一个副本。

所有这些例子的共同之处是它们都需要在某个表发生更改时自动处理。这确切地说就是触发器。

触发器是MySQL响应以下任意语句而自动执行的一条MySQL语句(或位于BEGIN和END语句之间的一组语句):

  1. DELETE;
  2. INSERT;
  3. UPDATE。

其他MySQL语句不支持触发器。

25.2 创建触发器

在创建触发器时,需要给出4条信息:

  1. 唯一的触发器名;
  2. 触发器关联的表;
  3. 触发器应该响应的活动(DELETE、INSERT或UPDATE);
  4. 触发器何时执行(处理之前或之后)。

保持每个数据库的触发器名唯一 在MySQL 5中,触发器名必须在每个表中唯一,但不是在每个数据库中唯一。这表示同一数据库中的两个表可具有相同名字的触发器。这在其他每个数据库触发器名必须唯一的DBMS中是不允许的,而且以后的MySQL版本很可能会使命名规则更为严格。因此,现在最好是在数据库范围内使用唯一的触发器名。

触发器用CREATE TRIGGER语句创建。trigger - 触发 英 [ˈtrɪɡə®] vt.触发; 引起; 起动; 发动; 开动;n. 触发器;
下面是一个简单的例子:

CREATE TRIGGER newproduct AFTER INSERT ON products
FOR EACH ROW SELECT 'Product added';

CREATE TRIGGER创建名为newproduct的新触发器。触发器可在一个操作发生之前或之后执行,这里给出了AFTER INSERT,所以此触发器将在INSERT语句成功执行后执行
这个触发器还指定FOR EACH ROW,因此代码对每个插入行执行。在这个例子中,文本Product added将对每个插入的行显示一次。

为了测试这个触发器,使用INSERT语句添加一行或多行到products中,你将看到对每个成功的插入,显示Product added消息。

仅支持表 只有表才支持触发器,视图不支持(临时表也不支持)。
触发器按每个表每个事件每次地定义,每个表每个事件每次只允许一个触发器。因此,每个表最多支持6个触发器(每条INSERT、UPDATE和DELETE的之前和之后)。单一触发器不能与多个事件或多个表关联,所以,如果你需要一个对INSERT和UPDATE操作执行的触发器,则应该定义两个触发器。

触发器失败 如果BEFORE触发器失败,则MySQL将不执行请求的操作。此外,如果BEFORE触发器或语句本身失败,MySQL将不执行AFTER触发器(如果有的话)。

25.3 删除触发器

删除触发器的语法应该很明显了。为了删除一个触发器,可使用DROP TRIGGER语句,如下所示:

DROP TRIGGER newproduct;

触发器不能更新或覆盖。为了修改一个触发器,必须先删除它,然后再重新创建。

25.4 使用触发器

25.4.1 INSERT触发器

INSERT触发器在INSERT语句执行之前或之后执行。需要知道以下几点:

  1. 在INSERT触发器代码内,可引用一个名为NEW的虚拟表,访问被插入的行;
  2. 在BEFORE INSERT触发器中,NEW中的值也可以被更新(允许更改被插入的值);
  3. 对于AUTO_INCREMENT列,NEW在INSERT执行之前包含0,在INSERT执行之后包含新的自动生成值。

下面举一个例子(一个实际有用的例子)。AUTO_INCREMENT列具有MySQL自动赋予的值。第21章建议了几种确定新生成值的方法,但下面是一种更好的方法:

CREATE TRIGGER neworder AFTER INSERT ON orders
FOR EACH ROW SELECT NEW.order_num;

此代码创建一个名为neworder的触发器,它按照AFTER INSERT ON orders执行。在插入一个新订单到orders表时,MySQL生成一个新订单号并保存到order_num中。触发器从NEW.order_num取得这个值并返回它。

此触发器必须按照AFTER INSERT执行,因为在BEFORE INSERT语句执行之前,新order_num还没有生成。对于orders的每次插入使用这个触发器将总是返回新的订单号。

为测试这个触发器,试着插入一下新行,如下所示:

INSERT INTO orders(order_date,cust_id)
VLUES(Now(),10001);

输出:

+----------+
 order_num
+----------+
    20010
+----------+

orders包含3个列。order_date和cust_id必须给出,order_num由MySQL自动生成,而现在order_num还自动被返回。

BEFORE或AFTER? 通常,将BEFORE用于数据验证和净化(目的是保证插入表中的数据确实是需要的数据)。本提示也适用于UPDATE触发器。

25.4.2 DELETE触发器

DELETE触发器在DELETE语句执行之前或之后执行。需要知道以下两点:

  1. 在DELETE触发器代码内,你可以引用一个名为OLD的虚拟表,访问被删除的行;
  2. OLD中的值全都是只读的,不能更新。

下面的例子演示使用OLD保存将要被删除的行到一个存档表中:

CREATE TRIGGER deleteorder BEFORE DELETE ON orders
FOR EACH ROW
BEGIN
	INSERT INTO archive_orders(order_num,order_date,cust_id)
	VALUES(OLD.order_num,OLD.order_date,OLD.cust_id)
END;

在任意订单被删除前将执行此触发器。它使用一条INSERT语句将OLD中的值(要被删除的订单)保存到一个名为archive_orders的存档表中(为实际使用这个例子,你需要用与orders相同的列创建一个名为archive_orders的表)。

使用BEFORE DELETE触发器的优点(相对于AFTER DELETE触发器来说)为,如果由于某种原因,订单不能存档,DELETE本身将被放弃。

多语句触发器 正如所见,触发器deleteorder使用BEGIN和END语句标记触发器体。这在此例子中并不是必需的,不过也没有害处。使用BEGIN END块的好处是触发器能容纳多条SQL语句(在BEGIN END块中一条挨着一条)。

25.4.3 UPDATE触发器

UPDATE触发器在UPDATE语句执行之前或之后执行。需要知道以下几点:

  1. 在UPDATE触发器代码中,你可以引用一个名为OLD的虚拟表访问以前(UPDATE语句前)的值,引用一个名为NEW的虚拟表访问新更新的值;
  2. 在BEFORE UPDATE触发器中,NEW中的值可能也被更新(允许更改将要用于UPDATE语句中的值);
  3. OLD中的值全都是只读的,不能更新。

下面的例子保证州名缩写总是大写(不管UPDATE语句中给出的是大写还是小写):

CREATE TRIGGER updatevendor BEFORE UPDATE ON vendors
FOR EACH ROW SET NEW.vend_state = Upper(NEW.vend_state);

任何数据净化都需要在UPDATE语句之前进行,就像这个例子中一样。每次更新一个行时,NEW.vend_state中的值(将用来更新表行的值)都用Upper(NEW.vend_state)替换。

25.4.4 关于触发器的进一步介绍

在结束本章之前,我们再介绍一些使用触发器时需要记住的重点。

  • 与其他DBMS相比,MySQL 5中支持的触发器相当初级。未来的MySQL版本中有一些改进和增强触发器支持的计划。
  • 创建触发器可能需要特殊的安全访问权限,但是,触发器的执行是自动的。如果INSERT、UPDATE或DELETE语句能够执行,则相关的触发器也能执行。
  • 应该用触发器来保证数据的一致性(大小写、格式等)。在触发器中执行这种类型的处理的优点是它总是进行这种处理,而且是透明地进行,与客户机应用无关。
  • 触发器的一种非常有意义的使用是创建审计跟踪。使用触发器,把更改(如果需要,甚至还有之前和之后的状态)记录到另一个表非常容易。
  • 遗憾的是,MySQL触发器中不支持CALL语句。这表示不能从触发器内调用存储过程。所需的存储过程代码需要复制到触发器内。

第 26 章 管理事务处理

本章介绍什么是事务处理以及如何利用COMMIT和ROLLBACK语句来管理事务处理。

26.1 事务处理

并非所有引擎都支持明确的事务处理管理。MyISAM和InnoDB是两种最常使用的引擎。前者不支持明确的事务处理管理,而后者支持。这就是为什么本书中使用的样例表被创建来使用InnoDB而不是更经常使用的MyISAM的原因。如果你的应用中需要事务处理功能,则一定要使用正确的引擎类型。

事务处理(transaction processing)可以用来维护数据库的完整性,它保证成批的MySQL操作要么完全执行,要么完全不执行。

正如第15章所述,关系数据库设计把数据存储在多个表中,使数据更容易操纵、维护和重用。前面章中使用的orders表就是一个很好的例子。订单存储在orders
和orderitems两个表中:orders存储实际的订单,而orderitems存储订购的各项物品。这两个表使用称为主键(参阅第1章)的唯一ID互相关联。这两个表又与包含客户和产品信息的其他表相关联。

给系统添加订单的过程如下。

(1) 检查数据库中是否存在相应的客户(从customers表查询),如果不存在,添加他/她。
(2) 检索客户的ID。
(3) 添加一行到orders表,把它与客户ID关联。
(4) 检索orders表中赋予的新订单ID。
(5) 对于订购的每个物品在orderitems表中添加一行,通过检索出来的ID把它与orders表关联(以及通过产品ID与products表关联)。

现在,假如由于某种数据库故障(如超出磁盘空间、安全限制、表锁等)阻止了这个过程的完成。数据库中的数据会出现什么情况?
如果故障发生在添加了客户之后,orders表添加之前,不会有什么问题。某些客户没有订单是完全合法的。在重新执行此过程时,所插入的客户记录将被检索和使用。可以有效地从出故障的地方开始执行此过程。

但是,如果故障发生在orders行添加之后,orderitems行添加之前,怎么办呢?现在,数据库中有一个空订单。更糟的是,如果系统在添加orderitems行之中出现故障。结果是数据库中存在不完整的订单,而且你还不知道。

如何解决这种问题?这里就需要使用事务处理了。事务处理是一种机制,用来管理必须成批执行的MySQL操作,以保证数据库不包含不完整的操作结果。利用事务处理,可以保证一组操作不会中途停止,它们或者作为整体执行,或者完全不执行(除非明确指示)。如果没有错误发生,整组语句提交给(写到)数据库表。如果发生错误,则进行回退(撤销)以恢复数据库到某个已知且安全的状态。

因此,请看相同的例子,这次我们说明过程如何工作。

(1) 检查数据库中是否存在相应的客户,如果不存在,添加他/她。
(2) 提交客户信息。
(3) 检索客户的ID。
(4) 添加一行到orders表。
(5) 如果在添加行到orders表时出现故障,回退。
(6) 检索orders表中赋予的新订单ID。
(7) 对于订购的每项物品,添加新行到orderitems表。
(8) 如果在添加新行到orderitems时出现故障,回退所有添加的orderitems行和orders行。
(9) 提交订单信息。

在使用事务和事务处理时,有几个关键词汇反复出现。下面是关于事务处理需要知道的几个术语:

事务(transaction)指一组SQL语句;
回退(rollback)指撤销指定SQL语句的过程;
提交(commit)指将未存储的SQL语句结果写入数据库表;
保留点(savepoint)指事务处理中设置的临时占位符(place-holder),你可以对它发布回退(与回退整个事务处理不同)。

26.2 控制事务处理

管理事务处理的关键在于将SQL语句组分解为逻辑块,并明确规定数据何时应该回退,何时不应该回退。

ySQL使用下面的语句来标识事务的开始:

START TRANSACTION

26.2.1 使用ROLLBACK

ROLLBACK命令用来回退(撤销)MySQL语句:

SELECT * FROM ordertotals;
START TRANSACTION
DELETE FROM ordertotals;
SELECT * FROM ordertotals; 
ROLLBACK
SELECT * FROM ordertotals;

这个例子从显示ordertotals表(此表在第24章中填充)的内容开始。首先执行一条SELECT以显示该表不为空。然后开始一个事务处理,用一条DELETE语句删除ordertotals中的所有行。另一条SELECT语句验证ordertotals确实为空。这时用一条ROLLBACK语句回退START TRANSACTION之后的所有语句,最后一条SELECT语句显示该表不为空。

显然,ROLLBACK只能在一个事务处理内使用(在执行一条START TRANSACTION命令之后)。

哪些语句可以回退?

事务处理用来管理INSERT、UPDATE和DELETE语句。
你不能回退SELECT语句。(这样做也没有什么意义。)你不能回退CREATE或DROP操作。事务处理块中可以使用这两条语句,但如果你执行回退,它们不会被撤销。

26.2.2 使用COMMIT

一般的MySQL语句都是直接针对数据库表执行和编写的。这就是所谓的隐含提交(implicit commit),即提交(写或保存)操作是自动进行的。

但是,在事务处理块中,提交不会隐含地进行。为进行明确的提交,使用COMMIT语句,如下所示:

START TRANSACTION;
DELETE FROM orderitems WHERE order_num = 20010;
DELETE FROM orders WHERE order_num = 20010;
COMMIT;

在这个例子中,从系统中完全删除订单20010。因为涉及更新两个数据库表orders和orderItems,所以使用事务处理块来保证订单不被部分删除。最后的COMMIT语句仅在不出错时写出更改。如果第一条DELETE起作用,但第二条失败,则DELETE不会提交(实际上,它是被自动撤销的)。

隐含事务关闭 当COMMIT或ROLLBACK语句执行后,事务会自动关闭(将来的更改会隐含提交)。

26.2.3 使用保留点

简单的ROLLBACK和COMMIT语句就可以写入或撤销整个事务处理。但是,只是对简单的事务处理才能这样做,更复杂的事务处理可能需要部分提交或回退。

例如,前面描述的添加订单的过程为一个事务处理。如果发生错误,只需要返回到添加orders行之前即可,不需要回退到customers表(如果存在的话)。为了支持回退部分事务处理,必须能在事务处理块中合适的位置放置占位符。这样,如果需要回退,可以回退到某个占位符。这些占位符称为保留点。为了创建占位符,可如下使用SAVEPOINT语句。

SAVEPOINT delete1;

每个保留点都取标识它的唯一名字,以便在回退时,MySQL知道要回退到何处。为了回退到本例给出的保留点,可如下进行:

ROLLBACK TO delete1;

保留点越多越好 可以在MySQL代码中设置任意多的保留点,越多越好。为什么呢?因为保留点越多,你就越能按自己的意愿灵活地进行回退。
释放保留点 保留点在事务处理完成(执行一条ROLLBACK或COMMIT)后自动释放。自MySQL 5以来,也可以用RELEASE SAVEPOINT明确地释放保留点。

26.2.4 更改默认的提交行为
默认的MySQL行为是自动提交所有更改。换句话说,任何时候你执行一条MySQL语句,该语句实际上都是针对表执行的,而且所做的更改立即生效。为指示MySQL不自动提交更改,需要使用以下语句:

SET autocommit=0;

autocommit标志决定是否自动提交更改,不管有没有COMMIT语句。设置autocommit为0(假)指示MySQL不自动提交更改(直到autocommit被设置为真为止)。

第 27 章 全球化和本地化

本章介绍MySQL处理不同字符集和语言的基础知识。

27.1 字符集和校对顺序

数据库表被用来存储和检索数据。不同的语言和字符集需要以不同的方式存储和检索。因此,MySQL需要适应不同的字符集(不同的字母和字符),适应不同的排序和检索数据的方法。

在讨论多种语言和字符集时,将会遇到以下重要术语:

  • 字符集为字母和符号的集合;
  • 编码为某个字符集成员的内部表示;
  • 校对为规定字符如何比较的指令。
    在MySQL的正常数据库活动(SELECT、INSERT等)中,不需要操心太多的东西。使用何种字符集和校对的决定在服务器、数据库和表级进行。

27.2 使用字符集和校对顺序

MySQL支持众多的字符集。为查看所支持的字符集完整列表,使用以下语句:

SHOW CHARACTER SET;

这条语句显示所有可用的字符集以及每个字符集的描述和默认校对。

为了查看所支持校对的完整列表,使用以下语句:

SHOW COLLATION;

此语句显示所有可用的校对,以及它们适用的字符集。可以看到有的字符集具有不止一种校对。例如,latin1对不同的欧洲
语言有几种校对,而且许多校对出现两次,一次区分大小写(由_cs表示),一次不区分大小写(由_ci表示)。

通常系统管理在安装时定义一个默认的字符集和校对。此外,也可以在创建数据库时,指定默认的字符集和校对。为了确定所用的字符集
和校对,可以使用以下语句:

SHOW VARIABLES LIKE 'character%';
SHOW VARIABLES LIKE 'collation%';

不同的表,甚至不同的列都可能需要不同的字符集,而且两者都可以在创建表时指定。

为了给表指定字符集和校对,可使用带子句的CREATE TABLE(参见第21章):

CREATE TABLE mytable
(column1 INT,
 column2  VARCHAR(10)) DEFAULT CHARACTER SET hebrew COLLATIE hebrew_general_ci;

此语句创建一个包含两列的表,并且指定一个字符集和一个校对顺序。

这个例子中指定了CHARACTER SET和COLLATE两者。一般,MySQL如下确定使用什么样的字符集和校对。

  1. 如果指定CHARACTER SET和COLLATE两者,则使用这些值。
  2. 如果只指定CHARACTER SET,则使用此字符集及其默认的校对(如SHOW CHARACTER SET的结果中所示)。
  3. 如果既不指定CHARACTER SET,也不指定COLLATE,则使用数据库默认。

MySQL还允许对每个列设置它们,如下所示:

CREATE TABLE mytable
(column1  INT,
column2  VARCHAR(10),
column3  VARCHAR(10) CHARACTER SET latin1 COLLATE latin1_general_ci
) DEFAULT CHARACTER SET hebrew
COLLATE hebrew_general_ci;

这 里 对 整 个 表 以 及 一 个 特 定 的 列 指 定 了 CHARACTER SET 和COLLATE。

校对在对用ORDER BY子句检索出来的数据排序时起重要的作用。如果你需要用与创建表时不同的校对顺序排序特定的SELECT语句,可以在SELECT语句自身中进行:

SELECT * FROM customers
ORDER BY lastname,firstname COLLATE latin1_general_cs;

此SELECT使用COLLATE指定一个备用的校对顺序(在这个例子中,为区分大小写的校对)。这显然将会影响到结果排序的次序。

临时区分大小写 上面的SELECT语句演示了在通常不区分大小写的表上进行区分大小写搜索的一种技术。当然,反过来也是可以的。

SELECT的其他COLLATE子句
除了这里看到的在ORDER BY子句中使用以外,COLLATE还可以用于GROUP BY、HAVING、聚集函数、别名等。

最后,值得注意的是,如果绝对需要,串可以在字符集之间进行转换。为此,使用Cast()或Convert()函数。

第 28 章 安 全 管 理

数据库服务器通常包含关键的数据,确保这些数据的安全和完整需要利用访问控制。

28.1 访问控制

MySQL服务器的安全基础是:用户应该对他们需要的数据具有适当的访问权,既不能多也不能少。换句话说,用户不能对过多的数据具有过多的访问权。

考虑以下内容:

  1. 多数用户只需要对表进行读和写,但少数用户甚至需要能创建和删除表;
  2. 某些用户需要读表,但可能不需要更新表;
  3. 你可能想允许用户添加数据,但不允许他们删除数据;
  4. 某些用户(管理员)可能需要处理用户账号的权限,但多数用户不需要;
  5. 你可能想让用户通过存储过程访问数据,但不允许他们直接访问数据;
  6. 你可能想根据用户登录的地点限制对某些功能的访问。

这些都只是例子,但有助于说明一个重要的事实,即你需要给用户提供他们所需的访问权,且仅提供他们所需的访问权。这就是所谓的访问控制,管理访问控制需要创建和管理用户账号。

使用MySQL Administrator
MySQL Administrator(在第2章中描述)提供了一个图形用户界面,可用来管理用户及账号权限。MySQL Administrator在内部利用本章介绍的语句,使你能交互地、方便地管理访问控制。

我们知道,为了执行数据库操作,需要登录MySQL。MySQL创建一个名为root的用户账号,它对整个MySQL服务器具有完全的控制。你可能已经在本书各章的学习中使用root进行过登录,在对非现实的数据库试验MySQL时,这样做很好。不过在现实世界的日常工作中,决不能使用root。应该创建一系列的账号,有的用于管理,有的供用户使用,有的供开发人员使用,等等。

防止无意的错误 重要的是注意到,访问控制的目的不仅仅是防止用户的恶意企图。数据梦魇更为常见的是无意识错误的结果,如错打MySQL语句,在不合适的数据库中操作或其他一些用户错误。通过保证用户不能执行他们不应该执行的语句,访问控制有助于避免这些情况的发生。

不要使用root

应该严肃对待root登录的使用。仅在绝对需要时使用它(或许在你不能登录其他管理账号时使用)。不应该在日常的MySQL操作中使用root。

28.2 管理用户

MySQL用户账号和信息存储在名为mysql的MySQL数据库中。一般不需要直接访问mysql数据库和表(你稍后会明白这一点),但有时需要直接访问。需要直接访问它的时机之一是在需要获得所有用户账号列表时。为此,可使用以下代码:

USE mysql;
SELECT user FROM user;

输出:

+--------+
  user
+--------+
  root
+--------+

mysql数据库有一个名为user的表,它包含所有用户账号。user表有一个名为user的列,它存储用户登录名。新安装的服务器
可能只有一个用户(如这里所示),过去建立的服务器可能具有很多用户。

28.2.1 创建用户账号

为了创建一个新用户账号,使用CREATE USER语句,如下所示:

CREATE USER ben IDENTIFIED BY 'p@$$w0rd';

CREATE USER创建一个新用户账号。在创建用户账号时不一定需要口令,不过这个例子用IDENTIFIED BY 'p@$$wOrd’给出了
一个口令。

如果你再次列出用户账号,将会在输出中看到新账号。

指定散列口令 IDENTIFIED BY指定的口令为纯文本,MySQL将在保存到user表之前对其进行加密。为了作为散列值指定口令,使用IDENTIFIED BY PASSWORD。

使用GRANT或INSERT GRANT语句(稍后介绍)也可以创建用户账号,但一般来说CREATE USER是最清楚和最简单的句子。
此外,也可以通过直接插入行到user表来增加用户,不过为安全起见,一般不建议这样做。MySQL用来存储用户账号信息的表(以及表模式等)极为重要,对它们的任何毁坏都可能严重地伤害到MySQL服务器。因此,相对于直接处理来说,最好是用标记和函数来处理这些表。

为重新命名一个用户账号,使用RENAME USER语句,如下所示:

RENAME USER ben TO bforta;

28.2.2 删除用户账号

删除一个用户账号(以及相关的权限),使用DROP USER语句,如下所示:

DROP USER bforta;

28.2.3 设置访问权限

创建用户账号后,必须接着分配访问权限。新创建的用户账号没有访问权限。它们能登录MySQL,但不能看到数据,不能执行任何数据库操作。

为看到赋予用户账号的权限,使用SHOW GRANTS FOR,如下所示:

SHOW GRANTS FOR bforta;

输出:

+-----------------------------------+
| Grants for bforta@%               |
+-----------------------------------+
| GRANT USAGE ON *.* TO 'bforta'@'%'|
+-----------------------------------+

输出结果显示用户bforta有一个权限USAGE ON .。USAGE表示根本没有权限(我知道,这不很直观),所以,此结果表示在
任意数据库和任意表上对任何东西没有权限。

grant 英 [ɡrɑːnt]	美 [ɡrænt] vt. 	授予; 
usage 英 [ˈjuːsɪdʒ]  n. 	使用; (词语的)用法
revoke 英 [rɪˈvəʊk] vt. 	撤销; 取消; 废除;

为设置权限,使用GRANT语句。GRANT要求你至少给出以下信息:

  1. 要授予的权限;
  2. 被授予访问权限的数据库或表;
  3. 用户名。

以下例子给出GRANT的用法:

GRANT SELECT ON crashcourse.* TO bforta;
  1. 此GRANT允许用户在crashcourse.*(crashcourse数据库的所有表)上使用SELECT。
  2. 通过只授予SELECT访问权限,用户bforta对crashcourse数据库中的所有数据具有只读访问权限。

SHOW GRANTS反映这个更改:

SHOW GRANTS FOR bforta;

输出:

+-------------------------------------------------+
| Grants for bforta@%                             |
+-------------------------------------------------+
| GRANT USAGE ON *.* TO 'bforta'@'%'              |
| GRANT SELECT ON 'crashcourse'.* TO 'bforta'@'%' |
+-------------------------------------------------+

每个GRANT添加(或更新)用户的一个权限。MySQL读取所有授权,并根据它们确定权限。
GRANT的反操作为REVOKE,用它来撤销特定的权限。下面举一个例子:

REVOKE SELECT ON crashcourse.* FROM bforta;

这条REVOKE语句取消刚赋予用户bforta的SELECT访问权限。被撤销的访问权限必须存在,否则会出错。

GRANT和REVOKE可在几个层次上控制访问权限:

  1. 整个服务器,使用GRANT ALL和REVOKE ALL;
  2. 整个数据库,使用ON database.*;
  3. 特定的表,使用ON database.table;
  4. 特定的列;
  5. 特定的存储过程

表28-1列出可以授予或撤销的每个权限。

				表28-1 权限

权 限 					说 明
---------------------------------------------------------------------------------------------------------
ALL						除GRANT OPTION外的所有权限
ALTER 					使用ALTER TABLE
ALTER 					ROUTINE 使用ALTER PROCEDURE和DROP PROCEDURE
CREATE 					使用CREATE TABLE
CREATE ROUTINE  		使用CREATE PROCEDURE
CREATE TEMPORARY TABLES	使用CREATE TEMPORARY TABLE
CREATE USER 			使用CREATE USER、DROP USER、RENAME USER和REVOKE ALL PRIVILEGES
CREATE VIEW 			使用CREATE VIEW
DELETE 					使用DELETE
DROP 					使用DROP TABLE
EXECUTE 				使用CALL和存储过程
FILE 					使用SELECT INTO OUTFILE和LOAD DATA INFILE
GRANT OPTION 			使用GRANT和REVOKE
INDEX 					使用CREATE INDEX和DROP INDEX
INSERT 					使用INSERT
LOCK TABLES 			使用LOCK TABLES
PROCESS 				使用SHOW FULL PROCESSLIST
RELOAD 					使用FLUSH
REPLICATION CLIENT 		服务器位置的访问
REPLICATION SLAVE 		由复制从属使用
SELECT 					使用SELECT
SHOW DATABASES 			使用SHOW DATABASES
SHOW VIEW 				使用SHOW CREATE VIEW
SHUTDOWN 				使用mysqladmin shutdown(用来关闭MySQL)
SUPER 					使用CHANGE MASTER、KILL、LOGS、PURGE、MASTER和SET GLOBAL。还允许mysqladmin调试登录
UPDATE 					使用UPDATE
USAGE 					无访问权限
---------------------------------------------------------------------------------------------------------

使用GRANT和REVOKE,再结合表28-1中列出的权限,你能对用户可以就你的宝贵数据做什么事情和不能做什么事情具有完全的控制。

简化多次授权 可通过列出各权限并用逗号分隔,将多条GRANT语句串在一起,如下所示:

GRANT SELECT,INSERT ON crashcourse.* TO bforta;

28.2.4 更改口令

为了更改用户口令,可使用SET PASSWORD语句。新口令必须如下加密:

SET PASSWORD FOR bforta = Password('n3w p@$$w0rd');

SET PASSWORD更新用户口令。新口令必须传递到Password()函数进行加密。

SET PASSWORD还可以用来设置你自己的口令:

SET PASSWORD = Password('n3w p@$$w0rd');

在不指定用户名时,SET PASSWORD更新当前登录用户的口令。

第 29 章 数据库维护

29.1 备份数据

像所有数据一样,MySQL的数据也必须经常备份。由于MySQL数据库是基于磁盘的文件,普通的备份系统和例程就能备份MySQL的数据。
但是,由于这些文件总是处于打开和使用状态,普通的文件副本备份不一定总是有效。

下面列出这个问题的可能解决方案。

  1. 使用命令行实用程序mysqldump转储所有数据库内容到某个外部文件。在进行常规备份前这个实用程序应该正常运行,以便能正确地备份转储文件。
  2. 可用命令行实用程序mysqlhotcopy从一个数据库复制所有数据(并非所有数据库引擎都支持这个实用程序)。
  3. 可以使用MySQL的BACKUP TABLE或SELECT INTO OUTFILE转储所有数据到某个外部文件。这两条语句都接受将要创建的系统文件名,此系统文件必须不存在,否则会出错。数据可以用RESTORE TABLE来复原。

首先刷新未写数据 为了保证所有数据被写到磁盘(包括索引数据),可能需要在进行备份前使用FLUSH TABLES语句。

29.2 进行数据库维护

以下是你应该知道的一些语句。

  1. ANALYZE TABLE,用来检查表键是否正确。ANALYZE TABLE返回如下所示的状态信息:

     ANAlYZE TABLE orders;
    

输出:

+-------------------+-----------+--------------+------------+
| Table             |  Op       | Msg_type     | Msg_text   |
+-------------------+-----------+--------------+------------+
| crashcourse.orders|  analyze  | status       | OK         |
+-------------------+-----------+--------------+------------+
  1. CHECK TABLE用来针对许多问题对表进行检查。在MyISAM表上还对索引进行检查。CHECK TABLE支持一系列的用于MyISAM表的方式。
    CHANGED检查自最后一次检查以来改动过的表。EXTENDED执行最彻底的检查,FAST只检查未正常关闭的表,MEDIUM检查所有被删除的链接并进行键检验,QUICK只进行快速扫描。如下所示,CHECK TABLE发现和修复问题:

     CHECK TABLE orders,orderitems;
    

输出:

+-----------------------+-----------+--------------+---------------------------+
| Table                 |  Op       | Msg_type     | Msg_text                  |
+-----------------------+-----------+--------------+---------------------------+
| crashcourse.orders    |  check    | status       | OK                        |
+-----------------------+-----------+--------------+---------------------------+
| crashcourse.orderitems|  check    | warning      | Table is marked as crashed|     
| crashcourse.orderitems|  check    | status       | OK                        |
+-----------------------+-----------+--------------+---------------------------+
  1. 如果MyISAM表访问产生不正确和不一致的结果,可能需要用REPAIR TABLE来修复相应的表。这条语句不应该经常使用,如果需要经常使用,可能会有更大的问题要解决。
  2. 如果从一个表中删除大量数据,应该使用OPTIMIZE TABLE来收回所用的空间,从而优化表的性能。

29.3 诊断启动问题

服务器启动问题通常在对MySQL配置或服务器本身进行更改时出现。MySQL在这个问题发生时报告错误,但由于多数MySQL服务器是作为系统进程或服务自动启动的,这些消息可能看不到。

在排除系统启动问题时,首先应该尽量用手动启动服务器。MySQL服务器自身通过在命令行上执行mysqld启动。下面是几个重要的mysqld命令行选项:

--help显示帮助——一个选项列表;
--safe-mode装载减去某些最佳配置的服务器;
--verbose显示全文本消息(为获得更详细的帮助消息与--help联合使用);
--version显示版本信息然后退出。

几个另外的命令行选项(与日志文件的使用有关)在下一节列出。

29.4 查看日志文件

MySQL维护管理员依赖的一系列日志文件。主要的日志文件有以下几种。

  1. 错误日志。它包含启动和关闭问题以及任意关键错误的细节。此日志通常名为hostname.err,位于data目录中。此日志名可用–log-error命令行选项更改。
  2. 查询日志。它记录所有MySQL活动,在诊断问题时非常有用。此日志文件可能会很快地变得非常大,因此不应该长期使用它。此日志通常名为hostname.log,位于data目录中。此名字可以用–log命令行选项更改。
  3. 二进制日志。它记录更新过数据(或者可能更新过数据)的所有语句。此日志通常名为hostname-bin,位于data目录内。此名字可以用–log-bin命令行选项更改。注意,这个日志文件是MySQL5中添加的,以前的MySQL版本中使用的是更新日志。
  4. 缓慢查询日志。顾名思义,此日志记录执行缓慢的任何查询。这个日志在确定数据库何处需要优化很有用。此日志通常名为hostname-slow.log,位于data目录中。此名字可以用–log-slow-queries命令行选项更改。

在使用日志时,可用FLUSH LOGS语句来刷新和重新开始所有日志文件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值