本文来自本·福达的《SQL必知必会(第5版)》的学习总结。
如果之前学习过SQL相关知识,这篇文档将很有用。
开始:
- SQL语句不区分大小写,因此SELECT与select是相同的。同样,写成Select也没有关系。
- 检索的基本语法:
检索单个列:
SELECT prod_name FROM Products;
检索多个列:
SELECT prod_id, prod_name, prod_price FROM Products;
检索所有列:
SELECT * FROM Products
注意:除非你确实需要表中的每一列,否则最好别使用*通配符。
使用通配符有一个大优点。由于不明确指定列名(因为星号检索每一列),所以能检索出名字未知的列。
- DISTINCT关键字:
SELECT DISTINCT vend_id FROM Products;
指示数据库只返回不同的值,可用于去重。
DISTINCT关键字作用于所有的列,不仅仅是跟在其后的那一列。
DISTINCT关键字是检查所有列组合的不同。
例如,你指定SELECT DISTINCT vend_id, prod_price,则指定的两列组合起来所有不同的结果都将被检测出来。
- 限制结果
在SQL Server中返回前几行:
SELECT TOP 5 prod_name FROM Products;
在DB2中返回前几行:
SELECT prod_name FROM Products FETCH FIRST 5 ROWS ONLY;
在Oracle中返回前几行:
SELECT prod_name FROM Products WHERE ROWNUM <=5;
在MySQL、MariaDB、PostgreSQL或者SQLite中返回前几行
SELECT prod_name FROM Products LIMIT 5;
注意:第0行第一个被检索的行是第0行,而不是第1行。
- 使用注释
行内注释:
SELECT prod_name -- 这是一条注释
FROM Products;
注释使用–(两个连字符)嵌在行内。
🌂 下面是另一种形式的行内注释(有些DBMS不支持这种形式):
# 这是一条注释
SELECT prod_name FROM Products;
🌂 多行注释,注释可以在脚本的任何位置停止和开始。
/* SELECT prod_name, vend_id FROM Products; */
- 关系数据库设计理论认为,如果不明确规定排序顺序,则不应该假定检索出的数据的顺序有任何意义。
- ORDER BY子句取一个或多个列的名字,据此对输出进行排序:
SELECT prod_name
FROM Products
ORDER BY prod_name;
注意:ORDER BY子句的位置在指定一条ORDER BY子句时,应该保证它是SELECT语句中最后一条子句。如果它不是最后的子句,将会出错。
- 按多个列排序
列名之间用逗号分开即可(就像选择多个列时那样)
SELECT prod_id, prod_price, prod_name
FROM Products
ORDER BY prod_price, prod_name;
ORDER BY支持按相对列位置进行排序:
SELECT prod_id, prod_price, prod_name
FROM Products
ORDER BY 2, 3;
- ORDER BY 2, 3表示先按prod_price,再按prod_name进行排序。
可混合使用实际列名和相对列位置。
- 指定排序方向
为了进行降序排序,必须指定DESC关键字:
SELECT prod_id, prod_price, prod_name
FROM Products
ORDER BY prod_price DESC;
下面的例子以降序排序产品(最贵的在最前面),再加上产品名:
SELECT prod_id, prod_price, prod_name
FROM Products
ORDER BY prod_price DESC, prod_name;
DESC关键字只应用到直接位于其前面的列名,如果想在多个列上进行降序排序,必须对每一列指定DESC关键字。
DESC是DESCENDING的缩写,这两个关键字都可以使用。
与DESC相对的是ASC(或ASCENDING),在升序排序时可以指定它。
- 使用WHERE子句
在SELECT语句中,数据根据WHERE子句中指定的搜索条件进行过滤。WHERE子句在表名(FROM子句)之后给出:
SELECT prod_name, prod_price
FROM Products
WHERE prod_price = 3.49;
🌂 在同时使用ORDER BY和WHERE子句时,应该让ORDER BY位于WHERE之后,否则将会产生错误
- WHERE子句操作符:
SELECT prod_name, prod_price
FROM Products
WHERE prod_price < 10;
并非所有DBMS都支持这些操作符。
- 单引号用来限定字符串。如果将值与字符串类型的列进行比较,就需要限定引号。用来与数值列进行比较的值不用引号。
- BETWEEN操作符,检索价格在5美元和10美元之间的所有产品:
SELECT prod_name, prod_price
FROM Products
WHERE prod_price BETWEEN 5 AND 10;
- NULL的使用:
SELECT prod_name FROM Products WHERE prod_price IS NULL;
SELECT prod_name FROM Products WHERE prod_price IS NOT NULL;
NULL无值(no value),它与字段包含0、空字符串或仅仅包含空格不是一回事。
- 组合WHERE子句
SQL允许给出多个WHERE子句,以AND或OR子句连接。
要通过不止一个列进行过滤,可以使用AND操作符给WHERE子句附加条件:
SELECT prod_id, prod_price, prod_name
FROM Products
WHERE vend_id = 'DLL01' AND prod_price <= 4;
🌂 OR操作符指示DBMS检索匹配任一条件的行,第一个条件得到满足,就不再计算第二个条件:
SELECT prod_id, prod_price, prod_name
FROM Products
WHERE vend_id = 'DLL01' OR vend_id = 'BRS01';
🌂 WHERE子句可以包含任意数目的AND和OR操作符。
🌂 SQL(像多数语言一样)在处理OR操作符前,优先处理AND操作符。
🌂 最好使用圆括号对操作符进行明确分组
- IN操作符:用来指定条件范围,取一组由逗号分隔、括在圆括号中的合法值,完成了与OR相同的功能:
SELECT prod_name, prod_price
FROM Products
WHERE vend_id IN ('DLL01','BRS01')
ORDER BY prod_name;
IN操作符一般比一组OR操作符执行得更快。
IN的最大优点是可以包含其他SELECT语句,能够更动态地建立WHERE子句。
- NOT操作符:
🌂 WHERE子句中的NOT操作符有且只有一个功能,那就是否定其后所跟的任何条件。
🌂 NOT关键字可以用在要过滤的列前,而不仅是在其后。
🌂 大多数DBMS允许使用NOT否定任何条件。
- LIKE和通配符
🌂 LIKE是谓词而不是操作符
🌂 通配符(wildcard)用来匹配值的一部分的特殊字符。
🌂 搜索模式(search pattern)由字面值、通配符或两者组合构成的搜索条件。
🌂 通配符搜索只能用于文本字段(字符串),非文本数据类型字段不能使用通配符搜索。
🌂 在搜索串中,%表示任何字符出现任意次数:
SELECT prod_id, prod_name FROM Products
WHERE prod_name LIKE 'Fish%';
上面的SQL语句将检索任意以Fish起头的词。%告诉DBMS接受Fish之后的任意字符,不管它有多少字符。
SELECT prod_id, prod_name
FROM Products
WHERE prod_name LIKE '%bean bag%';
上面的SQL语句中,搜索模式’%bean bag%’表示匹配任何位置上包含文本bean bag的值,不论它之前或之后出现什么字符。
SELECT prod_name
FROM Products
WHERE prod_name LIKE 'F%y';
🌂 %还能匹配0个字符。%代表搜索模式中给定位置的0个、1个或多个字符。
🌂 通配符%不能匹配NULL。
🌂 只匹配单个字符(DB2不支持通配符):
SELECT prod_id, prod_name
FROM Products
WHERE prod_name LIKE '__ inch teddy bear';
🌂 找出所有名字以J或M起头的联系人,可进行如下查询:
SELECT cust_contact
FROM Customers
WHERE cust_contact LIKE '[JM]%'
ORDER BY cust_contact;
可以用前缀字符^(脱字号)来否定:
SELECT cust_contact
FROM Customers
WHERE cust_contact LIKE '[^JM]%'
ORDER BY cust_contact;
上面的查询匹配以J和M之外的任意字符起头的任意联系人名,也可以使用NOT操作符得出类似的结果:
SELECT cust_contact
FROM Customers
WHERE NOT cust_contact LIKE '[JM]%'
ORDER BY cust_contact;
🌂 不要过度使用通配符。如果其他操作符能达到相同的目的,应该使用其他操作符。
🌂 在确实需要使用通配符时,也尽量不要把它们用在搜索模式的开始处。把通配符置于开始处,搜索起来是最慢的。
-
计算字段
在SQL语句内可完成的许多转换和格式化工作都可以直接在客户端应用程序内完成。但一般来说,在数据库服务器上完成这些操作比在客户端中完成要快得多。 -
拼接字段
把两个列拼接起来。在SELECT语句中,可拼接两个列。根据所用的DBMS,可用加号(+)或两个竖杠(||)表示。在MySQL和MariaDB中,必须使用特殊的函数:
SELECT vend_name + '(' + vend_country + ')'
FROM Vendors ORDER BY vend_name;
SELECT vend_name || '(' || vend_country || ')'
FROM Vendors ORDER BY vend_name;
下面是使用MySQL或MariaDB时需要使用的语句:
SELECT Concat(vend_name, ' (', vend_country, ')')
FROM Vendors ORDER BY vend_name;
- RTRIM()、LTRIM()、TRIM()函数可用于去空格:
SELECT RTRIM(vend_name) + ' (' + RTRIM(vend_country) + ')'
FROM Vendors
ORDER BY vend_name;
RTRIM()(去掉字符串右边的空格)、LTRIM()(去掉字符串左边的空格)以及TRIM()(去掉字符串左右两边的空格)。
- 别名用AS关键字赋予:
SELECT RTRIM(vend_name) + ' (' + RTRIM(vend_country) + ')' AS vend_title
FROM Vendors ORDER BY vend_name;
现在列名为vend_title,任何客户端应用都可以按名称引用这个列,就像它是一个实际的表列一样。在很多DBMS中,AS关键字是可选的,不过最好使用它,这被视为一条最佳实践。
- 执行算术计算
检索订单号为20008中的所有物品:
SELECT prod_id, quantity, item_price
FROM OrderItems
WHERE order_num = 20008;
汇总物品的价格(单价乘以订购数量):
SELECT prod_id, quantity, item_price,
quantity*item_price AS expanded_price
FROM OrderItems
WHERE order_num = 20008;
🌂 虽然SELECT通常用于从表中检索数据,但是省略了FROM子句后就是简单地访问和处理表达式,例如SELECT 3*2;将返回6,SELECT Trim(’ abc ');将返回abc,SELECT Curdate();使用Curdate()函数返回当前日期和时间。
- 函数每一个DBMS都有特定的函数,只有少数几个函数被所有主要的DBMS等同地支持。虽然所有类型的函数一般都可以在每个DBMS中使用,但各个函数的名称和语法可能极其不同。
使用函数:
SELECT vend_name, UPPER(vend_name) AS vend_name_upcase
FROM Vendors
ORDER BY vend_name;
UPPER()将文本转换为大写。
常用的文本处理函数:
SOUNDEX是一个将任何文本串转换为描述其语音表示的字母数字模式的算法。SOUNDEX考虑了类似的发音字符和音节,使得能对字符串进行发音比较而不是字母比较。
下面的语句匹配所有发音类似于Michael Green的联系名:
SELECT cust_name, cust_contact
FROM Customers
WHERE SOUNDEX(cust_contact) = SOUNDEX('Michael Green');
返回:
cust_name cust_contact
Kids Place Michelle Green
- 应用程序一般不使用日期和时间的存储格式,因此日期和时间函数总是用来读取、统计和处理这些值。由于这个原因,日期和时间函数在SQL中具有重要的作用。遗憾的是,它们很不一致,可移植性最差。
SELECT order_num
FROM Orders
WHERE DATEPART(yy, order_date) = 2020;
🌂 SQL Server中不起作用,因为它不支持to_date()函数。
-
常用数值处理函数
-
聚集函数
🌂 确定表中行数(或者满足某个条件或包含某个特定值的行数);
🌂 获得表中某些行的和;
🌂 找出表列(或所有行或某些特定的行)的最大值、最小值、平均值。
SQL聚集函数
-
AVG的使用:
SELECT AVG(prod_price) AS avg_price FROM Products;
输出:
avg_price
6.823333
🌂 AVG()也可以用来确定特定列或行的平均值:
SELECT AVG(prod_price) AS avg_price
FROM Products
WHERE vend_id = 'DLL01';
获得多个列的平均值,必须使用多个AVG()函数。
- COUNT()函数有两种使用方式:
❑ 使用COUNT(*)对表中行的数目进行计数,包括所有行:
SELECT COUNT(*) AS num_cust FROM Customers;
❑ 使用COUNT(column)对特定列中有值的行进行计数,忽略NULL值:
SELECT COUNT(cust_email) AS num_cust FROM Customers;
这条SELECT语句使用COUNT(cust_email)对cust_email列中有值的行进行计数。
- SUM的使用:
SELECT SUM(quantity) AS items_ordered
FROM OrderItems
WHERE order_num = 20005;
也可以用来合计计算值:
SELECT SUM(item_price*quantity) AS total_price
FROM OrderItems
WHERE order_num = 20005;
SUM()函数忽略列值为NULL的行。
- 聚集不同值
❑ 对所有行执行计算,指定ALL参数或不指定参数(因为ALL是默认行为)。
❑ 只包含不同的值,指定DISTINCT参数。
使用DISTINCT参数,平均值只考虑各个不同的价格:
SELECT AVG(DISTINCT prod_price) AS avg_price
FROM Products
WHERE vend_id = 'DLL01';
注意:如果指定列名,则DISTINCT能用于COUNT()。
DISTINCT不能用于COUNT(*)。
类似地,DISTINCT必须使用列名,不能用于计算或表达式。
- 组合聚集函数。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;
输出如下:
num_items price_min price_max price_avg
9 3.4900 11.9900 6.823333
- 数据分组:使用分组可以将数据分为多个逻辑组,对每个组进行聚集计算。
- 创建分组
分组是使用SELECT语句的GROUP BY子句建立的:
SELECT vend_id, COUNT(*) AS num_prods
FROM Products
GROUP BY vend_id;
❑ GROUP BY子句可以包含任意数目的列,因而可以对分组进行嵌套,更细致地进行数据分组。
❑ 如果在GROUP BY子句中嵌套了分组,数据将在最后指定的分组上进行汇总。换句话说,在建立分组时,指定的所有列都一起计算(所以不能从个别的列取回数据)。
❑ GROUP BY子句中列出的每一列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在SELECT中使用表达式,则必须在GROUP BY子句中指定相同的表达式。不能使用别名。
❑ 大多数SQL实现不允许GROUP BY列带有长度可变的数据类型(如文本或备注型字段)。
❑ 除聚集计算语句外,SELECT语句中的每一列都必须在GROUP BY子句中给出。
❑ 如果分组列中包含具有NULL值的行,则NULL将作为一个分组返回。如果列中有多行NULL值,它们将分为一组。❑ GROUP BY子句必须出现在WHERE子句之后,ORDER BY子句之前。
-
过滤分组
除了能用GROUP BY分组数据外,SQL还允许过滤分组,规定包括哪些分组,排除哪些分组。例如,你可能想要列出至少有两个订单的所有顾客。为此,必须基于完整的分组而不是个别的行进行过滤。
HAVING非常类似于WHERE。事实上,目前为止所学过的所有类型的WHERE子句都可以用HAVING来替代。唯一的差别是,WHERE过滤行,而HAVING过滤分组:
-
下面的例子列出具有两个以上产品且其价格大于等于4的供应商:
SELECT vend_id, COUNT(*) AS num_prods
FROM Products WHERE prod_price >= 4
GROUP BY vend_id
HAVING COUNT(*) >= 2;
使用HAVING时应该结合GROUP BY子句,而WHERE子句用于标准的行级过滤。
- 分组和排序
ORDER BY与GROUP BY
一般在使用GROUP BY子句时,应该也给出ORDER BY子句。这是保证数据正确排序的唯一方法。千万不要仅依赖GROUP BY排序数据:
SELECT order_num, COUNT(*) AS items
FROM OrderItems
GROUP BY order_num
HAVING COUNT(*) >= 3;
按订购物品的数目排序输出,需要添加ORDER BY子句:
SELECT order_num, COUNT(*) AS items
FROM OrderItems
GROUP BY order_num
HAVING COUNT(*) >= 3
ORDER BY items, order_num;
输出:
order_num items
20006 3
20009 3
20007 5
20008 5
-
SELECT子句及其顺序
-
子查询
SQL还允许创建子查询(subquery),即嵌套在其他查询中的查询。
利用子查询进行过滤:
SELECT cust_id
FROM Orders
WHERE order_num
IN (SELECT order_num FROM OrderItems WHERE prod_id = 'RGAN01');
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 = 'RGAN01'));
对于能嵌套的子查询的数目没有限制,不过在实际使用时由于性能的限制,不能嵌套太多的子查询。
注意:只能是单列作为子查询的SELECT语句只能查询单个列。企图检索多个列将返回错误。
- 作为计算字段使用子查询
对顾客1000000001的订单进行计数:
SELECT COUNT(*) AS orders
FROM Orders
WHERE cust_id = 1000000001;
要对每个顾客执行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。
下面的WHERE子句告诉SQL,比较Orders表中的cust_id和当前正从Customers表中检索的cust_id: WHERE Orders.cust_id = Customers.cust_id
如果在SELECT语句中操作多个表,就应使用完全限定列名来避免歧义。
- SQL最强大的功能之一就是能在数据查询的执行中联结(join)表
- 相同的数据出现多次决不是一件好事,这是关系数据库设计的基础。关系表的设计就是要把信息分解成多个表,一类数据一个表。各表通过某些共同的值互相关联(所以才叫关系数据库)。
- 联结是一种机制,用来在一条SELECT语句中关联表,因此称为联结。
创建联结:
SELECT vend_name, prod_name, prod_price
FROM Vendors, Products
WHERE Vendors.vend_id = Products.vend_id;
指定的两列(prod_name和prod_price)在一个表中,而第一列(vend_name)在另一个表中
数据库表的定义中没有指示DBMS如何对表进行联结的内容。
返回笛卡儿积的联结,也称叉联结(cross join)
等值联结(equijoin),基于两个表之间的相等测试,也称为内联结(inner join)。
ANSI SQL规范首选INNER JOIN语法:
SELECT vend_name, prod_name, prod_price
FROM Vendors
INNER JOIN Products ON Vendors.vend_id = Products.vend_id;
SQL不限制一条SELECT语句中可以联结的表的数目,但许多DBMS都有限制。创建联结的基本规则也相同。不要联结不必要的表。联结的表越多,性能下降越厉害。
- 执行给定的SQL操作一般不止一种方法。很少有绝对正确或绝对错误的方法。性能可能会受操作类型、所使用的DBMS、表中数据量、是否存在索引或键等条件的影响。因此,有必要试验不同的选择机制,找出最适合具体情况的方法。
- SQL除了可以对列名和计算字段使用别名,还允许给表名起别名。这样做有两个主要理由:
❑ 缩短SQL语句;
❑ 允许在一条SELECT语句中多次使用相同的表。
需要注意,表别名只在查询执行中使用。与列别名不一样,表别名不返回到客户端。 - 使用不同类型的联结(Oracle用户应该记住去掉AS)。
🌂 自联结通常作为外部语句,用来替代从相同表中检索数据的使用子查询语句。虽然最终的结果是相同的,但许多DBMS处理联结远比处理子查询快得多。应该试一下两种方法,以确定哪一种的性能更好。
🌂 无论何时对表进行联结,应该至少有一列不止出现在一个表中(被联结的列)。标准的联结返回所有数据,相同的列甚至多次出现。自然联结排除多次出现,使每一列只返回一次。
SELECT C.*, O.order_num, 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 = 'RGAN01';
- 联结包含了那些在相关表中没有关联行的行。这种联结称为外联结。对比:
内联结:
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
1000000001 20005
1000000001 20009
1000000002 NULL
1000000003 20006
1000000004 20007
1000000005 20008
在使用OUTER JOIN语法时,必须使用RIGHT或LEFT关键字指定包括其所有行的表(RIGHT指出的是OUTER JOIN右边的表,而LEFT指出的是OUTER JOIN左边的表)
上面的例子使用LEFT OUTER JOIN从FROM子句左边的表(Customers表)中选择所有行。
- 全外联结(full outer join),检索两个表中的所有行并关联那些可以关联的行。与左外联结或右外联结包含一个表的不关联的行不同,全外联结包含两个表的不关联的行。
SELECT Customers.cust_id, Orders.order_num
FROM Customers
FULL OUTER JOIN Orders ON Customers.cust_id = Orders.cust_id;
- 使用带聚集函数的联结
检索所有顾客及每个顾客所下的订单数:
SELECT 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;
聚集函数也可以方便地与其他联结一起使用。请看下面的例子:
SELECT 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;
-
组合查询
SQL也允许执行多个查询(多条SELECT语句),并将结果作为一个查询结果集返回。这些组合查询通常称为并(union)或复合查询(compound query)。
有两种情况需要使用组合查询:
❑ 在一个查询中从不同的表返回结构数据;
❑ 对一个表执行多个查询,按一个查询返回数据。 -
创建组合查询:给出每条SELECT语句,在各条语句之间放上关键字UNION:
SELECT cust_name, cust_contact, cust_email
FROM Customers WHERE cust_state IN ('IL','IN','MI')
UNION
SELECT cust_name, cust_contact, cust_email
FROM Customers WHERE cust_name = 'Fun4All';
使用多条WHERE子句而不是UNION的相同查询:
SELECT cust_name, cust_contact, cust_email
FROM Customers WHERE cust_state IN ('IL','IN','MI')
UNION
SELECT cust_name, cust_contact, cust_email
FROM Customers WHERE cust_name = 'Fun4All';
-
UNION规则:
❑ 语句之间用关键字UNION分隔(因此,如果组合四条SELECT语句,将要使用三个UNION关键字)。
❑ UNION中的每个查询必须包含相同的列、表达式或聚集函数(不过,各个列不需要以相同的次序列出)。
❑ 列数据类型必须兼容:类型不必完全相同,但必须是DBMS可以隐含转换的类型(例如,不同的数值类型或不同的日期类型)。
如果一条语句是SELECT prod_name,而另一条语句是SELECT productname,那么它会返回第一个名字,举的这个例子就会返回prod_name,而不管第二个不同的名字。这也意味着你可以对第一个名字使用别名,因而返回一个你想要的名字。
这种行为带来一个有意思的副作用。由于只使用第一个名字,那么想要排序也只能用这个名字。拿我们的例子来说,可以用ORDER BY prod_name对结果排序,如果写成ORDER BY productname就会出错,因为查询结果里没有叫作productname的列。
UNION从查询结果集中自动去除了重复的行;换句话说,它的行为与一条SELECT语句中使用多个WHERE子句条件一样。使用UNION时,重复的行会被自动取消。 -
如果想返回所有的匹配行,不取消重复的行,可使用UNION ALL而不是UNION:
SELECT cust_name, cust_contact, cust_email
FROM Customers WHERE cust_state IN ('IL','IN','MI')
UNION ALL
SELECT cust_name, cust_contact, cust_email
FROM Customers WHERE cust_name = 'Fun4All';
下面的例子对前面UNION返回的结果进行排序:
SELECT cust_name, cust_contact, cust_email
FROM Customers WHERE cust_state IN ('IL','IN','MI')
UNION
SELECT cust_name, cust_contact, cust_email
FROM Customers WHERE cust_name = 'Fun4All'
ORDER BY cust_name, cust_contact;
虽然ORDER BY子句似乎只是最后一条SELECT语句的组成部分,但实际上DBMS将用它来排序所有SELECT语句返回的所有结果。
- 插入有几种方式:
❑ 插入完整的行;
❑ 插入行的一部分;
❑ 插入某些查询的结果。 - 使用INSERT语句可能需要客户端/服务器DBMS中的特定安全权限。在你试图使用INSERT前,应该保证自己有足够的安全权限。
INSERT INTO Customers
VALUES(1000000006, 'Toy Land', '123 Any Street',
'NewYork', 'NY', '11111', 'USA', NULL, NULL);
这个例子将一个新顾客插入到Customers表中。存储到表中每一列的数据在VALUES子句中给出,必须给每一列提供一个值。如果某列没有值,如上面的cust_contact和cust_email列,则应该使用NULL值(假定表允许对该列指定空值)。各列必须以它们在表定义中出现的次序填充。
- 编写依赖于特定列次序的SQL语句是很不安全的,这样做迟早会出问题。
更安全(不过更烦琐)的方法如下:
INSERT INTO Customers(cust_id,
cust_name,
cust_address,
cust_city,
cust_state,
cust_zip,
cust_country,
cust_contact,
cust_email)
VALUES(1000000006,
'Toy Land',
'123 Any Street',
'New York',
'NY',
'11111',
'USA',
NULL,
NULL);
这样即使表的结构改变,这条INSERT语句仍然能正确工作。使用这种语法,还可以省略列,这表示可以只给某些列提供值,给其他列不提供值。
-
如果表的定义允许,则可以在INSERT操作中省略某些列。省略的列必须满足以下某个条件。
❑ 该列定义为允许NULL值(无值或空值)。
❑ 在表定义中给出默认值。这表示如果不给出值,将使用默认值。 -
想把另一表中的顾客列合并到Customers表中,不需要每次读取一行再将它用INSERT插入,可以如下进行:
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通常只插入一行。要插入多行,必须执行多个INSERT语句。INSERT SELECT是个例外,它可以用一条INSERT插入多行,不管SELECT语句返回多少行,都将被INSERT插入。
- 要将一个表的内容复制到一个全新的表(运行中创建的表),可以使用CREATE SELECT语句(或者在SQL Server里也可用SELECT INTO语句)。
与INSERT SELECT将数据添加到一个已经存在的表不同,CREATE SELECT将数据复制到一个新表(有的DBMS可以覆盖已经存在的表,这依赖于所使用的具体DBMS):
CREATE TABLE CustCopy AS SELECT * FROM Customers;
使用SQL Server,可以这么写:
SELECT * INTO CustCopy FROM Customers;
❑ 任何SELECT选项和子句都可以使用,包括WHERE和GROUP BY;
❑ 可利用联结从多个表插入数据;
❑ 不管从多少个表中检索数据,数据都只能插入到一个表中。
- 更新数据
有两种使用UPDATE的方式:
❑ 更新表中的特定行;
❑ 更新表中的所有行。
基本的UPDATE语句由三部分组成
❑ 要更新的表;
❑ 列名和它们的新值;
❑ 确定要更新哪些行的过滤条件。
UPDATE Customers
SET cust_email = 'kim@thetoystore.com'
WHERE cust_id = 1000000005;
UPDATE Customers
SET cust_contact = 'Sam Roberts',
cust_email = 'sam@toyland.com'
WHERE cust_id = 1000000006;
要删除某个列的值,可设置它为NULL(假如表定义允许NULL值)
UPDATE Customers
SET cust_email = NULL
WHERE cust_id = 1000000005;
- 删除数据
有两种使用DELETE的方式:
❑ 从表中删除特定的行;
❑ 从表中删除所有行。
DELETE不删除表本身。
如果想从表中删除所有行,不要使用DELETE。可使用TRUNCATE TABLE语句,它完成相同的工作,而速度更快(因为不记录数据的变动)。 - 更新和删除的指导原则:如果执行DELETE语句而不带WHERE子句,表的所有数据都将被删除。
- 创建表
一般有两种创建表的方法:
❑ 多数DBMS都具有交互式创建和管理数据库表的工具;
❑ 表也可以直接用SQL语句操纵。
利用CREATE TABLE创建表,必须给出下列信息:
❑ 新表的名字,在关键字CREATE TABLE之后给出;
❑ 表列的名字和定义,用逗号分隔;
❑ 有的DBMS还要求指定表的位置。
CREATE TABLE Products (prod_id CHAR(10) NOT NULL,
vend_id CHAR(10) NOT NULL,
prod_name CHAR(254) NOT NULL,
prod_price DECIMAL(8,2) NOT NULL,
prod_desc VARCHAR(1000) NULL );
这条语句在绝大多数DBMS中有效,但对于DB2,必须从最后一列中去掉NULL。
在创建新的表时,指定的表名必须不存在,否则会出错。防止意外覆盖已有的表,SQL要求首先手工删除该表,然后再重建它,而不是简单地用创建表语句覆盖它。
NULL为默认设置,如果不指定NOT NULL,就认为指定的是NULL。
某些DBMS要求指定关键字NULL,如果不指定将出错。
只有不允许NULL值的列可作为主键,允许NULL值的列不能作为唯一标识。
- 默认值在CREATE TABLE语句的列定义中用关键字DEFAULT指定。
CREATE TABLE OrderItems
(order_num INTEGER NOT NULL,
order_item INTEGER NOT NULL,
prod_id CHAR(10) NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1,
item_price DECIMAL(8,2) NOT NULL);
quantity列为订单中每个物品的数量。在这个例子中,这一列的描述增加了DEFAULT 1,指示DBMS,如果不给出数量则使用数量1。默认值经常用于日期或时间戳列。例如,通过指定引用系统日期的函数或变量,将系统日期用作默认日期。
获得系统日期
许多数据库开发人员喜欢使用DEFAULT值而不是NULL列,对于用于计算或数据分组的列更是如此。
- 更新表定义,可以使用ALTER TABLE语句。
使用ALTER TABLE时需要考虑的事情:
❑ 理想情况下,不要在表中包含数据时对其进行更新。应该在表的设计过程中充分考虑未来可能的需求,避免今后对表的结构做大改动。
❑ 所有的DBMS都允许给现有的表增加列,不过对所增加列的数据类型(以及NULL和DEFAULT的使用)有所限制。
❑ 许多DBMS不允许删除或更改表中的列。
❑ 多数DBMS允许重新命名表中的列。
❑ 许多DBMS限制对已经填有数据的列进行更改,对未填有数据的列几乎没有限制。可以看出,对已有表做更改既复杂又不统一。对表的结构能进行何种更改,请参阅具体的DBMS文档。使用ALTER TABLE更改表结构,必须给出下面的信息:
❑ 在ALTER TABLE之后给出要更改的表名(该表必须存在,否则将出错);
❑ 列出要做哪些更改。 - 语句:
ALTER TABLE Vendors ADD vend_phone CHAR(20);
下面的例子并非对所有DBMS都有效:
ALTER TABLE Vendors DROP COLUMN vend_phone;
复杂的表结构更改一般需要手动删除过程,它涉及以下步骤:
(1) 用新的列布局创建一个新表;
(2) 使用INSERT SELECT语句从旧表复制数据到新表。有必要的话,可以使用转换函数和计算字段;
(3) 检验包含所需数据的新表;
(4) 重命名旧表(如果确定,可以删除它);
(5) 用旧表原来的名字重命名新表;
(6) 根据需要,重新创建触发器、存储过程、索引和外键。
使用ALTER TABLE要极为小心,应该在进行改动前做完整的备份(表结构和数据的备份)。数据库表的更改不能撤销,如果增加了不需要的列,也许无法删除它们。类似地,如果删除了不应该删除的列,可能会丢失该列中的所有数据。
- 删除表
删除表是指删除整个表而不是其内容:
DROP TABLE CustCopy;
注意:删除表没有确认步骤,也不能撤销,执行这条语句将永久删除该表。
许多DBMS允许强制实施有关规则,防止删除与其他表相关联的表。在实施这些规则时,如果对某个表发布一条DROP TABLE语句,且该表是某个关系的组成部分,则DBMS将阻止这条语句执行,直到该关系被删除为止。如果允许,应该启用这些选项,它能防止意外删除有用的表。 - 重命名表
DB2、MariaDB、MySQL、Oracle和PostgreSQL用户使用RENAME语句,SQL Server用户使用sp_rename存储过程,SQLite用户使用ALTER TABLE语句。 - 视图:
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 = 'RGAN01';
假如可以把整个查询包装成一个名为ProductCustomers的虚拟表,则可以如下轻松地检索出相同的数据:
SELECT cust_name, cust_contact
FROM ProductCustomers
WHERE prod_id = 'RGAN01';
ProductCustomers是一个视图,作为视图,它不包含任何列或数据,包含的是一个查询(与上面用以正确联结表的查询相同)。
视图的一些常见应用。
❑ 重用SQL语句。
❑ 简化复杂的SQL操作。
❑ 使用表的一部分而不是整个表。
❑ 保护数据。可以授予用户访问表的特定部分的权限,而不是整个表的访问权限。
❑ 更改数据格式和表示。
❑ 与表一样,视图必须唯一命名(不能给视图取与别的视图或表相同的名字)。
❑ 对于可以创建的视图数目没有限制。
❑ 创建视图,必须具有足够的访问权限。这些权限通常由数据库管理人员授予。
❑ 视图可以嵌套,即可以利用从其他视图中检索数据的查询来构造视图。所允许的嵌套层数在不同的DBMS中有所不同(嵌套视图可能会严重降低查询的性能,因此在产品环境中使用之前,应该对其进行全面测试)。❑ 许多DBMS禁止在视图查询中使用ORDER BY子句。
❑ 有些DBMS要求对返回的所有列进行命名,如果列是计算字段,则需要使用别名(关于列别名的更多信息,请参阅第7课)。
❑ 视图不能索引,也不能有关联的触发器或默认值。❑ 有些DBMS把视图作为只读的查询,这表示可以从视图检索数据,但不能将数据写回底层表。详情请参阅具体的DBMS文档。
❑ 有些DBMS允许创建这样的视图,它不能进行导致行不再属于视图的插入或更新。例如有一个视图,只检索带有电子邮件地址的顾客。如果更新某个顾客,删除他的电子邮件地址,将使该顾客不再属于视图。这是默认行为,而且是允许的,但有的DBMS可能会防止这种情况发生。
- 操作视图
与CREATE TABLE一样,CREATE VIEW只能用于创建不存在的视图。
删除视图,可以使用DROP语句,其语法:
DROP VIEW viewname;。
覆盖(或更新)视图,必须先删除它,然后再重新创建。
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;
SELECT cust_name, cust_contact
FROM ProductCustomers
WHERE prod_id = 'RGAN01';
视图极大地简化了复杂SQL语句的使用。利用视图,可一次性编写基础的SQL,然后根据需要多次使用。
视图的另一常见用途是重新格式化检索出的数据。
SELECT RTRIM(vend_name) + ' (' + RTRIM(vend_country) + ')' AS vend_title
FROM Vendors
ORDER BY vend_name;
下面是相同的语句,但使用了||语法 :
SELECT RTRIM(vend_name) || ' (' || RTRIM(vend_country) || ')' AS vend_title
FROM Vendors
ORDER BY vend_name;
假设经常需要这个格式的结果,可转换为视图,实现复用:
CREATE VIEW VendorLocations AS SELECT RTRIM(vend_name) + ' (' + RTRIM(vend_country) + ')' AS vend_title
FROM Vendors;
使用||语法的相同语句:
CREATE VIEW VendorLocations AS SELECT RTRIM(vend_name) || ' (' || RTRIM(vend_country) || ')' AS vend_title
FROM Vendors;
- 用视图过滤不想要的数据:
CREATE VIEW CustomerEMailList AS
SELECT cust_id, cust_name, cust_email
FROM Customers
WHERE cust_email IS NOT NULL;
现在,可以像使用其他表一样使用视图CustomerEMailList。
SELECT * FROM CustomerEMailList;
- 在简化计算字段的使用上
SELECT prod_id, quantity, item_price, quantity*item_price AS expanded_price
FROM OrderItems
WHERE order_num = 20008;
转换为一个视图
CREATE VIEW OrderItemsExpanded AS
SELECT order_num, prod_id, quantity, item_price, quantity*item_price AS expanded_price
FROM OrderItems
SELECT *
FROM OrderItemsExpanded
WHERE order_num = 20008;
- 存储过程
存储过程就是为以后使用而保存的一条或多条SQL语句。可将其视为批文件,虽然它们的作用不仅限于批处理。(SQLite不支持存储过程)
使用存储过程有三个主要的好处,即简单、安全、高性能
执行存储过程的SQL语句很简单,即EXECUTE。EXECUTE接受存储过程名和需要传递给它的任何参数。
EXECUTE AddNewProduct('JTS01', 'Stuffed Eiffel Tower', 6.49, 'Plush stuffed toy with the text La Tour Eiffel in red white and blue');
- 存储过程所完成的工作
❑ 验证传递的数据,保证所有4个参数都有值;
❑ 生成用作主键的唯一ID;
❑ 将新产品插入Products表,在合适的列中存储生成的主键和传递的数据。
❑ 参数可选,具有不提供参数时的默认值。
❑ 不按次序给出参数,以“参数=值”的方式给出参数值。
❑ 输出参数,允许存储过程在正执行的应用程序中更新所用的参数。
❑ 用SELECT语句检索数据。
❑ 返回代码,允许存储过程返回一个值到正在执行的应用程序。 - 创建存储过程
例子:对邮件发送清单中具有邮件地址的顾客进行计数。
Oracle版本:
CREATE PROCEDURE MailingListCount (
ListCount OUT INTEGER
) IS
v_rows INTEGER;
BEGIN
SELECT COUNT(*) INTO v_rows
FROM Customers
WHERE NOT cust_email IS NULL;
ListCount := v_rows;
END;
这个存储过程有一个名为ListCount的参数。此参数从存储过程返回一个值而不是传递一个值给存储过程。关键字OUT用来指示这种行为。Oracle支持IN(传递值给存储过程)、OUT(从存储过程返回值,如这里)、INOUT(既传递值给存储过程也从存储过程传回值)类型的参数。存储过程的代码括在BEGIN和END语句中,这里执行一条简单的SELECT语句,它检索具有邮件地址的顾客。然后用检索出的行数设置ListCount(要传递的输出参数)。
var ReturnValue NUMBER
EXEC MailingListCount(:ReturnValue);
SELECT ReturnValue;
SQL Server版本:
CREATE PROCEDURE MailingListCount
AS
DECLARE @cnt INTEGER
SELECT @cnt = COUNT(*)
FROM Customers
WHERE NOT cust_email IS NULL;
RETURN @cnt;
此存储过程没有参数。调用程序检索SQL Server的返回代码提供的值。其中用DECLARE语句声明了一个名为@cnt的局部变量(SQL Server中所有局部变量名都以@起头);然后在SELECT语句中使用这个变量,让它包含COUNT()函数返回的值;最后,用RETURN @cnt语句将计数返回给调用程序。
调用SQL Server:
DECLARE @ReturnValue INT
EXECUTE ReturnValue=MailingListCount;
SELECT @ReturnValue;
另一个SQL Server例子:
CREATE PROCEDURE NewOrder @cust_id CHAR(10) AS
-- 为订单号声明一个变量
DECLARE @order_num INTEGER
-- 获取当前最大订单号
SELECT @order_num=MAX(order_num) FROM Orders
-- 决定下一个订单号
SELECT @order_num=@order_num+1
-- 插入新订单
INSERT INTO Orders(order_num, order_date, cust_id)
VALUES(@order_num, GETDATE(), @cust_id)
-- 返回订单号
RETURN @order_num;
CREATE PROCEDURE NewOrder @cust_id CHAR(10) AS
-- 插入新订单
INSERT INTO Orders(cust_id) VALUES(@cust_id)
-- 返回订单号
SELECT order_num = @@IDENTITY;
- 事务处理,事务是必须完整执行的SQL语句块
使用事务处理(transaction processing),通过确保成批的SQL操作要么完全执行,要么完全不执行,来维护数据库的完整性。
事务处理是一种机制,用来管理必须成批执行的SQL操作,保证数据库不包含不完整的操作结果。
关于事务处理需要知道的几个术语:
❑ 事务(transaction)指一组SQL语句;
❑ 回退(rollback)指撤销指定SQL语句的过程;
❑ 提交(commit)指将未存储的SQL语句结果写入数据库表;
❑ 保留点(savepoint)指事务处理中设置的临时占位符
事务处理用来管理INSERT、UPDATE和DELETE语句。
不能回退SELECT语句(回退SELECT语句也没有必要),也不能回退CREATE或DROP操作。事务处理中可以使用这些语句,但进行回退时,这些操作也不撤销。 - 控制事务处理
管理事务的关键在于将SQL语句组分解为逻辑块,并明确规定数据何时应该回退,何时不应该回退。
SQL Server:
BEGIN TRANSACTION
...
COMMIT TRANSACTION
在这个例子中,BEGIN TRANSACTION和COMMIT TRANSACTION语句之间的SQL必须完全执行或者完全不执行。
DELETE FROM Orders;
ROLLBACK;
一般的SQL语句都是针对数据库表直接执行和编写的。这就是所谓的隐式提交(implicit commit),即提交(写或保存)操作是自动进行的。
BEGIN TRANSACTION
DELETE OrderItems WHERE order_num = 12345
DELETE Orders WHERE order_num = 12345
COMMIT TRANSACTION
- 要支持回退部分事务,必须在事务处理块中的合适位置放置占位符。这样,如果需要回退,可以回退到某个占位符。
在SQL Server中:
SAVE TRANSACTION delete1;
ROLLBACK TRANSACTION delete1;
BEGIN TRANSACTION
INSERT INTO Customers(cust_id, cust_name)
VALUES(1000000010, 'Toys Emporium');
SAVE TRANSACTION StartOrder;
INSERT INTO Orders(order_num, order_date, cust_id)
VALUES(20100,'2001/12/1',1000000010);
IF @@ERROR <> 0 ROLLBACK TRANSACTION StartOrder;
INSERT INTO OrderItems(order_num, order_item, prod_id, quantity, item_price)
VALUES(20100, 1, 'BR01', 100, 5.49);
IF @@ERROR <> 0 ROLLBACK TRANSACTION StartOrder;
INSERT INTO OrderItems(order_num, order_item, prod_id, quantity, item_price)
VALUES(20100, 2, 'BR03', 100, 10.99);
IF @@ERROR <> 0 ROLLBACK TRANSACTION StartOrder;
COMMIT TRANSACTION
- 游标
有时,需要在检索出来的行中前进或后退一行或多行,这就是游标的用途所在。
游标(cursor)是一个存储在DBMS服务器上的数据库查询,它不是一条SELECT语句,而是被该语句检索出来的结果集。
❑ 能够标记游标为只读,使数据能读取,但不能更新和删除。
❑ 能控制可以执行的定向操作(向前、向后、第一、最后、绝对位置和相对位置等)。
❑ 能标记某些列为可编辑的,某些列为不可编辑的。
❑ 规定范围,使游标对创建它的特定请求(如存储过程)或对所有请求可访问。
❑ 指示DBMS对检索出的数据(而不是指出表中活动数据)进行复制,使数据在游标打开和访问期间不变化。
游标主要用于交互式应用,其中用户需要滚动屏幕上的数据,并对数据进行浏览或做出更改。 - 使用游标
❑ 在使用游标前,必须声明(定义)它。这个过程实际上没有检索数据,它只是定义要使用的SELECT语句和游标选项。
❑ 一旦声明,就必须打开游标以供使用。这个过程用前面定义的SELECT语句把数据实际检索出来。
❑ 对于填有数据的游标,根据需要取出(检索)各行。
❑ 在结束游标使用时,必须关闭游标,可能的话,释放游标(有赖于具体的DBMS)。
使用OPEN CURSOR语句打开游标:
OPEN CURSOR CustCursor;
SQL Server语法:
DECLARE @cust_id CHAR(10),
@cust_name CHAR(50),
@cust_address CHAR(50),
@cust_city CHAR(50),
@cust_state CHAR(5),
@cust_zip CHAR(10),
@cust_country CHAR(50),
@cust_contact CHAR(50),
@cust_email CHAR(255)
OPEN CustCursor
FETCH NEXT FROM CustCursor
INTO @cust_id,
@cust_name, @cust_address,
@cust_city, @cust_state, @cust_zip,
@cust_country, @cust_contact, @cust_email
...
WHILE @@FETCH_STATUS = 0
BEGIN
FETCH NEXT FROM CustCursor
INTO @cust_id,
@cust_name, @cust_address,
@cust_city, @cust_state, @cust_zip, @cust_country, @cust_contact, @cust_email
...
END
CLOSE CustCursor
游标在使用完毕时需要关闭:
CLOSE CustCursor DEALLOCATE CURSOR CustCursor
CLOSE语句用来关闭游标。一旦游标关闭,如果不再次打开,将不能使用。第二次使用它时不需要再声明,只需用OPEN打开它即可。
- 约束
约束(constraint)管理如何插入或处理数据库数据的规则。
表中任意列只要满足以下条件,都可以用于主键。
❑ 任意两行的主键值都不相同。
❑ 每行都具有一个主键值(即列中不允许NULL值)。
❑ 包含主键值的列从不修改或更新。(大多数DBMS不允许这么做,但如果你使用的DBMS允许这样做,好吧,千万别!)
❑ 主键值不能重用。如果从表中删除某一行,其主键值不分配给新行。
CREATE TABLE Vendors
(vend_id CHAR(10) NOT NULL PRIMARY KEY,
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);
ALTER TABLE Vendors
ADD CONSTRAINT PRIMARY KEY (vend_id);
-
外键是表中的一列,其值必须列在另一表的主键中。外键是保证引用完整性的极其重要部分。
唯一约束用来保证一列(或一组列)中的数据是唯一的。
它们类似于主键,但存在以下重要区别。
❑ 表可包含多个唯一约束,但每个表只允许一个主键。
❑ 唯一约束列可包含NULL值。
❑ 唯一约束列可修改或更新。
❑ 唯一约束列的值可重复使用。
❑ 与主键不一样,唯一约束不能用来定义外键。 -
唯一约束既可以用UNIQUE关键字在表定义中定义,也可以用单独的CONSTRAINT定义。
检查约束用来保证一列(或一组列)中的数据满足一组指定的条件。
带限制其值为M或F(对于未知值或许还允许NULL) -
索引
索引用来排序数据以加快搜索和排序操作的速度
CREATE INDEX prod_name_ind ON Products (prod_name);
- 触发器
触发器是特殊的存储过程,它在特定的数据库活动发生时自动执行。触发器可以与特定表上的INSERT、UPDATE和DELETE操作(或组合)相关联。
触发器内的代码具有以下数据的访问权:
❑ INSERT操作中的所有新数据;
❑ UPDATE操作中的所有新数据和旧数据;
❑ DELETE操作中删除的数据。
下面是触发器的一些常见用途。
❑ 保证数据一致。例如,在INSERT或UPDATE操作中将所有州名转换为大写。
❑ 基于某个表的变动在其他表上执行活动。例如,每当更新或删除一行时将审计跟踪记录写入某个日志表。
❑ 进行额外的验证并根据需要回退数据。
❑ 计算计算列的值或更新时间戳。
SQL Server版本:
CREATE TRIGGER customer_state ON Customers
FOR INSERT, UPDATE
AS
UPDATE Customers
SET cust_state = Upper(cust_state)
WHERE Customers.cust_id = inserted.cust_id;
一般来说,约束的处理比触发器快,因此在可能的时候,应该尽量使用约束。
- 数据库安全
任何安全系统的基础都是用户授权和身份确认。
安全性使用SQL的GRANT和REVOKE语句来管理