《SQL基础教程》

《SQL基础教程》

《SQL基础教程(第2版)》读书笔记

基础知识

管理数据库的计算机系统被称为数据库管理系统(DataBase Management System,DBMS),按数据的保存格式,其主要可分为五类:

  • 层次数据库(Hierarchical,HDB):把数据通过层次(树形)结构表现出来
  • 关系数据库(Relational,RDB):用行和列组成的二维表来管理数据
  • 面向对象数据库(Objec Oriented, OODB):把数据及其操作集合起来以对象为单位进行操作
  • XML数据库(XMLDB):处理XML数据
  • 键值存储系统(Key-Value Store, KVS):保存查询所用的主键和值的组合

目前应用最广也是数据库为RDB,这里要学习的就是专用用来对RDB数据进行操作的结构化查询语言(Structured Query Language,SQL)。RDBMS常采用客户端/服务器(C/S)结构,当前具有代表性的有:

  • Oracle Database
  • SQL Server (Microsoft)
  • DB2 (IBM)
  • PostgreSQL
  • MySQL

在RDB中,包含如下概念:

  • 表(table):用来管理数据的二维表
  • 字段:表的列,表示表中的数据项
  • 记录:表的行,表示一条数据

在RDBMS中,必须以行为单位进行数据读写,而用来实现这些操作的一条(ISO定义的)标准SQL语句通常由关键字、表名、列名等组合而成,根据对RDBMS赋予的指令种类,SQL语句大致可分为三类:

  • DDL(Data Definition Language,数据定义):对数据库或表对象进行增删改操作
  • DML(Date Manipulation Lanugage,数据操纵):对表中的数据进行增删改操作
  • DCL(Data Control Lanugage,数据控制):确定、取消数据变更、设定用户权限等操作

实际使用的SQL语句大都为DML,这里重点学习的也是DML。SQL语句不区分大小写,通常习惯将关键字及表名首字母大写,除数字可以直接书,字符串、时间等常数都需要包括在单引号(’)中,各条语句以分号(;)结束。

基本操作语句

表对象操作

对数据库或表对象进行增删改的常用SQL语句有:

1
2
3
4
5
6
CREATE DATABASE <database_name>; -- 创建数据库
CREATE TABLE <table_name>
( <line_name_1> <data_type> <constraint>,
<line_name_n> <data_type> <constraint>); -- 创建并定义表
DROP TABLE <table_name>; -- 删除表
ALTER TABLE <table_name> <ops>; -- 更新表的定义

其中可由半角英文字母、数字、下划线组成,创建表的同时需要定义表中各字段的数据类型及约束,标准SQL中几种常见的数据类型有:

  • INTEGER:整型
  • CHAR:定长字符串,以定长存储,长度可自行指定
  • VARCHAR:可变字符串,以边长存储,长度可自行指定
  • DATE:日期
  • NUMERIC:数值,可定义全体及小数位数

约束用以对各字段中存储的数据进行一些限制或追加条件,几种常用的约束有:

  • PRIMARY KEY:主键
  • NOT NULL:非空
  • DEFAULT:默认值

更新表的定义便是表中的字段进行增改,中常用的有:

1
2
3
ADD COLUMN <line_define>; -- 新增列
DROP COLUMN <line_name>; -- 删除列
RENAME TO <new_name>; -- 重命名

在PostgreSQL中,使用这些语句操作表对象的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE DATABASE shop; -- 创建数据库shop

CREATE TABLE Product
(product_id CHAR(4) NOT NULL,
product_name VARCHAR(100) NOT NULL,
product_type VARCHAR(32) NOT NULL,
sale_price INTEGER ,
purchase_price INTEGER ,
regist_date DATE ,
PRIMARY KEY (product_id)); -- 创建并定义表Product

ALTER TABLE Product ADD COLUMN product_name_pingyin VARCHAR(100); -- 增加字段
ALTER TABLE Product RENAME TO product_name_chinese; -- 重命名字段
ALTER TABLE Product DROP COLUMN product_name_chinese; -- 删除字段

DROP TABLE Product; -- 删除表Product

表的增删改

对表中数据进行增删改的常用SQL语句有:

