分组数据
数据分组
分组允许把数据分为多个逻辑组,以便能对每个组进行聚集计算。WHERE
过滤指定的是行而不是分组。事实
上,WHERE
没有分组的概念。
创建分组
分组是在SELECT
语句的GROUP BY
子句中建立的。
1 | SELECT vend_id, COUNT(*) AS num_prods FROM products |
上面的SELECT
语句指定了两个列,vend_id和num_prods为计算字段(用COUNT(*)
函数建立),GROUP BY
子句指示MySQL
按vend_id排序并分组数据。这导致对每个vend_id而不是整个表计算num_prods一次。
因为使用了GROUP BY
,就不必指定要计算和估值的每个组了。系统会自动完成。GROUP BY子
句指示MySQL
分组数据,然后对每个组而不是整个结果集进行聚集。
在具体使用GROUP BY
子句前,需要知道一些重要的规定:
GROUP BY
子句可以包含任意数目的列。这使得能对分组进行嵌套,为数据分组提供更细致的控制。- 如果在
GROUP BY
子句中嵌套了分组,数据将在最后规定的分组上进行汇总。换句话说,在建立分组时,指定的所有列都一起计算(所以不能从个别的列取回数据)。 GROUP BY
子句中列出的每个列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在SELECT
中使用表达式,则必须在GROUP BY
子句中指定相同的表达式。不能使用别名。- 除聚集计算语句外,
SELECT
语句中的每个列都必须在GROUP BY
子句中给出。 - 如果分组列中具有
NULL
值,则NULL
将作为一个分组返回。如果列中有多行NULL
值,它们将分为一组。 GROUP BY
子句必须出现在WHERE
子句之后,ORDER BY
子句之前。
ROLLUP: 使用WITH ROLLUP
关键字,可以得到每个分组以及每个分组汇总级别(针对每个分组)的值,如下所示:
1 | SELECT vend_id, COUNT(*) AS num_prods FROM products |
过滤分组
除了能用GROUP BY
分组数据外,MySQL
还允许过滤分组,规定包括哪些分组,排除哪些分组。
MySQL
提供了HAVING
子句,HAVING
非常类似于WHERE
。唯一的差别是WHERE
过滤行,而HAVING
过滤分组。
1 | SELECT cust_id, COUNT(*) AS orders FROM orders |
同时使用WHERE
和HAVING
子句:
下面的语句,列出具有2个(含)以上、prod_price为10(含)以上的产品的vend_id:
1 | SELECT vend_id, COUNT(*) AS num_prods FROM products |
WHERE
子句过滤所有prod_price至少为10的行。然后按vend_id分组数据,HAVING
子句过滤计数为2或2以上的分组。
分组和排序
我们经常发现用GROUP BY
分组的数据确实是以分组顺序输出的。但情况并不总是这样,它并不是SQL
规范所要求的。
一般在使用GROUP BY
子句时,应该也给出ORDER BY
子句。这是保证数据正确排序的唯一方法。千万不要仅依赖GROUP BY
排序数据。
下面的SELECT
语句,它检索总计订单价格大于等于50的订单的order_num和总计订单价格:
1 | SELECT order_num, SUM(quantity*item_price) AS ordertotal |
为按总计订单价格排序输出,需要添加ORDER BY
子句,如下所示:
1 | SELECT order_num, SUM(quantity*item_price) AS ordertotal |
SELECT子句顺序
SELECT
语句中使用时必须遵循的次序:
字句 | 说明 | 是否必须使用 |
---|---|---|
SELECT | 要返回的列或表达式 | 是 |
FROM | 从中检索数据的表 | 仅在从表选择数据时使用 |
WHERE | 行级过滤 | 否 |
GROUP BY | 分组说明 | 仅在按组计算聚集时使用 |
HAVING | 组级过滤 | 否 |
ORDER BY | 输出排序顺序 | 否 |
LIMIT | 要检索的行数 | 否 |
使用子查询
子查询
SQL
还允许创建子查询(subquery),即嵌套在其他查询中的查询。
利用子查询进行过滤
订单存储在两个表中。对于包含订单号、客户ID、订单日期的每个订单,orders表存储一行。各订单的物品存储在相关的orderitems表中。orders表不存储客户信息。它只存储客户的ID。实际的客户信息存储在customers表中。
假如需要列出订购物品TNT2的所有客户:
检索包含物品TNT2的所有订单的编号:
1
SELECT order_num FROM orderitems WHERE prod_id = 'TNT2';
检索具有前一步骤列出的订单编号(20005, 20007)的所有客户的ID:
1
SELECT cust_id FROM orders WHERE order_num IN (20005, 20007)
检索前一步骤返回的所有客户ID(10001, 10004)的客户信息:
1
SELECT cust_name, cust_contact FROM customers WHERE cust_id IN (10001, 10004);
可以将上面的WHERE
字句转换为子查询:
1 | SELECT cust_name, cust_contact FROM customers |
执行上述SELECT
语句,MySQL
实际上必须执行3条SELECT
语句。最里边的子查询返回order_num列表,此列表用于其外面的子查询的WHERE
子句。外面的子查询返回cust_id列表,此cust_id列表用于最外层查询的WHERE
子句。最外层查询确实返回所需的数据。
在WHERE
子句中使用子查询能够编写出功能很强并且很灵活的SQL
语句。对于能嵌套的子查询的数目没有限制,不过在实际使用时由于性能的限制,不能嵌套太多的子查询。
作为计算字段使用子查询
使用子查询的另一方法是创建计算字段。假如需要显示customers表中每个客户的订单总数。订单与相应的客户ID存储在orders表中。
为了执行这个操作,遵循下面的步骤:
- 从customers表中检索客户列表。
- 对于检索出的每个客户,统计其在orders表中的订单数目。
可使用SELECT COUNT(*)
对表中的行进行计数,并且通过提供一条WHERE
子句来过滤某个特定的客户ID,可仅对该客户的订单进行计数。例如,下面的代码对客户10001的订单进行计数:
1 | SELECT COUNT(*) AS orders FROM orders WHERE cust_id = 10001; |
为了对每个客户执行COUNT(*)
计算,应该将COUNT(*)
作为一个子查询:
1 | SELECT cust_name, cust_state, (SELECT COUNT(*) |
这条SELECT
语句对customers 表中每个客户返回3 列:cust_name、cust_state和orders。orders是一个计算字段,它是由圆括号中的子查询建立的。该子查询对检索出的每个客户执行一次。
联结表
联结
SQL
最强大的功能之一就是能在数据检索查询的执行中联结(join)表。
关系表
关系表的设计就是要保证把信息分解成多个表,一类数据一个表。各表通过某些常用的值互相关联。
外键: 为某个表中的一列,它包含另一个表的主键值,定义了两个表之间的关系。
为什么要使用联结
联结是一种机制,用来在一条SELECT
语句中关联表,因此称之为联结。使用特殊的语法,可以联结多个表返回一组输出,联结在运行时关联表中正确的行。
创建联结
联结的创建非常简单,规定要联结的所有表以及它们如何关联即可。
1 | SELECT vend_name, prod_name, prod_price |
SELECT
语句指定的两个列(prod_name和prod_price)在一个表中,而另一个列(vend_name)在另一个表中。FROM
子句列出了两个表,分别是vendors和products。WHERE
子句指示MySQL
匹配vendors表中的vend_id和products表中的vend_id。
匹配的两个列以vendors.vend_id 和products.vend_id指定。这里需要这种完全限定列名,因为如果只给出vend_id,则MySQL
不知道指的是哪一个(它们有两个,每个表中一个)。
完全限定列名: 在引用的列可能出现二义性时,必须使用完全限定列名(用一个点分隔的表名和列名)。如果引用一个没有用表名限制的具有二义性的列名,MySQL将返回错误。
不要忘了WHERE子句: 应该保证所有联结都有WHERE子句,否则MySQL
将返回比想要的数据多得多的数据。同理,应该保证WHERE
子句的正确性。不正确的过滤条件将导致MySQL
返回不正确的数据。
内部联结
目前为止所用的联结称为 等值联结 (equijoin),它基于两个表之间的相等测试。这种联结也称为 内部联结 。其实,对于这种联结可以使用稍微不同的语法来明确指定联结的类型。下面的SELECT
语句返回与前面例子完全相同的数据:
1 | SELECT vend_name, prod_name, prod_price |
两个表之间的关系是FROM
子句的组成部分,以INNER JOIN
指定。在使用这种语法时,联结条件用特定的ON
子句而不是WHERE
子句给出。传递给ON
的实际条件与传递给WHERE
的相同。
使用哪种语法: ANSI SQL
规范首选INNER JOIN
语法。此外,尽管使用WHERE
子句定义联结的确比较简单,但是使用明确的联结语法能够确保不会忘记联结条件,有时候这样做也能影响性能。
联结多个表
SQL
对一条SELECT
语句中可以联结的表的数目没有限制。创建联结的基本规则也相同。首先列出所有表,然后定义表之间的关系。
1 | SELECT prod_name, vend_name, prod_price, quantity |
此例子显示编号为20005的订单中的物品。订单物品存储在orderitems表中。每个产品按其产品ID存储,它引用products表中的产品。这些产品通过供应商ID联结到vendors表中相应的供应商,供应商ID存储在每个产品的记录中。
性能考虑: MySQL
在运行时关联指定的每个表以处理联结。这种处理可能是非常耗费资源的,因此不要联结不必要的表。联结的表越多,性能下降越厉害。
创建高级联结
使用表别名
别名除了用于列名和计算字段外,SQL
还允许给表名起别名。这样做有两个主要理由:
- 缩短SQL语句
- 允许在单条SELECT语句中多次使用相同的表
1 | SELECT cust_name, cust_contact |
FROM
子句中3个表全都具有别名。customers AS c
建立c作为customers的别名,等等。这使得能使用省写的c而不是全名customers。
应该注意,表别名只在查询执行中使用。与列别名不一样,表别名不返回到客户机。
使用不同类型的联结
查询要求首vend_id为DTNTR的物品的供应商,然后找出这个供应商生产的其他物品。
使用子查询:
1
2
3
4
5SELECT prod_id, prod_name
FROM products
WHERE vend_id = (SELECT vend_id
FROM products
WHERE prod_id = 'DTNTR');使用联结查询:
1
2
3
4SELECT 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';
products表在FROM
子句中出现了两次。使用了表别名。products的第一次出现为别名p1,第二次出现为别名p2。SELECT
语句使用p1前缀明确地给出所需列的全名。如果不这样,MySQL
将返回错误,因为分别存在两个名为prod_id、prod_name的列。MySQL
不知道想要的是哪一个列(即使它们事实上是同一个列)。WHERE
(通过匹配p1中的vend_id和p2中的vend_id)首先联结两个表,然后按第二个表中的prod_id过滤数据,返回所需的数据。
外部联结
许多联结将一个表中的行与另一个表中的行相关联。但有时候会需要包含没有关联行的那些行。这种类型的联结称为 外部联结 。
下面的SELECT
语句给出一个简单的内部联结。它检索所有客户及其订单:
1 | SELECT customers.cust_id, orders.order_num |
外部联结语法类似。为了检索所有客户,包括那些没有订单的客户,可如下进行:
1 | SELECT customers.cust_id, orders.order_num |
这条SELECT
语句使用了关键字OUTER JOIN
来指定联结的类型。但是,与内部联结关联两个表中的行不同的是,外部联结还包括没有关联行的行。在使用OUTER JOIN
语法时,必须使用RIGHT
或LEFT
关键字指定包括其所有行的表(RIGHT
指出的是OUTER JOIN
右边的表,而LEFT
指出的是OUTER JOIN
左边的表)。上面的例子使用LEFT OUTER JOIN
从FROM
子句的左边表(customers表)中选择所有行。
使用带聚集函数的联结
聚集函数可以与联结一起使用。
1 | SELECT customers.cust_name, customers.cust_id, COUNT(orders.order_num) |
此SELECT
语句使用INNER JOIN
将customers和orders表互相关联。GROUP BY
子句按客户分组数据, 因此, 函数调用COUNT(orders.order_num)
对每个客户的订单计数,将它作为num_ord返回。
组合查询
MySQL
也允许执行多个查询(多条SELECT
语句),并将结果作为单个查询结果集返回。这些组合查询通常称为 并 (union)或 复合查询 (compound query)。
有两种基本情况,其中需要使用组合查询:
- 单个查询中从不同的表返回类似结构的数据
- 对单个表执行多个查询,按单个查询返回数据
组合查询和多个WHERE条件: 多数情况下,组合相同表的两个查询完成的工作与具有多个WHERE
子句条件的单条查询完成的工作相同。
创建组合查询
利用UNION
,可给出多条SELECT
语句,将它们的结果组合成单个结果集。
假如需要prod_price小于等于5的所有物品的一个列表,而且还想包括vend_id为1001和1002生产的所有物品(不考虑价格)。
使用UNION:
1 | SELECT vend_id, prod_id, prod_price |
使用WHERE:
1 | SELECT vend_id, prod_id, prod_price |
在这个简单的例子中,使用UNION
可能比使用WHERE
子句更为复杂。但对于更复杂的过滤条件,或者从多个表(而不是单个表)中检索数据的情形,使用UNION
可能会使处理更简单。
UNION规则
进行UNION
时有几条规则需要注意:
UNION
必须由两条或两条以上的SELECT
语句组成,语句之间用关键字UNION
分隔(因此,如果组合4条SELECT
语句,将要使用3个UNION
关键字)UNION
中的每个查询必须包含相同的列、表达式或聚集函数(不过各个列不需要以相同的次序列出)- 列数据类型必须兼容:类型不必完全相同,但必须是DBMS可以隐含地转换的类型
包含或取消重复的行
UNION
从查询结果集中自动去除了重复的行,如果想不取消重复的行,可使用UNION ALL
而不是UNION
。
1 | SELECT vend_id, prod_id, prod_price |
对组合查询结果排序
SELECT
语句的输出用ORDER BY
子句排序。在用UNION
组合查询时,只能使用一条ORDER BY
子句,它必须出现在最后一条SELECT
语句之后。对于结果集,不存在用一种方式排序一部分,而又用另一种方式排序另一部分的情况,因此不允许使用多条ORDER BY
子句。
1 | SELECT vend_id, prod_id, prod_price |
全文本搜索
MyISAM引擎: 支持全文检索
InnoDB: 不支持全文检索
在使用全文本搜索时,MySQL
不需要分别查看每个行,不需要分别分析和处理每个词。MySQL
创建指定列中各词的一个索引,搜索可以针对这些词进行。
使用全文本搜索
为了进行全文本搜索,必须索引被搜索的列,而且要随着数据的改变不断地重新索引。在对表列进行适当设计后,MySQL
会自动进行所有的索引和重新索引。
启用全文本搜索支持
一般在创建表时启用全文本搜索。CREATE TABLE
语句接受FULLTEXT
子句,它给出被索引列的一个逗号分隔的列表。
1 | CREATE TABLE productnotes |
有一个名为note_text的列,为了进行全文本搜索,MySQL
根据子句FULLTEXT(note_text)
的指示对它进行索引。这里的FULLTEXT
索引单个列,如果需要也可以指定多个列。
在定义之后,MySQL
自动维护该索引。在增加、更新或删除行时,索引随之自动更新。
进行全文本搜索
在索引之后,使用两个函数Match()
和Against()
执行全文本搜索,其中Match()
指定被搜索的列,Against()
指定要使用的搜索表达式。
1 | SELECT note_text FROM productnotes |
Match(note_text)
指示MySQL
针对指定的列进行搜索,Against('rabbit')
指定词rabbit作为搜索文本。
传递给Match()
的值必须与FULLTEXT()
定义中的相同。如果指定多个列,则必须列出它们(而且次序正确)。
全文本搜索的一个重要部分就是对结果排序。具有较高等级的行先返回。等级由MySQL
根据行中词的数目、唯一词的数目、整个索引中词的总数以及包含该词的行的数目计算出来。
1 | SELECT note_text, Match(note_text) Against('rabbit') |
计算列(别名为rank),此列包含全文本搜索计算出的等级值。不包含词rabbit的行等级为0。
布尔文本搜索
MySQL
支持全文本搜索的另外一种形式,称为布尔方式。
下表列出支持的所有布尔操作符:
布尔操作符 | 说明 |
---|---|
+ | 包含,词必须存在 |
- | 排除,词必须不出现 |
> | 包含,而且增加等级值 |
< | 包含,且减少等级值 |
() | 把词组成子表达式(允许这些子表达式作为一个组被包含、排除、排列等) |
~ | 取消一个词的排序值 |
* | 词尾的通配符 |
“” | 定义一个短语(与单个词的列表不一样,它匹配整个短语以便包含或排除这个短语) |
匹配包含heavy但不包含任意以rope开始的词的行:
1 | SELECT note_text FROM productnotes |
搜索匹配包含词rabbit和bait的行:
1 | SELECT note_text FROM productnotes |
没有指定操作符,这个搜索匹配包含rabbit和bait中的至少一个词的行:
1 | SELECT note_text FROM productnotes |
匹配短语rabbit bait而不是匹配两个词rabbit和bait:
1 | SELECT note_text FROM productnotes |
匹配rabbit和carrot,增加前者的等级,降低后者的等级:
1 | SELECT note_text FROM productnotes |
搜索匹配词safe和combination,降低后者的等级:
1 | SELECT note_text FROM productnotes |
插入数据
插入完整的行
1 | INSERT INTO customers |
虽然这种语法很简单,但并不安全,应该尽量避免使用。上面的SQL
语句高度依赖于表中列的定义次序,并且还依赖于其次序容易获得的信息。即使可得到这种次序信息,也不能保证下一次表结构变动后各个列保持完全相同的次序。
编写INSERT语句的更安全的方法如下:
1 | INSERT INTO customers(cust_name, |
在表名后的括号里明确地给出了列名。在插入行时,MySQL
将用VALUES
列表中的相应值填入列表中的对应项。VALUES
中的第一个值对应于第一个指定的列名。第二个值对应于第二个列名,如此等等。
省略列: 如果表的定义允许,则可以在INSERT
操作中省略某些列。省略的列必须满足以下某个条件。
- 该列定义为允许NULL值(无值或空值)。
- 在表定义中给出默认值。这表示如果不给出值,将使用默认值。
插入多个行
1 | INSERT INTO customers(cust_name, |
INSERT
语句有多组值,每组值用一对圆括号括起来,用逗号分隔。
插入检索出的数据
INSERT
还存在另一种形式,可以利用它将一条SELECT
语句的结果插入表中。
1 | INSERT INTO customers(cust_id, cust_contact, cust_email, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country) |
INSERT SELECT中的列名: 在INSERT
和SELECT
语句中不一定要求列名匹配。它使用的是列的位置,因此SELECT
中的第一列(不管其列名)将用来填充表列中指定的第一个列,第二列将用来填充表列中指定的第二个列,如此等等。
更新和删除数据
更新数据
为了更新(修改)表中的数据,可使用UPDATE
语句。
1 | UPDATE customers SET cust_email = 'elmer@fudd.com' |
更新多个列:
1 | UPDATE customers SET cust_name = 'The Fudds', cust_email = 'elmer@fudd.com' |
为了删除某个列的值,可设置它为NULL:
1 | UPDATE customers SET cust_email = NULL |
删除数据
为了从一个表中删除(去掉)数据,使用DELETE
语句。
1 | DELETE FROM customers WHERE cust_id = 10006; |
如果执行UPDATE
而不带WHERE
子句,则表中每个行都将用新值更新。类似地,如果执行DELETE
语句而不带WHERE
子句,表的所有数据都将被删除。
创建和操纵表
CREATE TABLE
创建表,必须给出下列信息:
- 新表的名字,在关键字
CREATE TABLE
之后给出 - 表列的名字和定义,用逗号分隔。
1 | CREATE TABLE customers |
每列的定义以列名(它在表中必须是唯一的)开始,后跟列的数据类型,表的主键可以在创建表时用PRIMARY KEY
关键字指定。这里,列cust_id指定作为主键列。
使用NULL值
NULL
值就是没有值或缺值。允许NULL
值的列也允许在插入行时不给出该列的值。不允许NULL
值的列,在插入或更新行时,该列必须有值。
主键
表中的每个行必须具有唯一的主键值。如果主键使用单个列,则它的值必须唯一。如果使用多个列,则这些列的组合值必须唯一。
主键中只能使用不允许NULL值的列。允许NULL值的列不能作为唯一标识。
使用AUTO_INCREMENT
AUTO_INCREMENT
告诉MySQL
,自动对该列增量。每个表只允许一个AUTO_INCREMENT
列,而且它必须被索引(如,通过使它成为主键)。
指定默认值
默认值用CREATE TABLE
语句的列定义中的DEFAULT
关键字指定。
1 | CREATE TABLE orderitems |
引擎类型
MySQL
有一个具体管理和处理数据的内部引擎。在使用CREATE TABLE
语句时,该引擎具体创建表,而在使用SELECT
语句或进行其他数据库处理时,该引擎在内部处理你的请求。多数时候,此引擎都隐藏在DBMS内,不需要过多关注它。
- InnoDB是一个可靠的事务处理引擎,它不支持全文本搜索
- MEMORY在功能等同于MyISAM,但由于数据存储在内存(不是磁盘)中,速度很快(特别适合于临时表)
- MyISAM是一个性能极高的引擎,它支持全文本搜索,但不支持事务处理。
外键不能跨引擎: 外键不能跨引擎,即使用一个引擎的表不能引用具有使用不同引擎的表的外键。
更新表
更新表
为更新表定义,可使用ALTER TABLE
语句。
给表添加一个列:
1 | ALTER TABLE vendors ADD vend_phone CHAR(20); |
删除列:
1 | ALTER TABLE vendors DROP COLUMN vend_phone; |
ALTER TABLE
的一种常见用途是定义外键:
1 | ALTER TABLE orderitems |
删除表
删除表使用DROP TABLE
语句:
1 | DROP TABLE customers; |
重命名表
1 | RENAME TABLE customers2 TO customers; |
使用视图
视图
视图是虚拟的表。与包含数据的表不一样,视图只包含使用时动态检索数据的查询。
为什么使用视图
- 重用
SQL
语句。 - 简化复杂的
SQL
操作。在编写查询后,可以方便地重用它而不必知道它的基本查询细节。 - 使用表的组成部分而不是整个表。
- 保护数据。可以给用户授予表的特定部分的访问权限而不是整个表的访问权限。
- 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。
使用视图
- 视图用
CREATE VIEW
语句来创建。 - 使用
SHOW CREATE VIEW viewname
;来查看创建视图的语句。 - 用
DROP
删除视图,其语法为DROP VIEW viewname
; - 更新视图时,可以先用
DROP
再用CREATE
,也可以直接用CREATE ORREPLACE VIEW
。
利用视图简化复杂的联结
1 | CREATE VIEW productcustomers AS |
这条语句创建一个名为productcustomers的视图,它联结三个表,以返回已订购了任意产品的所有客户的列表。如果执行SELECT * FROM productcustomers
,将列出订购了任意产品的客户。
为检索订购了产品TNT2的客户,可如下进行:
1 | SELECT cust_name, cust_contact |
用视图重新格式化检索出的数据
下面的SELECT
语句在单个组合计算列中返回供应商名和位置:
1 | SELECT Concat(RTrim(vend_name), '(', RTrim(vend_country), ')') AS vend_title |
假如经常需要这个格式的结果。不必在每次需要时执行联结,创建一个视图,每次需要时使用它即可。
1 | CREATE VIEW vendorlocations AS |
使用SELECT
语句相查询的视图:
1 | SELECT * FROM vendorlocations; |
用视图过滤不想要的数据
可以定义customeremaillist视图,它过滤没有电子邮件地址的客户:
1 | CREATE VIEW customeremaillist AS |
使用视图与计算字段
下面SELECT
语句。它检索某个特定订单中的物品,计算每种物品的总价格:
1 | SELECT order_num, prod_id, quantity, item_price, |
将其转换为一个视图:
1 | CREATE VIEW orderitemsexpanded AS |
检索订单20005的详细内容:
1 | SELECT * FROM orderitemsexpanded |
更新视图
视图主要用于数据检索。对视图增加或删除行,实际上是对其基表增加或删除行。
并非所有视图都是可更新的。基本上可以说,如果MySQL
不能正确地确定被更新的基数据,则不允许更新(包括插入和删除)。这实际上意味着,如果视图定义中有以下操作,则不能进行视图的更新:
- 分组(使用GROUP BY和HAVING)
- 联结
- 子查询
- 并
- 聚集函数(Min()、Count()、Sum()等)
- DISTINCT
- 导出(计算)
使用存储过程
存储过程简单来说,就是为以后的使用而保存的一条或多条MySQL
语句的集合。可将其视为批文件,虽然它们的作用不仅限于批处理。
使用存储过程有3个主要的好处,即简单、安全、高性能。
创建存储过程
1 | DELIMITER // |
此存储过程名为productpricing,用CREATE PROCEDURE productpricing()
语句定义。如果存储过程接受参数,它们将在()中列举出来。此存储过程没有参数,但后跟的()仍然需要。BEGIN
和END
语句用来限定存储过程体,过程体本身仅是一个简单的SELECT
语句。
mysql命令行客户机的分隔符: mysql
命令行实用程序使用;
作为语句分隔符。如果命令行实用程序要解释存储过程自身内的;
字符,则它们最终不会成为存储过程的成分,这会使存储过程中的SQL
出现句法错误。 解决办法是临时更改命令行实用程序的语句分隔符,DELIMITER //
。为恢复为原来的语句分隔符,可使用DELIMITER ;
。
使用存储过程
1 | CALL productpricing(); |
CALL productpricing();
执行刚创建的存储过程并显示返回的结果。因为存储过程实际上是一种函数,所以存储过程名后需要有()符号(即使不传递参数也需要)。
删除存储过程
存储过程在创建之后,被保存在服务器上以供使用,直至被删除。删除命令从服务器中删除存储过程。
1 | DROP PROCEDURE productpricing; |
使用参数
一般,存储过程并不显示结果,而是把结果返回给你指定的变量。
1 | CREATE PROCEDURE productpricing( |
此存储过程接受3个参数:pl存储产品最低价格,ph存储产品最高价格,pa存储产品平均价格。每个参数必须具有指定的类型,这里使用十进制值。关键字OUT
指出相应的参数用来从存储过程传出一个值(返回给调用者)。MySQL
支持IN
(传递给存储过程)、OUT
(从存储过程传出,如这里所用)和INOUT
(对存储过程传入和传出)类型的参数。存储过程的代码位于BEGIN
和END
语句内,它们是一系列SELECT
语句,用来检索值,然后保存到相应的变量(通过指定INTO
关键字)。
参数的数据类型: 存储过程的参数允许的数据类型与表中使用的数据类型相同。注意,记录集不是允许的类型,因此,不能通过一个参数返回多个行和列。这就是前面的例子为什么要使用3个参数(和3条SELECT
语句)的原因。
为调用此修改过的存储过程,必须指定3个变量名,如下所示:
1 | CALL productpricing(@pricelow, @pricehigh, @priceaverage); |
由于此存储过程要求3个参数,因此必须正好传递3个参数,不多也不少。所以,这条CALL
语句给出3个参数。它们是存储过程将保存结果的3个变量的名字。
变量名: 所有MySQL
变量都必须以@
开始。
在调用时,这条语句并不显示任何数据。它返回以后可以显示的变量。
为了显示检索出的产品平均价格,可如下进行:
1 | SELECT @priceaverage; |
为了获得3个值,可使用以下语句:
1 | SELECT @priceaverage @pricehigh, @pricelow |
下面是另外一个例子,这次使用IN
和OUT
参数。ordertotal接受订单号并返回该订单的合计:
1 | CREATE PROCEDURE ordertotal( |
onumber定义为IN
,因为订单号被传入存储过程。ototal定义为OUT
,因为要从存储过程返回合计。SELECT
语句使用这两个参数,WHERE
子句使用onumber选择正确的行,INTO
使用ototal存储计算出来的合计。
为调用这个新存储过程,可使用以下语句:
1 | CALL ordertotal(20005, @total); |
必须给ordertotal传递两个参数;第一个参数为订单号,第二个参数为包含计算出来的合计的变量名。
为了显示此合计,可如下进行:
1 | SELECT @total; |
检查存储过程
为显示用来创建一个存储过程的CREATE
语句,使用SHOW CREATE PROCEDURE
语句:
1 | SHOW CREATE PROCEDURE ordertotal |
为了获得包括何时、由谁创建等详细信息的存储过程列表,使用SHOW PROCEDURE STATUS
。
使用游标
游标(cursor)是一个存储在MySQL
服务器上的数据库查询,它不是一条SELECT
语句,而是被该语句检索出来的结果集。在存储了游标之后,应用程序可以根据需要滚动或浏览其中的数据。
不像多数DBMS
,MySQL
游标只能用于存储过程(和函数)。
创建游标
游标用DECLARE
语句创建。DECLARE
命名游标,并定义相应的SELECT
语句,根据需要带WHERE
和其他子句。
1 | CREATE PROCEDURE processorders() |
DECLARE
语句用来定义和命名游标,这里为ordernumbers。存储过程处理完成后,游标就消失(因为它局限于存储过程)。
打开和关闭游标
游标用OPEN CURSOR
语句来打开:
1 | OPEN ordernumbers; |
在处理OPEN
语句时执行查询,存储检索出的数据以供浏览和滚动。
游标处理完成后,应当使用如下语句关闭游标:
1 | CLOSE ordernumbers; |
CLOSE
释放游标使用的所有内部内存和资源,因此在每个游标不再需要时都应该关闭。
隐含关闭: 如果你不明确关闭游标,MySQL
将会在到达END
语句时自动关闭它。
1 | CREATE PROCEDURE processorders() |
使用游标数据
在一个游标被打开后,可以使用FETCH
语句分别访问它的每一行。FETCH
指定检索什么数据(所需的列),检索出来的数据存储在什么地方。它还向前移动游标中的内部行指针,使下一条FETCH
语句检索下一行(不重复读取同一行)。
1 | CREATE PROCEDURE processorders() |
其中FETCH
用来检索当前行的order_num列(将自动从第一行开始)到一个名为o的局部声明的变量中。对检索出的数据不做任何处理。
循环检索数据,从第一行到最后一行:
1 | CREATE PROCEDURE processorders() |
FETCH
是在REPEAT
内,因此它反复执行直到done为真(由UNTIL done END REPEAT;
规定)。为使它起作用,用一个DEFAULT 0
(假,不结束)定义变量done。
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done=1;
这条语句定义了一个CONTINUE HANDLER
,它是在条件出现时被执行的代码。这里,它指出当SQLSTATE '02000'
出现时,SET done=1
。 SQLSTATE '02000'
是一个未找到条件,当REPEAT
由于没有更多的行供循环而不能继续时,出现这个条件。
如果一切正常,可以在循环内放入任意需要的处理(在FETCH
语句之后,循环结束之前)。
使用触发器
触发器是MySQL
响应以下任意语句而自动执行的一条MySQL
语句:
- DELETE
- INSERT
- UPDATE
其他MySQL
语句不支持触发器。
只有表才支持触发器,视图不支持(临时表也不支持)。
创建触发器
触发器用CREATE TRIGGER
语句创建:
1 | CREATE TRIGGER newproduct AFTER INSERT ON products |
CREATE TRIGGER
用来创建名为newproduct的新触发器。触发器可在一个操作发生之前或之后执行,这里给出了AFTER INSERT
,所以此触发器将在INSERT
语句成功执行后执行。这个触发器还指定FOR EACH ROW
,因此代码对每个插入行执行。在这个例子中,文本Product added将对每个插入的行显示一次。
触发器按每个表每个事件每次地定义,每个表每个事件每次只允许一个触发器。因此,每个表最多支持6个触发器(每条INSERT
、UPDATE
和DELETE
的之前和之后)。单一触发器不能与多个事件或多个表关联,所以,如果需要一个对INSERT
和UPDATE
操作执行的触发器,则应该定义两个触发器。
删除触发器
1 | DROP TRIGGER newproduct; |
管理事务处理
事务处理
事务处理(transaction processing)可以用来维护数据库的完整性,它保证成批的MySQL
操作要么完全执行,要么完全不执行。
事务处理的几个术语:
- 事务(transaction)指一组SQL语句
- 回退(rollback)指撤销指定SQL语句的过程
- 提交(commit)指将未存储的SQL语句结果写入数据库表
- 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),可以对它发布回退(与回退整个事务处理不同)。
控制事务处理
MySQL
使用下面的语句来标识事务的开始:
1 | START TRANSACTION; |
使用ROLLBACK
MySQL
的ROLLBACK
命令用来回退(撤销)MySQL
语句
哪些语句可以回退: 事务处理用来管理INSERT
、UPDATE
和DELETE
语句。不能回退SELECT
语句。(这样做也没有什么意义。)不能回退CREATE
或DROP
操作。事务处理块中可以使用这两条语句,但如果执行回退,它们不会被撤销。
使用COMMIT
一般的MySQL
语句都是直接针对数据库表执行和编写的。这就是所谓的隐含提交(implicit commit),即提交(写或保存)操作是自动进行的。
但是,在事务处理块中,提交不会隐含地进行。为进行明确的提交,使用COMMIT
语句。
1 | START TRANSCATION; |
从系统中完全删除订单20010。因为涉及更新两个数据库表orders和orderItems,所以使用事务处理块来保证订单不被部分删除。最后的COMMIT
语句仅在不出错时写出更改。如果第一条DELETE
起作用,但第二条失败,则DELETE
不会提交(实际上,它是被自动撤销的)。
隐含事务关闭: 当COMMIT
或ROLLBACK
语句执行后,事务会自动关闭。
使用保留点
简单的ROLLBACK
和COMMIT
语句就可以写入或撤销整个事务处理。但是,只是对简单的事务处理才能这样做,更复杂的事务处理可能需要部分提交或回退。
为了支持回退部分事务处理,必须能在事务处理块中合适的位置放置占位符。这样,如果需要回退,可以回退到某个占位符。
这些占位符称为保留点。为了创建占位符,可如下使用SAVEPOINT
语句:
1 | SAVEPOINT delete1; |
每个保留点都取标识它的唯一名字,以便在回退时,MySQL
知道要回退到何处。为了回退到给出的保留点,可如下进行:
1 | ROLLBACK TO delete1; |
释放保留点: 保留点在事务处理完成(执行一条ROLLBACK
或COMMIT
)后自动释放。