1
2
3
INSERT INTO <table_name> (<line_name_list>) VALUES (<value_list>); -- 插入数据
DELETE FROM <table_name> WHERE <cond_expr>; -- 依条件删除数据
UPDATE <table_name> SET (<line_name_list>) = (<expr>) WHERE <cond_expr>; -- 依条件更新字段

插入数据时,清单(list)中的名字或值须以逗号(,)分隔,要插入的数据值清单必须和字段名清单一一对应,且可省略,此时将默认以从左到右的顺序赋到各个字段上,例如向Product中插入数据:

1
2
INSERT INTO Product (product_id, product_name, product_type, sale_price, purchase_price, regist_date) VALUES ('0001', 'T恤' ,'衣服', 1000, 500, '2009-09-20');
INSERT INTO Product VALUES ('0002', '打孔器', '办公用品', 500, 320, '2009-09-11'); -- 两者的效果是一样的

更新或删除数据时,可通过WHERE子句来指定条件,是由比较及逻辑运算符(AND、OR、NOT)组成的条件表达式,可以为常量算术表达式,没有WHERE子句时将对整张表进行相应的操作。要注意的是,在SQL中进行比较运算时用“<>”来表示不等于,判断表中为N记录项是否为空是不能用“=”,而需要用IS NULL、IS NOT NULL。

以上语句的用例如下所示:

1
2
3
DELETE FROM Product WHERE sale_price >= 4000 AND purchase_price >= 4000; -- 删除符合条件的数据
UPDATE Product SET regist_date = '2009-10-10'; -- 整张表的regist_date字段都更新为该值
UPDATE Product SET sale_price = sale_price * 10 WHERE product_type = '厨房用具'; -- 根据条件更新数据

查询语句

基本查询

SELECT是SQL最基本也是最重要的语句,它常用以查询表中的数据,几个基本的SELECT查询语句如下:

1
2
3
4
SELECT <line_name> FROM <table_name>; -- 简单查询
SELECT <line_name>/<constant> AS <alias> FROM <table_name>; -- 取别名
SELECT DISTINCT <line_name> FROM <table_name>; -- 去重
SELECT <line_name> FROM <table_name> WHERE <cond_expr>; -- 依条件查询

查询过程中,在SELECT子句中列出要查询的字段名,得到的查询结果将字段的罗列顺序一致,SQL语句中常用星号(*)来代表查询表中的所有字段。查询过程中还可以用AS关键字为一些字段取别名,查询结果中的字段名就会被别名所替换,以中文作为别名时需要用双引号(”)将文本括起来。另外,SELECT子句中还可以接常数,此时在得到的查询结果中,所有记录的该字段都将为该常数值。

在SELECT子句中使用DISTINCT关键字,可以根据一些字段来删除查询结果中的重复数据,也通过WHERE子句来指定条件,筛选出想要的查询结果。

这些基本查询语句的用例如下:

1
2
3
4
5
SELECT product_id, product_name, purchase_price FROM Product; -- 查询其中三列
SELECT product_id AS "商品编号", product_name AS "商品名称" FROM Product; -- 在查询结果中为一些字段设置别名
SELECT '商品' AS string, 38 AS number, product_id, product_name FROM Product; -- 查询结果中所有记录的string、number字段都是相同的常数
SELECT DISTINCT product_type, regist_date FROM Product; -- 对两个字段组合后的数据去重
SELECT product_name, product_type FROM Product WHERE product_type = '衣服’; -- 得到符合条件的查询结果

聚合查询

通过SQL对数据进行某种操作或计算时需要使用函数,其中对数据进行一些汇总操作的函数就被称为聚合函数,常用的聚合函数有

  • COUNT():计算表中的记录数
  • SUM():计算某字段的合计值
  • AVG():计算某字段的平均值
  • MAX/MIN():求出某字段的最大/最小值

以聚合函数为SELECT子句,结合GROUP BY关键字,可以实现对表中地数据进行分组汇总,其基本地语法如下:

1
SELECT <line_name> FROM <table_name> GROUP BY <line_name>; -- 分组聚合

例如,可使用如下语句来统计Product中各类产品的数量:

1
SELECT product_type, COUNT(*) FROM Product GROUP BY product_type; -- 分组聚合

其中GROUP BY子句中的字段名被称为聚合键,它决定了表的分组方式,SELECT子句中除了聚合函数外不可出现除该聚合键以外的字段名。

句中可引入WHERE语句,以实现依条件进行汇总:

1
SELECT <line_name> FROM <table_name>  WHERE <cond_expr> GROUP BY <line_name>; -- 根据条件进行分组聚合

GROUP BY子句一定要写在FROM及WHERE语句之后,且上述语句中各子句的执行顺序如下:

FROM -> WHERE -> GROUP BY -> SELECT

因此,GROUP BY中不可使用SELECT子句中由AS定义的别名作为聚合键。

用WHERE语句中只能对原始记录指定条件,要由分组聚合结果指定条件,即从分组结果中筛选数据,就需要用到HAVING子句,其语法如下:

1
SELECT <line_name> FROM <table_name> GROUP BY <line_name> HAVING <cond_expr>; -- 从分组结果中依条件选择

HAVING子句可由常数、聚合函数或聚合键构造。例如,将Product按商品种类分组后,可使用如下语句取出其中数据行为2的组:

1
SELECT product_type. COUNT(*) FROM Product GROUP BY product_type HAVING COUNT(*) = 2;

加入HAVING子句后,整个查询语句的执行顺序如下:

SELECT -> FROM -> WHERE -> GROUP BY -> HAVING

在查询表中数据时,默认情况下的查询结果都是无序的。通过ORDER BY关键字,可查询到的表记录按某个字段进行排序,具体的语法如下:

1
SELECT <line_name> FROM <table_name> ORDER BY <line_name> DESC/ASC; -- 按某字段进行升/降排序

其中通过最后的关键字为DESC或ASC来指定升降序。ORDER BY子句中的字段名被称为排序键,类似GROUP BY,排序键也可以有多个,且越先写的排序键优先级越高。

ORDER BY子句通常要放在SELECT语句的末尾,其中可以使用别名,因为ORDER BY子句总在最后执行,具体顺序如下:

FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY

要注意的是,聚合函数只能在SELECT、HAVING、ORDER BY子句中使用,WHERE子句中不可出现聚合函数。

事务与视图

所谓的事务(transaction),指的是需要在同一个处理单元中执行的一系列更新处理的集合。表的增删改操作常需要用多条SQL语句才能完成,因此可以多条这样的SQL语句封装成一个事务来统一执行。在RDBMS中,事务是对表中数据进行更新的单位。

封装多条用于操作表的数据的SQL语句的方法,及构建事务的语法如下:

1
2
3
4
5
事务开始语句;
DML语句1;
DML语句2;
...
事务结束语句;

例如下面在PostgreSQL中创建一个事务:

1
2
3
4
BEGIN TRANSACTION;
UPDATE Product SET sale_price = sale_price - 1000 WHERE product_name = '运动T恤';
UPDATE Product SET sale_price = sale_price + 1000 WHERE product_name = 'T恤衫';
COMMIT;

标准SQL中并没有定义事务的开始语句,因此此语句随DBMS的不同而不同,如PostgreSQL中规定的开始语句为“BEGIN TRANSACTION”,MySQL中的则为“START TRANSCTION”。事务的结束语句可为“COMMIT”或“ROLLBACK”,前者是按顺序执行事务中的DML语句并在最终提交,后者则是取消事务中所有对数据库的更新处理而恢复其原始状态。

DBMS中的事务都需要遵循四大特性:

  • 原子性(Atomicity):事务结束时,其中包含的更新处理要么全部执行,要么全部不执行
  • 一致性(Consistency):事务中包含的处理要满足数据库提前设置的约束
  • 隔离性(Isolation):不同事务间互不干扰
  • 持久性(Durability):事务结束后,DBMS能保证该时间点的数据被保存,即使发生故障也有恢复手段

取每个特性的首字符,这四大特性又称ACID特性。

事务是将多条对数据库进行增删改的语句封装到一起,如果将一些常用的查询语句封装起来,就有了视图(VIEW)。查询语句执行后的结果是一张表,要保存这张表的话需要一定存储空间,将查询语句封装为视图,需要这张表的时候只需要调用该视图即可,这可以极大地节省存储空间。

创建视图地语法如下:

1
2
3
CREATE VIEW <view_name> (<view_line_name_1>, <view_line_name_2>, ...)
AS
SELECT语句;

例如将前面例子中用过的一条查询语句封装为ProductSum视图:

1
2
3
CREATE VIEW ProductSum (product_type, cnt_product)
AS
SELECT product_type, COUNT(*) FROM Product GROUP BY product_type;

视图中将SELECT语句的查询结果中包含的字段为其字段,并为它们设置别名,且视图的使用方法也与普通的表无异:

1
SELECT product_type, cnt_product FROM ProductSum;

要注意的是,因为RDB中的记录本身都是无序的,所以不能在视图中使用ORDER BY语句进行排序。虽然对视图的操作与表无异,但无法随意使用更新语句对视图进行数据更新,因为视图的更新过程将对原表中数据产生直接影响。

另外,可以使用如下语句删除某个视图:

1
DROP VIEW <view_name>;

子查询

所谓子查询,是将SELECT语句进行多层嵌套,而直接以SELECT语句作为某些关键字的子句。如下面的例子:

1
2
3
SELECT Product_type, cnt_product FROM (
SELECT Product_type, COUNT(*) AS cnt_product FROM Product GROUP BY product_type;
) AS ProductSum;

其中内层的SELECT语句将首先被执行并得到查询结果,外层的查询将以这些结果为源数据进行二次查询。简单来说,子查询就是一次性视图,在SELECT语句执行结束后便消失,而不是像视图那样永久保存在磁盘上。在上例中,还使用了AS关键字为这个子查询命名。

子查询可以进行无限嵌套,例如下面的SELECT语句中便嵌套了两层的SELECT语句:

1
2
3
4
5
SELECT product_type, cnt_product FROM (
SELECT * FROM (
SELECT product_type, COUNT(*) AS cnt_product FROM Product GROUP BY product_type
) AS ProductSum WHERE cnt_product = 4
) AS ProductSum2;

返回结果为一行一列即单一值的子查询语句被称为标量子查询。不管是SELECT、WHERE子句还是GROUP BY、HAVING、ORDER BY子句,只要是能够用常数或列名的地方,都可以使用标量子查询语句。例如下面的例中WHERE子句的条件表达式:

1
SELECT product_id, product_name, sale_price FROM Product WHERE sale_price > (SELECT AVG(sale_price) FROM Product);

有时想要在GOROUP BY细分好的各组内进行一些比较操作,例如根据product_type将Product中的记录分组后,想要分别选出各组中价格高于该平均价格的商品,可能会想到如下语句:

1
SELECT prodcut_id, product_name, sale_price FROM Product WHERE sale_price > (SELECT AVG(sale_price) FROM Product GROUP BY product_type);

然而这是一个错误的SQL语句,其中的SELECT子查询语句不为标量,无法参与比较。要解决这类问题,需要用到关联子查询,如下所示:

1
2
3
SELECT product_type, product_name, sale_price FROM Product AS P1 WHERE sale_price > (
SELECT AVG(sale_price) FROM Product AS P2 WHERE P1.product_type = P2.product_type GROUP BY product_type
);

子查询语句中,会根据product_type返回相应的平均价格,从而筛选中表中满足要求的记录。

集合与联结

SQL语句除了能对RDB中的表进行增删改以及查询操作外,还能在多个表之间进行集合运算。其中用到的关键字如下:

1
2
3
SELECT ... FROM <table_name_1> UNION SELECT ... FROM <table_name_2>; -- 并集
SELECT ... FROM <table_name_1> INTERSECT SELECT ... FROM <table_name_2>; -- 交集
SELECT ... FROM <table_name_1> EXCEPT SELECT ... FROM <table_name_2>; -- 差集

其中UINON、INTERSERT、EXCEPT关键字分别代表了对两表进行并集、交集、差集运算。对于两个具有相同字段的表,并集是将这些记录进行合并,交集则选出两表中的公共记录,差集则将把公共记录从第一个表中去除,这些过程都不会保留重复的记录,可在这些关键字后面添加ALL来把重复数据予以保留。

集合运算是以行方向为单位进行操作,而联结是以列为单位对表进行的操作。在联结运算中,应用最为广泛的为内联结,其用到的关键字为INNER JOIN。它是以两表共有的字段为桥梁,从而将两表的字段合并到一起而得到一张表。

内联结语句的语法为:

1
SELECT <line_name> FROM <table_name_1> INNER JOIN <table_ame_2> ON <expr>; -- 由联结键内联

其中关键字ON后面的表达式便是联结两表的桥梁——联结键。例如有下面一张表ShopProduct:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CREATE TABLE ShopProduct
(shop_id CHAR(4) NOT NULL,
shop_name VARCHAR(200) NOT NULL,
product_id CHAR(4) NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (shop_id, product_id));

INSERT INTO ShopProduct (shop_id, shop_name, product_id, quantity) VALUES ('000A', '东京', '0001', 30);
INSERT INTO ShopProduct (shop_id, shop_name, product_id, quantity) VALUES ('000A', '东京', '0002', 50);
INSERT INTO ShopProduct (shop_id, shop_name, product_id, quantity) VALUES ('000A', '东京', '0003', 15);
INSERT INTO ShopProduct (shop_id, shop_name, product_id, quantity) VALUES ('000B', '名古屋', '0002', 30);
INSERT INTO ShopProduct (shop_id, shop_name, product_id, quantity) VALUES ('000B', '名古屋', '0003', 120);
INSERT INTO ShopProduct (shop_id, shop_name, product_id, quantity) VALUES ('000B', '名古屋', '0004', 20);
INSERT INTO ShopProduct (shop_id, shop_name, product_id, quantity) VALUES ('000B', '名古屋', '0006', 10);
INSERT INTO ShopProduct (shop_id, shop_name, product_id, quantity) VALUES ('000B', '名古屋', '0007', 40);
INSERT INTO ShopProduct (shop_id, shop_name, product_id, quantity) VALUES ('000C', '大阪', '0003', 20);
INSERT INTO ShopProduct (shop_id, shop_name, product_id, quantity) VALUES ('000C', '大阪', '0004', 50);
INSERT INTO ShopProduct (shop_id, shop_name, product_id, quantity) VALUES ('000C', '大阪', '0006', 90);
INSERT INTO ShopProduct (shop_id, shop_name, product_id, quantity) VALUES ('000C', '大阪', '0007', 70);
INSERT INTO ShopProduct (shop_id, shop_name, product_id, quantity) VALUES ('000D', '福冈', '0001', 100);

该表与Product有相同的字段product_id,于是可以以此为联结键,将两表的字段进行合并:

1
2
3
SELECT SP.shop_id, SP.shop_name, SP_product_id, P.product_name, P.sale_price
FROM ShopProduct AS SP INNER JOIN Product AS P
ON SP.product_id = P.product_id;

其中使用了别名以分别从两表中选出想要查询的字段,当ShopProduct中某个记录的product_id与Product中的某个记录一致,Product中该记录的几个字段就会被联结到ShopProduct上。使用这样的联结运算将满足规则的表联结起来时,前面用到的WHERE、GROUP BY HAVING、ORDER BY等语句都能正常使用。

既然有内连接,那必有与之相对的外联结。外联结以OUTER JOIN为关键字,过程中可使用关键字LEFT或RIGHT,将OUTER JOIN左边或右边的表作为主表,联结过程中不管与之联结的表中是否存在对应字段,主表中的记录都会被全部保留。如下面的例子所示:

1
2
3
4
5
6
7
SELECT SP.shop_id, SP.shop_name, SP_product_id, P.product_name, P.sale_price
FROM ShopProduct AS SP RIGHT OUTER JOIN Product AS P
ON SP.product_id = P.product_id;

SELECT SP.shop_id, SP.shop_name, SP_product_id, P.product_name, P.sale_price
FROM Product AS P LEFT OUTER JOIN ShopProduct AS SP
ON SP.product_id = P.product_id; -- 两条语句的结果一致

除了内外联之外,还存在一种称为交叉联结的表联结方式,在实际情况中的应用相对较少,具体的语法如下:

1
SELECT <line_name> FROM <table_name_1> CROSS JOIN <table_ame_2>; -- 交叉联结

该联结过程是满足相同规则的表进行笛卡尔积运算,因此不支持ON关键字,要得到与内外联一致的结果,就必须使用WHERE关键字从大量的结果中进行筛选。

函数、谓词、CASE

在聚合查询中是曾了解过几个简单的聚合函数,除此之外,SQL中还存在用以进行数值计算的算术函数、字符串操作的字符串函数、日期操作的日期函数及用于转换数据类型和值的转换函数。在这些函数中常用的如下:

  • ABS(*):求绝对值
  • MOD(m, n):求余(m/n)
  • ROUND(m, n):舍入m(n为保留位数)
  • ||/CONCAT():拼接字符串
  • LENGTH():求字符串长度
  • LOWER/UPPER():对字符串进行大/小写转换
  • REPLACE(str, m, n):将字符串str中的m替换为n
  • SUBSTRING(str FROM m FOR n):从str的第m位开始截取n个
  • CURRENT_DATE:当前日期
  • CURRENT_TIME:当前时间
  • CURRENT_TIMESTAMP:当前日期、时间
  • EXTRACT(m FROM n):从时间n中截取元素m
  • CAST(m AS n):m的类型转换为n
  • COALESCE(date1, date2, …):参数可变,返回参数中左侧第一个不是NULL的值,常可用在含有NULL的记录项时防止最后的计算结果变为NULL

SQL中还存在一个谓词的概念,所谓的谓词简单来说就是返回结果真值(True、False或Unknown)的函数,它们可以用于SQL语句中的WHERE、HAVING等子句中,参与各种逻辑计算。

LIKE谓词可用来进行模糊查询,从表中筛选出符合某种组合模式的数据。其中可用的通配符有:

  • %:0个以上任意字符
  • _:1个任意字符

例如,可用如下语句从表中选出符合模式“任意个任意字符+ab+1个任意字符”的字符串:

1
SELECT * FROM SampleLike WHERE strcol LIKE '%ab_';

BETWEEN谓词可用来代替通常的大于、小于号加逻辑运算符的组合,方便地实现范围查询,例如:

1
2
3
SELECT product_name, sale_price FROM Product WHERE sale_price BETWEEN 100 AND 1000;

SELECT product_name, sale_price FROM Product WHERE sale_price >= 100 AND sale_price <= 1000; -- 两者的结果一致

IS NULL和IS NOT NULL谓词则用来判断某个记录项是否为NULL。IN及NOT IN谓词可以用来代替OR,查询某个项目是否存在于某个集合中,如下所示:

1
2
3
SELECT product_name, purchase_price FROM Product WHERE purchase_price IN (320, 500, 5000); 

SELECT product_name, purchase_price FROM Product WHERE purchase_price = 320 OR purchase_price = 500 OR purchase_price = 5000; -- 两者的结果一致

另外还有一个功能强大的谓词EXIST,可用其代替IN/NOT IN,但该谓词的语法较为复杂及难以理解。由表ShopProduct和Product,使用该谓词来选出“东京店在售之外的商品的售价”的语句如下:

1
2
3
4
5
SELECT product_name, sale_price FROM Product AS P 
WHERE EXISTS (
SELECT * FROM ShopProduct AS SP
WHERE SP. shop_id = '000C' AND SP.product_id = P.product_id
);

使用SQL进行查询时,还可以使用CASE进行条件分支,其语法如下:

1
2
3
4
CASE WHEN <value_expr> THEN <expr>
WHEN <value_expr> THEN <expr>
ELSE <expr>
END

例如:

1
2
3
4
5
6
7
SELECT product_name,
CASE WHEN product_type = '衣服' THEN 'A:' || product_type
WHEN product_type = '办公用品' THEN 'B:' || product_type
WHEN product_type = '厨房用具' THEN 'C:' || product_type
ELSE NULL
END AS abc_product_type i
FROM Product;

更新历史:

  • 2019.11.12 完成初稿
作者

Hugsy

发布于

2019-11-04

更新于

2022-12-10

许可协议

评论