写在前面:本文主要注重 SQL 的理论、主流覆盖的功能范围及其基本语法/用法。至于详细的 SQL 语法/用法,由于每家 DBMS 都有些许不一样,我会在之后专门介绍某款DBMS(例如 PostgreSQL)的时候写到。html
数据库管理系统
(Database Management System, DBMS
) 是用来管理数据库的计算机系统。node
本文采用 PostgreSQL
为 DBMS
。mysql
问:为何不用 文本文件 或者 excel(+VBA)?c++
答:DBMS 的好处有:git
多人共享程序员
海量存储正则表达式
可编程sql
容灾机制数据库
……npm
1969年诞生,形式为行列二维表,相似 excel,包括:
SQL(Structured Query Language,结构化查询语言)
关系数据库管理系统
(Relational Database Management System,RDBMS
)
如无特殊说明,本文所提到的 DBMS 都是指 RDBMS。
C/S 结构。
参考个人文章 《PostgreSQL 安装 & 用户配置》
SQL
(Structured Query Language : 结构化查询语言)是一种特定目的编程语言,用于管理关系数据库管理系统(RDBMS)。
1986 年,ANSI 首次制定了 SQL 的标准,以后又进行了数次修订。
1987 年成为国际标准化组织(ISO)标准。称为 标准SQL。
原则上,本书介绍的都是 标准SQL 的书写方式.
1986年,ANSI X3.135-1986,ISO/IEC 9075:1986,SQL-86
1989年,ANSI X3.135-1989,ISO/IEC 9075:1989,SQL-89
1992年,ANSI X3.135-1992,ISO/IEC 9075:1992,SQL-92(SQL2)
1999年,ISO/IEC 9075:1999,SQL:1999(SQL3)
2003年,ISO/IEC 9075:2003,SQL:2003
2008年,ISO/IEC 9075:2008,SQL:2008
2011年,ISO/IEC 9075:2011,SQL:2011
2016年,ISO/IEC 9075:2016,SQL:2016
截止目前,最新的为 SQL:2016。
一、DDL
(Data Definition Language,数据定义语言
)
CREATE: 建立数据库和表等对象
DROP: 删除数据库和表等对象
ALTER: 修改数据库和表等对象的结构
二、DML
(Data Manipulation Language,数据操纵语言
)
SELECT:查询表中的数据
INSERT:向表中插入新数据
UPDATE:更新表中的数据
DELETE:删除表中的数据
三、DCL
(Data Control Language,数据控制语言
)
COMMIT: 确认对数据库中的数据进行的变动
ROLLBACK: 取消对数据库中的数据进行的变动
GRANT: 赋予用户操做权限
REVOKE: 取消用户的操做权限
总结:实际使用的 SQL 语句当中有 90% 属于 DML。
注意:不一样的数据库产品划分可能不尽相同。例如在 Oracle 中,把 TRUINCATE 定义为 DDL,而不是 DML。而事务只对 DML 有效,所以,Oracle 中的 TRUNCATE 不能使用 ROLLBACK。(执行 TRUNCATE 的同时会默认执行 COMMIT 操做。)
单行注释 —— --
以后
多行注释 —— /*
和*/
之间
一、关键字大写(虽然 SQL 不区分关键字的大小写。)
二、前置逗号
SELECT col_1 , col_2 , col_3 , col_4 FROM tbl_A;
好处:方便选中和快速添删。
虽然分号或句号是表示语句结束的终止符,可是逗号是一种链接符,用于链接要素,从这一点来讲,逗号的做用与 AND 或 OR 等是同样的。
CREATE DATABASE shop;
DROP DATABASE 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) );
DROP TABLE Product;
select *
的星号没法设定列的显示顺序,这时就会按照 CREATE TABLE 语句的定义对列进行排序。
DISTINCE
针对的是 select 后的全部列的去重,只须要在第一列的前面加就好(或者 * 前面)。所以,select DISTINCT "a","b"
不能够写成 select "a", DISTINCT "b"
。
下面介绍的 GROUP BY 也能够达到去重的效果。
大多数人都喜欢先写 SELECT 再写 FROM,但推荐先写 FROM 再写 SELECT,由于符合 SQL 的执行顺序,方便理解。
若是把从 SELECT 子句开始写的方法称为自顶向下法,那么从 FROM 子句开始写的方法就能够称为自底向上法。
所谓聚合
,就是将多行汇总为一行。
COUNT:计算表中的记录数(行数)
SUM:计算表中数值列中数据的合计值
AVG:计算表中数值列中数据的平均值
MAX:求出表中任意列中数据的最大值
MIN:求出表中任意列中数据的最小值
注意:MAX/MIN 函数和 SUM/AVG 函数有一点不一样,那就是 SUM/ AVG 函数只能对数值类型的列使用,而 MAX/MIN 函数原则上能够适用于任何数据类型的列。
聚合函数广泛会将 NULL 排除在外( COUNT 函数例外),具体以下:
一、COUNT 函数的结果根据参数的不一样而不一样。COUNT(*)
会获得包含 NULL 的数据行数,而 COUINT(<列名>)
会获得不包含 NULL的数据行数。
SELECT COUNT(DISTINCT <列名>)
能够获得不包含 NULL 且 不重复的数据行数。
二、SUM 函数将 NULL 忽略,也可理解成视为 0。
三、AVG 函数将 NULL 忽略,即不参与分母也不参与分子。
GROUP BY 通常习惯跟聚合函数搭配使用。
它们全都是非空集合。
全部子集的并集等于划分以前的集合。
任何两个子集之间都没有交集。
在数学(群论)中,知足以上3 个性质的各子集称为“类
”(partition),将原来的集合分割成若干个类的操做称为“分类
”。
因此 GROUP BY 和 下面要介绍的 PARTITION BY 都是用来划分 类 的函数。
若是用了 GROUP BY 却没用聚合函数,多半是为了去重,但有 DISTINCT 呀:
-- 一、DISTINCT SELECT DISTINCT "product_type" FROM "Product" -- 二、GROUP BY SELECT "product_type" FROM "Product" GROUP BY "product_type"
注意:上面两种结果,都会保留 NULL 行。
答:用 DISTINCT。可读性优先。
缘由:多余的列并无被聚合,固然没法显示。
解决方案:SELECT 子句中只能存在如下三种元素:
常数
聚合函数
GROUP BY 子句中指定的列名(也就是聚合键)
缘由:跟执行顺序有关:FROM→ WHERE→ GROUP BY→ SELECT
解决方案:其实 PostgreSQL 支持这种写法。但推荐为了遵循 标准 SQL ,尽可能不要这样写。
例子:
-- 错误1: SELECT "product_type", COUNT(*) FROM "Product" WHERE COUNT(*) = 2 GROUP BY "product_type" -- 错误2: SELECT "product_type", COUNT(*) FROM "Product" GROUP BY "product_type" WHERE COUNT(*) = 2 -- 正确: SELECT "product_type", COUNT(*) FROM "Product" GROUP BY "product_type" HAVING COUNT(*) = 2
缘由:WHERE 子句不能够使用聚合函数,由于他针对的是行而不是组。
解决方案:请用下面会介绍的 HAVING 子句代替这里的 WHERE。
WHERE 子句 —— 指定行条件
HAVING 子句 —— 指定组条件
例子:
-- WHERE 子句 SELECT * FROM "Product" WHERE "product_type" = '体育' -- HAVING 子句 SELECT "product_type", COUNT(*) FROM "Product" GROUP BY "product_type" HAVING COUNT(*) = 2 -- WHERE + HAVING 子句 SELECT "product_type", COUNT(*) FROM "Product" GROUP BY "product_type" WHERE "product_price" > 10 HAVING COUNT(*) = 2
HAVING 不加 GROUP BY(也可认为是对空字段进行了 GROUP BY 操做),整张表会被视为一个组。
SELECT '存在缺失的编号' AS gap FROM "Product" HAVING COUNT(*) <> MAX("product_id");
这种状况下,就不能在SELECT 子句里引用原来的表里的列了,要么就得像示例里同样使用常量,要么就得像 SELECT COUNT(*) 这样使用聚合函数。
相似于使用窗口函数时不指定 PARTITION BY 子句,就是把整个表看成一个窗口来处理。
例子:
-- 一、放在 HAVING SELECT "product_type", COUNT (*) FROM "Product" GROUP BY "product_type" HAVING "product_type" <> '衣服' -- 二、放在 WHERE SELECT "product_type", COUNT (*) FROM "Product" WHERE "product_type" <> '衣服' GROUP BY "product_type"
结论:放在 WHERE 子句。(能写在 WHERE 子句里的条件就不要写在 HAVING 子句里)
理由:
一、经过 WHERE 子句指定条件时,因为排序以前就对数据进行了过滤,所以可以减小排序的数据量。但 HAVING 子句是在排序以后オ对数据进行分组的,所以与在 WHERE 子句中指定条件比起来,须要排序的数据量就会多得多。
二、能够对 WIHERE 子句指定条件所对应的列建立索引,这样也能够大幅提升处理速度。
GROUP BY 生成的是派生表,HAVING 没法使用索引。
SELECT 语句末尾添加 ORDER BY 子句来明确指定排列顺序。
select * FROM "Activity" ORDER BY "id" DESC
ASC
—— ascendent(上升的)【省略即默认】
DESC
—— descendent(降低的)
执行顺序:FROM→ WHERE→ GROUP BY→ HAVING→ SELECT→ ORDER BY
注意:ORDER BY 子句中也能够使用聚合函数,跟 SELECT 里同样。
排序键中包含 NULL 时,会在开头或末尾进行汇总。到底是在开头显示仍是在末尾显示,并无特殊规定。每家 DBMS 可能不同。
PostgreSQL 是 ASC 在末尾,DESC 在开头。
INSERT INTO ProductIns (product_id, product_name, product_type, sale_price, purchase_price, regist_date) VALUES ('0001', 'T恤衫', '衣服', 1000, 500, '2009-09-20');
列清单
→ (product_id, product_name, product_type, sale_price, purchase_price, regist_date)
值清单
→ ('0001', 'T恤衫', '衣服', 1000, 500, '2009-09-20')
注意:
一、对表进行全列 INSERT 时,能够省略表名后的列清单。
二、对于指定了默认值的列,能够在列清单和值清单中都省略它,或者仅在值清单里键入 DEFAULT
。
拓展:多行插入
原则上,执行一次 INSERT 语句仅会插入一行数据。
但有的 RDBMS 支持 多行 INSERT。
语法即:多行的值清单,用逗号隔开。以下例:
INSERT INTO ProductIns VALUES ('0002', '打孔器', '办公用品', 500, 320, '2009-09-11'), ('0003', '运动T恤', '衣服', 4000, 2800, NULL), ('0004', '菜刀', '厨房用具', 3000, 2800, '2009-09-20');
该语法适用于 DB2 SQL、SOL Server、Postgresql 和 MYSQL,但不适用于 Oracle。
好处:减小了书写语句的数量,且直观方便理解。
坏处:排错困难。若发生 INSERT 错误,和单一行插入相比,找出究竟是哪行哪一个地方出错了,变得更加困难。
例如,建立了一个 ProductCopy 表,结构与以前使用的 Product 表彻底同样,只是更改了一下表名而己。而后:
-- 将 Product 中的数据复制到 ProductCopy 中 INSERT INTO ProductCopy (product_id, product_name, product_type, sale_price, purchase_price, regist_date) SELECT product_id, product_name, product_type, sale_price, purchase_price, regist_date FROM Product;
INSERT.. SELECT 中的 SELECT 语句,能够使用 WHERE 子句或者 GROUP BY 子句等任何 SQL 语法(但使用 ORDER BY 子句并不会产生任何效果)。
-- 删除表中数据 DELETE FROM Product WHERE sale_price >= 4000; -- 删除表中全部数据 TRUNCATE Product;
UPDATE Product SET sale_price = sale_price * 10 WHERE product_type = '厨房用具';
-- 使用逗号对列进行分隔排列 UPDATE Product SET sale_price = sale_price * 10, purchase_price = purchase_price / 2 WHERE product_type = '厨房用具'; -- 将列用()括起来的清单形式 (只能在 Postgresql 和 DB2 中使用) UPDATE Product SET (sale_price, purchase_price) = (sale_price * 10, purchase_price / 2) WHERE product_type = '厨房用具';
简单来说,事务
就是须要在同一个处理单元中执行的一系列更新处理的集合。
一、事务开始语句
标准 SQL —— 无
SQL Server、PostgreSQL —— BEGIN TRANSACTION
MySQL —— START TRANSACTION
Oracle、DB2 —— 无
二、DML 语句
三、事务结束语句
COMMIT
—— 提交处理
一旦提交,就没法恢复到事务开始前的状态了。
请在执行 DELETE 语句时尤为当心。
ROLLBACK
—— 取消处理
四、自动提交模式
自动提交模式 —— 每条 SQL 语句就是一个事务。
几乎全部的数据库产品的事务都默认开启了自动提交模式。
DBMS 的事务都遵循四种特性,将这四种特性的首字母结合起来统称为 ACID
特性。这是全部 DBMS 都必须遵照的规则。
一、原子性(Atomicity)
原子性是指在事务结東时,其中所包含的更新处理要么所有执行,要么彻底不执行,也就是要么占有一切要么一无全部。
二、一致性(Consistency)
一致性指的是事务中包含的处理要知足数据库提早设置的约束(即从一个正确的状态到另外一个正确的状态)。
一致性也称为完整性。
关于一致性的解释,其实网上有好几种版本,更多讨论见:如何理解数据库事务中的一致性的概念?
三、隔离性(Isolation)
隔离性指的是保证不一样事务之间互不干扰的特性。
四、持久性(Durability)
持久性也能够称为耐久性,指的是在事务(不管是提交仍是回滚)结束后,DBMS 可以保证该时间点的数据状态会被保存的特性。即便因为系统故障致使数据丢失,数据库也必定能经过某种手段进行恢复(如经过执行日志恢复)。
使用视图时并不会将数据保存到存储设备之中(正常的表),并且也不会将数据保存到其余任何地方。实际上视图保存的是 SELECT 语句,咱们从视图中读取数据时,视图会在内部执行该 SELECT 语句并建立出张临时表。
那么视图和表到底有什么不一样呢?区别只有一个,那就是“是否保存了实际的数据”。
一、因为视图无需保存数据,所以能够节省存储设备的容量。
二、因为视图保存的只是 SELECT 语句,所以表中的数据更新以后,视图也会自动更新,很是灵活方便。
三、能够将频繁使用的 SELECT 语句保存成视图,这样就不用每次都从新书写了。
CREATE VIEW ProductSum (product_type, cnt_product) AS SELECT product_type, COUNT(*) FROM Product GROUP BY product_type;
注意:
一、其实能够在视图的基础上再建立新的视图,可是咱们仍是应该尽可能避免。这是由于对多数 DBMS 来讲,多重视图
会下降 SQL 的性能。推荐使用仅使用单一视图
。
二、定义视图时不能使用 ORDER BY 子句(也没有意义,由于”表“数据原本就没有顺序的概念)。
但在 PostgreSQL 中能够。
一、查询 —— SELECT
SELECT product_type, cnt_product FROM ProductSum;
二、(同步)更新 —— INSERT、DELETE、UPDATE
视图和表会同时进行更新。
注意:经过汇总获得的视图没法进行更新,好比视图存在:
SELECT 子句中未使用DISTINCT
FROM 子句中只有一张表
未使用 GROUP BY 子句
未使用 HAVING 子句
PostgreSQL 若是要同步更新,须要事先执行一些语句,有点麻烦,这里略过不赘述了。
DROP VIEW ProductSum; -- 若是删除多重视图,可能会由于关联致使删除失败,这时能够使用 CASCADE DROP VIEW ProductSum CASCADE;
上面的视图有个问题,若是没有通过深刻思考就定义复杂的视图,可能会带来巨大的性能问题。特别是视图的定义语句中包含如下运算的时候:
聚合函数(AVG、COUNT、SUM、MIN、MAX)
集合运算符(UNION、INTERSECT、EXCEPT 等)
最近愈来愈多的数据库为了解决视图的这个缺点,实现了物化视图
(materialized view)技术。
PostgreSQL v9.3 才支持。
物化视图既真的是一个实实在在存在的表。
建立方法:
CREATE MATERIALIZED VIEW ProductSum (product_type, cnt_product) AS SELECT product_type, COUNT(*) FROM Product GROUP BY product_type;
其他方法与普通视图相似,不赘述了。
子查询
就是将用来定义视图的 SELECT 语句直接用于 FROM 子句当中。
能够理解成一张一次性视图,在 SELECT 语句执行以后就消失了。
-- 建立视图 CREATE VIEW ProductSum (product_type, cnt_product) AS SELECT product_type, COUNT(*) FROM Product GROUP BY product_type; -- 使用视图 SELECT product_type, cnt_product FROM ProductSum; -- === 等同于 === -- 子查询 写法 SELECT product_type, cnt_product FROM ( SELECT product_type, COUNT(*) AS cnt_product FROM Product GROUP BY product_type ) AS ProductSum;
注意:
子查询做为内层查询会首先执行
子查询能够继续嵌套子查询,甚至无限嵌套下去
为子查询设定名称时须要使用 AS 关键字,该关键字有时也能够省略
标量子查询
必须返回表中某一行的某一列的值。
标量
就是单一的意思,在数据库以外的领域也常用。
应用:因为返回的是单一的值,所以标量子查询能够用在 = 或者 <> 这样须要单一值的比较运算符之中。
-- 错误写法:在 WHERE 子句中不能使用聚合函数 SELECT product_id, product_name, sale_price FROM Product WHERE sale_price > AVG(sale_price); -- 正确写法 SELECT product_id, product_name, sale_price FROM Product WHERE sale_price > (SELECT AVG(sale_price) FROM Product);
问:关联子查询 和 非关联子查询的区别:
答:
非关联子查询:先执行内层查询,再执行外层查询
关联子查询:先执行外层查询,再执行内层查询(内层查询必须引用外层查询的变量)
例子:选取出 product_type 商品中高于该类商品的平均销售单价的商品了。
-- 错误写法:由于是 WHERE 比较的值不是标量 SELECT product_id, product_name, sale_price FROM Product WHERE sale_price > ( SELECT AVG(sale_price) FROM Product GROUP BY product_type ); -- 正确写法 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 -- 这句可要可不要 ); -- 错误写法:做用域错误。子查询内部能够看到外部,而外部看不到内部。 SELECT product_type, product_name, sale_price FROM Product AS P1 WHERE P1.product_type = P2.product_type AND sale_price > ( SELECT AVG(sale_price) FROM Product AS P2 GROUP BY product_type );
关联子查询的缺点:
可读性差
性能未必好
为何把这三块合并成一章,由于 谓词 和 CASE 表达式 本质上也是函数。
函数大体能够分为如下几种:
一、算术函数(用来进行数值计算的函数)
二、字符串函数(用来进行字符串操做的函数)
三、日期函数(用来进行日期操做的函数)
四、转换函数(用来转换数据类型和值的函数
五、聚合函数(用来进行数据聚合的函数)
SELECT COALESCE ( NULL, 1 ) AS col_1, COALESCE ( NULL, 'test', NULL ) AS col_2, COALESCE ( NULL, NULL, '2009-11-01' ) AS col_3;
普通语言里的布尔型只有 true 和 false 两个值,这种逻辑体系被称为二值逻辑
。
而 SQL 语言里,除此以外还有第三个值 unknown
,这种逻辑体系被称为三值逻辑
(three-valued logic)。关系数据库里引进了NULL
,因此不得不一样时引进第三个布尔值(可是 unknown 值不能被直接引用,直接使用的只能是 NULL)。
历史上最先提出三值逻辑(three-valued-logic)体系的是波兰的著名逻辑学家卢卡西维茨(Jan Lukasiewicz, 1878—1956)。在二十世纪二十年代,他定义了“真”和“假”以外的第三个逻辑值“可能”。
谓词逻辑
中,原子命题
分解成个体词
和谓词
。 个体词是能够独立存在的事或物,包括现实物、精神物和精神事三种。谓词则是用来刻划个体词的性质的词,即刻画事和物之间的某种关系表现的词。如“苹果”是一个现实物个体词,"苹果能够吃"是一个原子命题,“能够吃”是谓词,刻划“苹果”的一个性质,即与动物或人的一个关系。
因此,在谓词逻辑中,谓词的做用是,“判断(个体词)是否存在知足某种条件”,且返回真值
(在三值逻辑里,即 TRUE/ FALSE/ UNKNOWN)。
在逻辑中,真值(truth value),又称逻辑值(logical value),是指示一个陈述在什么程度上是真的。在计算机编程上多称作布林值、布尔值。
拓展 ——
排中律
(Law of Excluded Middle)就是指不承认中间状态,对命题真伪的断定黑白分明。是否认可这必定律被认为是古典逻辑学和非古典逻辑学的分界线。如,约翰的年龄,在现实世界中,“要么是20 岁,要么不是20 岁”——这样的常识在三值逻辑里却未必正确,也有可能未知,即 unknown。故,在 SQL 的世界里,排中律是不成立的。
谓词逻辑的出现具备划时代的意义,缘由就在于为命题分析提供了函数式的方法。因此谓词能够通俗理解为函数,区别在于返回值:
函数的返回值有多是数字、字符串或者日期等
谓词的返回值全都是真值
表经常被认为是行的集合,但从谓词逻辑的观点看,也能够认为是命题的集合。
一样,如 WHERE 子句,其实也能够当作是由多个谓词组合而成的新谓词。只有能让WHERE 子句的返回值为真的命题,才能从表(命题的集合)中查询到。
=
、<>
、>=
、>
、<=
、<
不等于也能够写做
!=
,可是为了兼容性,仍是推荐使用 标准sql 里的<>
。
一、LIKE
二、BETWEEN
如:WHERE sale_price BETWEEN 100 AND 1000;
左闭右闭
三、IS NULL
、IS NOT NULL
判断是否为 NULL 就不要用 <> 了,而是用这个。
四、限定谓词 - IN
(ANY
)
IN 是多个 OR 的简便用法,如 "col" IN (320, 500, 5000); 或 200 IN ("col1", "col2", "col3");
IN 还有个别称叫 ANY,为了跟下面的 ALL 对应。
五、限定谓词 - ALL
ALL 是 多个AND 的简便用法,如 "col" ALL (320, 500, 5000); 或 200 ALL ("col1", "col2", "col3");
拓展:推荐使用极值函数代替 ALL
SELECT * FROM Class_A WHERE age < ( SELECT MIN(age) FROM Class_B WHERE city = '东京' );
推荐缘由:极值函数在统计时会把为 NULL 的数据排除掉,避免出错。
ALL 跟 极值函数在语义上仍是有细微区别的:
● ALL 谓词:他的年龄比在东京住的全部学生都小
● 极值函数:他的年龄比在东京住的年龄最小的学生还要小
可是极值函数也有隐患,极值函数(聚合函数)在输入为空表(空集)时会返回 NULL。
建议:使用 COALESCE 函数将极值函数返回的 NULL 处理成合适的值。
很像 lodash 的 get 方法,给个返回的默认值。
六、EXISTS
例子:有 Product 产品表 和 ShopProduct 店铺表,选取出“大阪店(shop_id:000C)在售商品的销售单价”。
-- IN 写法 SELECT product_name, sale_price FROM Product WHERE product_id IN ( SELECT product_id FROM ShopProduct WHERE shop_id = '000C' ); -- EXISTS 写法 [推荐] 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 ); -- NOT EXISTS 写法 —— “东京店(shop_id:000A)在售以外的商品的销售单价” SELECT product_name, sale_price FROM Product AS P WHERE NOT EXISTS ( SELECT * FROM ShopProduct AS SP WHERE SP.shop_id = '000A' AND SP.product_id = P.product_id );
注意:① 这里的 SELECT *
,返回哪些列都没有关系(固然惯例仍是用 *
最好),由于 EXIST 只关心记录是否存在。
拓展:NOT EXISTS 具有有差集运算的功能。
七、NOT
、AND
、OR
NOT 运算符用来否认某一条件,可是不能滥用。不然会下降可读性。
谓词逻辑中,根据输入值的阶数(order)对谓词进行分类。
= 或者 BETWEEEN 等大多数输入值为一行的谓词叫做“一阶谓词
”,
而像 IN(ANY)、ALL 、EXISTS 还有 HAVING 这样输入值为行的集合的谓词叫做“二阶谓词
”。
二阶谓词通常都习惯跟 关联子查询 搭配使用。
二阶谓词,如 IN 和 EXISTS 和 HAVING 在不少状况下都是能够互换的,
三阶谓词
=输入值为“集合的集合”的谓词
四阶谓词
=输入值为“集合的集合的集合”的谓词
咱们能够像上面这样无限地扩展阶数,可是SQL 里并不会出现三阶以上的状况,因此不用太在乎。
使用过List、Hakell 等函数式语言或者Java 的读者可能知道“
高阶函数
”这一律念。它指的是不以通常的原子性的值为参数,而以函数为参数的函数。
上面从谓词的角度分类,这里咱们按照运算符的角度来划分的话:
一、算术运算符
+
、-
、*
、/
、%
二、比较运算符
即上面介绍的 比较谓词。
三、逻辑运算符
即上面介绍的 其余谓词。
四、其余运算符
||
:拼接字符串
运算符的优先级:(圆括号)> 算术运算符 > 比较运算符 > 逻辑运算符。
其中,逻辑运算符中的优先级:NOT > AND > OR。
一、NULL 不是值
NULL 容易被认为是值的缘由恐怕有两个。
第一个是在 C 语言等编程语言里面,NULL 被定义为了一个常量(不少语言将其定义为了整数0),这致使了人们的混淆。可是,其实 SQL 里的 NULL 和其余编程语言里的 NULL 是彻底不一样的东西。
第二个缘由是,IS NULL 这样的谓词是由两个单词构成的,因此人们容易把 IS 看成谓词,而把 NULL 看成值。咱们应该把 IS NULL 看做是一个谓词。所以,若是能够的话,写成 IS_NULL 这样也许更合适。
二、由于 NULL 不是值,因此常见的对 NULL 的说法也是错的:“列的值为NULL”、“NULL 值”……
三、算术运算符 赶上 NULL 结果都是 NULL
四、比较运算符 和 逻辑运算符 赶上 NULL 结果基本上是 unknown,或者说,对 NULL 使用谓词后的结果基本上是 unknown。
为何说基本上? 由于有特殊状况,如遇到 OR 和 AND,仍是会分别出现结果是 TRUE 和 FALSE 的;或者 EXIST
具体参考下面的真值表:
三值逻辑的真值表(AND)
AND | t | u | f |
---|---|---|---|
t | t | u | f |
u | u | u | f |
f | f | f | f |
三值逻辑的真值表(OR)
OR | t | u | f |
---|---|---|---|
t | t | t | t |
u | t | u | u |
f | t | u | f |
从上面的叙述,你能够看出 NULL 是有多么特殊和多么容易引发错误了。
NULL 最恐怖的地方就在于即便你认为本身已经彻底驾驭它了,但仍是一不当心就会被它在背后捅一刀。
一、避免使用的方法
加上 NOT NULL 约束
使用默认值
编号:使用异常编号
例如 ISO 的性别编号中,除了 “1: 男性”,“2: 女性”,还定义了 “0: 未知”,“9: 不适用” 这两个用于异
常状况的编号。
名字:使用“无名氏”
例如名字用 “未知” or “UNKNOWN” 代替,类别用"-"代替。
数值:使用 0
日期:用最大值或最小值代替
例如开始日期和结束日期,能够使用 0000-01-01 或者 9999-12-31。
二、但你没法100%避免
没法彻底消除 NULL 的缘由是它扎根于关系数据库的底层中。仅靠上面提到的方法并不足够。
例如,使用外链接,或者 SQL-99 中添加的带 CUBE 或 ROLLUP 的 GROUP BY 时,仍是很容易引入 NULL 的。
三、结论
所以咱们能作的最多也只是 “尽可能”去避免 NULL 的产生,并在不得不使用时适当使用。
注意:因为有 NULL 捣鬼,因此 IN 会返回 true / fasle / unknown,而 EXISTS 只会返回 true / false。所以,IN 和 EXISTS 能够互相替换使用,而 NOT IN 和NOT EXISTS 却不能够。
具体缘由能够回去翻阅原书,这里不赘述。
问:那参数是子查询时,用 IN 仍是 EXISTS 更好呢?
-- IN SELECT * FROM Class_A WHERE id IN ( SELECT id FROM Class_B ); -- EXISTS SELECT * FROM Class_A A WHERE EXISTS ( SELECT * FROM Class_B B WHERE A.id = B.id );
若是把上例的 Class_A 当作外表,Class_B 当作内表的话。
答:
维度一:从外表和内表的数据行大小的关系来看
一、IN 只执行一次,此内表查出后就缓存了,因此 IN 适合 外表 > 内表 的状况;
二、EXISTS 是针对外表去做循环,每次循环会跟内表做关联子查询,因此 EXISTS 适合 外表 < 内表 的状况;
三、当 内外表 数据差很少大时,IN 与 EXISTS 也差很少。
维度二:索引的角度
EXISTS 能够用到索引,而 IN 不行。因此 EXISTS 更佳。
维度三: 是否全表遍历
针对内表,EXISTS 只要查到一行数据知足条件就会终止遍历,而 IN 必须遍历整个内表。 因此 EXISTS 更佳。
维度四: 可读性
IN 更佳。
综上所述:仍是考虑实际状况。可是 EXISTS 替代 IN 提升性能的可能性更大。
一、要想改善 IN 的性能,除了使用 EXISTS,还能够使用链接。
二、最近有不少数据库也尝试着改善了 IN 的性能。例如,在 Oracle 数据库中,若是咱们使用了建有索引的列,那么即便使用 IN 也会先扫描索引;PostgreSQL 从版本 7.4 起也改善了使用子查询做为 IN 谓词参数时的查询速度。
注意:其实这个问题也能够当成 非关联子查询 vs. 关联子查询 来看待(除了维度三是 EXISTS 特有的优点外,其余的维度都适用)。
CASE表达式
的语法分为简单CASE表达式
和搜索CASE表达式
两种。
因为 搜索CASE 表达式 包含了 简单CASE 表达式 的所有功能,因此有更强大的表达能力。
注意:CASE 表达式是一种表达式而不是语句,CASE 表达式常常会由于同编程语言里的 CASE 混淆而被叫做 CASE 语句,实际上是不对的。(你也能够把 CASE 表达式理解成一种函数,运行后会返回值)
编程语言中的 CASE 语句,还有 break 的概念,而 SQL 中的 CASE 表达式没有。
例子:
-- 使用 搜索CASE表达式 的状况【推荐】 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 FROM Product; -- 使用 简单CASE表达式 的状况 SELECT product_name, CASE product_type WHEN '衣服' THEN 'A :' || product_type WHEN '办公用品' THEN 'B :' || product_type WHEN '厨房用具' THEN 'C :' || product_type ELSE NULL END AS abc_product_type FROM Product;
注意:
一、统一各分支返回的数据类型。例如用 CAST 函数。
二、ELSE 子句能够省略不写,这时会被默认为 ELSE NULL
。但仍是建议 养成写 ELSE 子句的习惯,减小失误。
三、记得写 END 。
四、WHEN NULL
错误,WHEN IS NULL
正确。
统计不一样县的男女比例(“县名”的列为:pref_name,“人口”的列为:population)
-- == old 写法: -- 男性人口 SELECT pref_name, SUM(population) FROM PopTbl2 WHERE sex = '1' GROUP BY pref_name; -- 女性人口 SELECT pref_name, SUM(population) FROM PopTbl2 WHERE sex = '2' GROUP BY pref_name; -- == new 写法: SELECT pref_name, -- 男性人口 SUM( CASE WHEN sex = '1' THEN population ELSE 0 END) AS cnt_m, -- 女性人口 SUM( CASE WHEN sex = '2' THEN population ELSE 0 END) AS cnt_f FROM PopTbl2 GROUP BY pref_name;
新手用 WHERE 子句进行条件分支,高手用 SELECT 子句进行条件分支。
std_id ( 学号) 、club_id ( 社团ID) 、club_name ( 社团名) 、main_club_flg ( 主社团标志)
获取只加入了一个社团的学生的社团ID。
获取加入了多个社团的学生的主社团ID。
-- == old 写法: -- 条件1 :选择只加入了一个社团的学生 SELECT std_id, MAX(club_id) AS main_club FROM StudentClub GROUP BY std_id HAVING COUNT(*) = 1; -- 条件2 :选择加入了多个社团的学生 SELECT std_id, club_id AS main_club FROM StudentClub WHERE main_club_flg = 'Y' ; -- == new 写法: SELECT std_id, CASE WHEN COUNT(*) = 1 THEN MAX(club_id) ELSE MAX( CASE WHEN main_club_flg = 'Y' THEN club_id ELSE NULL END ) END AS main_club FROM StudentClub GROUP BY std_id;
新手用 HAVING 子句进行条件分支,高手用 SELECT 子句进行条件分支。
假设某公司规定 “女性员工的工资必须在 20 万日元如下”
CONSTRAINT check_salary CHECK ( CASE WHEN sex = '2' THEN CASE WHEN salary <= 200000 THEN 1 ELSE 0 END ELSE 1 END = 1 )
例子1:屡次循环更新
例如你要:
对当前工资为 30 万日元以上的员工,降薪 10%。
对当前工资为 25 万日元以上且不满 28 万日元的员工,加薪 20%。
-- 错误写法:(问题在于,第一次的 UPDATE 操做执行后,“当前工资”发生了变化,若是还用它看成第二次 UPDATE 的断定条件,结果就会出错。) UPDATE Salaries SET salary = salary * 0.9 WHERE salary >= 300000; UPDATE Salaries SET salary = salary * 1.2 WHERE salary >= 250000 AND salary < 280000; -- 正确写法: UPDATE Salaries SET salary = CASE WHEN salary >= 300000 THEN salary * 0.9 WHEN salary >= 250000 AND salary < 280000 THEN salary * 1.2 ELSE salary -- 注意这里的 `ELSE salary` 很是重要 END;
例子2:两个值交换(替代传统的使用中间值的作法)
UPDATE SomeTable SET p_key = CASE WHEN p_key = 'a' THEN 'b' WHEN p_key = 'b' THEN 'a' ELSE p_key END WHERE p_key IN ('a', 'b');
面向对象语言以对象的方式来描述世界,而面向集合语言
SQL 以集合的方式来描述世界。
表的加法(并集) —— UNION
(UNION ALL)
表的减法(差集) —— EXCEPT
(EXCEPT ALL)
表的(交集) —— INTERSECT
(INTERSECT ALL)
表的乘法、除法下面会提到。
上面运算符后加了 ALL
的表示算出结果后,不会除去重复记录。
加了 ALL 就不会为了除去重复行而发生排序,因此性能会有提高。
注意事项 ① —— 做为运算对象的记录的列数必须相同
注意事项 ② —— 做为运算对象的记录中列的类型必须一致
注意事项 ③ —— ORDER BY 子句只能在最最后使用一次
注意事项 ④ —— UNION 和 INTERSECT 都具备幂等性,而 EXCEPT 不具备
-- 方法一 : 经过 集合运算符 EXCEPT 求补集 DELETE FROM Products WHERE rowid IN ( SELECT rowid -- 所有rowid FROM Products EXCEPT -- 减去 SELECT MAX(rowid) -- 要留下的rowid FROM Products GROUP BY name, price ); -- 方法二 : 或者省略集合运算符 EXCEPT ,直接经过 NOT IN 求补集 DELETE FROM Products WHERE rowid NOT IN ( SELECT MAX(rowid) FROM Products GROUP BY name, price );
联结其实属于 表的乘法(笛卡尔积)。
它是应用最普遍的联结。
例子:
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;
外联结分:
LEFT OUTER JOIN —— 简写 LEFT JOIN
RIGHT OUTER JOIN —— 简写 RIGHT JOIN
外联结指定主表的关键字是 LEFT 和 RIGHT。最终的结果中会包含主表的全部数据。
平时仍是习惯用左联结多一些。左联结有一个优点:通常状况下表头都出如今左边(笔者没碰见过表头出如今右边的状况)。使用左边的表做为主表的话,SQL 就能和执行结果在格式上保持一致。这样一来,在看到 SQL 语句时,咱们很容易就能想象出执行结果的格式。
例子:
SELECT P1.name AS name_1, P2.name AS name_2 FROM Products P1 CROSS JOIN Products P2; -- 旧写法 SELECT P1.name AS name_1, P2.name AS name_2 FROM Products P1, Products P2;
进行交叉联结时没法使用内联结和外联结中所使用的 ON 子句,这是由于交叉联结是对两张表中的所有记录进行交叉组合,所以结果中的记录数一般是两张表行数的乘积(笛卡儿积)。
交叉联结在实际业务中几乎并不会使用,那为何还要在这里进行介绍呢?这是由于交叉联结是全部联结
运算的基础。内联结是交叉联结的一部分,“内”也能够理解为“包含在交叉联结结果中的部分”。相反,外联结的“外”能够理解为“交叉联结结果以外的部分”。
全外联结 = 左外联结 UNION 右外联结
全外联结是可以从这样两张内容不一致的表里,没有遗漏地获取所有信息的方法,因此也能够理解成“把两张表都看成主表来使用”的链接。
-- 全外联结 SELECT COALESCE(A.id, B.id) AS id, A.name AS A_name, B.name AS B_name FROM Class_A A FULL OUTER JOIN Class_B B ON A.id = B.id; -- 数据库不支持全外联结时的替代方案 SELECT A.id AS id, A.name, B.name FROM Class_A A LEFT OUTER JOIN Class_B B ON A.id = B.id UNION SELECT B.id AS id, A.name, B.name FROM Class_A A RIGHT OUTER JOIN Class_B B ON A.id = B.id;
拓展:A 和 B 的异或
一种是 (A UNION B) EXCEPT (A INTERSECT B),另外一种是 (A EXCEPT B) UNION (B EXCEPT A)。
两种方法都比较麻烦,性能开销也大。建议用 FULL OUTER JOIN 来作:
SELECT COALESCE(A.id, B.id) AS id, COALESCE(A.name , B.name ) AS name FROM Class_A A FULL OUTER JOIN Class_B B ON A.id = B.id WHERE A.name IS NULL OR B.name IS NULL;
-- …… FROM ShopProduct AS SP INNER JOIN Product AS P ON SP.product_id = P.product_id INNER JOIN InventoryProduct AS IP ON SP.product_id = IP.product_id
如今是 3 张表,即便把联结的表增长到 4 张、5 张以上也是彻底相同的写法。
截至目前并无 DBMS 实现集合的除法。
所以,必须本身实现。方法比较多,其中具备表明性的:
嵌套使用 NOT EXISTS。
使用 HAVING 子句转换成一对一关系。
把除法变成减法。
针对相同的表进行的链接被称为“自链接
”(self join)。
原书 《SQL 基础教程》里都叫 xx 联结,到《SQL 进阶教程》又都变成了 xx 链接。
可见 联结 和 链接 能够通用。 本文又跟着使用混乱,请谅解。
假若有 Products 表:
name(商品名称) price(价格) 苹果 50 橘子 100 香蕉 80
一、可重排列
SELECT P1.name AS name_1, P2.name AS name_2 FROM Products P1, Products P2;
结果:
name_1 name_2
苹果 苹果
苹果 橘子
苹果 香蕉
橘子 苹果
橘子 橘子
橘子 香蕉
香蕉 苹果
香蕉 橘子
香蕉 香蕉
二、去重排列(考虑顺序,即有序对)
SELECT P1.name AS name_1, P2.name AS name_2 FROM Products P1, Products P2 WHERE P1.name <> P2.name;
结果:
name_1 name_2
苹果 橘子
苹果 香蕉
橘子 苹果
橘子 香蕉
香蕉 苹果
香蕉 橘子
三、去重排列(不考虑顺序,即无序对)
SELECT P1.name AS name_1, P2.name AS name_2 FROM Products P1, Products P2 WHERE P1.name > P2.name;
结果:
name_1 name_2
苹果 橘子
香蕉 橘子
香蕉 苹果
四、去重排列(不考虑顺序,即无序对)且 扩展成 3 列
SELECT P1.name AS name_1, P2.name AS name_2, P3.name AS name_3 FROM Products P1, Products P2, Products P3 WHERE P1.name > P2.name AND P2.name > P3.name;
结果:
name_1 name_2
香蕉 苹果 橘子
上面 应用1 里的 二、三、4 都是使用除 “=” 之外的其余比较运算符,如 “<、>、<>”,这样进行的链接称为 "非等值链接
"。
假若有个 Products 表,有 name 和 price 两列:
方法一:关联子查询
须要使用由数据库独自实现的行ID。
例如, Oracle 数据库里的
rowid
,或者 PostgreSQL 里的ctid
。
DELETE FROM Products P1 WHERE rowid < ( SELECT MAX(P2.rowid) FROM Products P2 WHERE P1.name = P2.name AND P1.price = P2.price );
方法二:EXISTS(关联子查询) + 非等值链接
DELETE FROM Products P1 WHERE EXISTS ( SELECT * FROM Products P2 WHERE P1.name = P2.name AND P1.price = P2.price AND P1.rowid < P2.rowid );
假若有个 Products 表,有 name 和 price 两列:
从 Products 表里查找价格相等但商品名称不一样的记录
SELECT DISTINCT P1.name, P1.price FROM Products P1, Products P2 WHERE P1.price = P2.price AND P1.name <> P2.name;
方法一:用窗口函数
SELECT name, price, RANK() OVER (ORDER BY price DESC) AS rank_1, DENSE_RANK() OVER (ORDER BY price DESC) AS rank_2 FROM Products;
方法二:自链接 + 非等值链接
-- 排序从 1 开始。若是已出现相同位次,则跳过以后的位次 -- 一、关联子查询 SELECT P1.name, P1.price, ( SELECT COUNT(P2.price) FROM Products P2 WHERE P2.price > P1.price ) + 1 AS rank_1 FROM Products P1 ORDER BY rank_1; -- 二、表的链接 SELECT P1.name, MAX(P1.price) AS price, COUNT(P2.name) +1 AS rank_1 FROM Products P1 LEFT OUTER JOIN Products P2 ON P1.price < P2.price GROUP BY P1.name ORDER BY rank_1;
此处蕴含了递归集合的思想。
本章介绍的 窗口函数 和 GROUPING 运算符都是为了实现 OLAP 用途而添加的功能,是 SQL 里比较新的功能。
截止到 2016 年 5 月,Oracle、SQL Server、DB二、PostgreSQL 的最新版本都已经支持这些功能了,但MySQL 的最新版本 5.7 仍是不支持这些功能。
OLAP
是 OnLine Analytical Processing 的简称,意思是对数据库数据进行实时分析处理,用来诸如生成报表。例如,市场分析、建立财务报表、建立计划等平常性商务工做。
下面会结合原书 + 我以前的一篇文章《 PostgreSQL 窗口函数 ( Window Functions ) 如何使用?》+ 本身的理解,梳理下。
窗口函数跟聚合仍是挺像的,但区别是:
窗口函数不会像聚合同样将参与计算的行合并成一行输出,而是将计算出来的结果带回到了计算行上。
完整示例:
SELECT "product_name", "product_type", "sale_price", AVG ("sale_price") OVER ( PARTITION BY "product_type" ORDER BY "sale_price" ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING ) AS "avg" FROM "Product";
一、AVG (sale_price)
为窗口函数
。
窗口函数大致能够分为如下两种:
全部的聚合函数都能用做窗口函数,如(SUM、AVG、COUNT、MAX、MIN)
RANK、DENSE_RANK、ROW_NUMBER 等专用窗口函数
问:专用窗口函数 跟 聚合函数 的用法区别 ?
答:
因为专用窗口函数无需参数,所以一般括号中都是空的。而聚合函数通常都须要传参来指定列名。
原则上窗口函数只能在 SELECT 子句中使用。其理由是,在 DBMS 内部,窗口函数是对 WHERE 子句或者 GROUP BY 子句处理后的“结果”进行的操做。
二、PARTITION BY "product_type"
的 PARTITION BY
,相似于 GROUP BY。经过 PARTITION BY 分组后的记录集合称为窗口
。此处的窗口并不是“窗户”的意思,而是表明范围
。
PARTITION BY 能够省略,表明所有记录集合为一个窗口。
三、ORDER BY "sale_price"
的 ORDER BY
,是在窗口函数调用前,先把每一个窗口内的记录集合排序。
问:为何用 GROUP BY 的时候不须要加 ORDER BY ?
答:由于跟 GROUP BY 一块儿使用的聚合函数针对的记录集合是每个分组,排不排序不影响最终结果,而窗口函数针对的记录集合是每个窗口里的子范围(这个子范围即”框架“,下面即将介绍 ),因此排序很关键。
ORDER BY 能够省略,即默认排序。
四、ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
中,ROWS
用来定义窗口内的(行)范围,称为框架
。
有三种写法:
① ROWS 2 PRECEDING -- 以前
② ROWS 2 FOLLOWING -- 以后
③ ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING -- 之间
可用
UNBOUNDED
代替数字表示无边界。
以 ① 为例,ROWS 2 PRECEDING 就是将窗口内的范围指定为“截止到以前 2 行”,也就是将做为汇总对象的记录限定为以下的 “最靠近的 3 行”:
● 自身(当前记录)
● 以前1行的记录
● 以前2行的记录
这样的统计方法称为移动平均
(moving average)。
因为这种方法在但愿实时把握“最近状态”时很是方便,所以经常会应用在对股市趋势的实时跟踪当中。
ROWS 能够省略,默认值为:
ORDER BY
,默认使用窗口内全部行,等于 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
ORDER BY
,默认使用窗口内第一行到当前值 ,等于ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
例子1(用于累计):
SELECT product_id, product_name, sale_price, AVG (sale_price) OVER (ORDER BY product_id) AS current_avg FROM Product;
结果:
product_id | product_name | sale_price | current_avg ----------+-----------+-------------+----------------------- 0001 | T恤衫 | 1000 | 1000.0000000000000000 ←(1000)/1 0002 | 打孔器 | 500 | 750.0000000000000000 ←(1000+500)/2 0003 | 运动T恤 | 4000 | 1833.3333333333333333 ←(1000+500+4000)/3 0004 | 菜刀 | 3000 | 2125.0000000000000000 ←(1000+500+4000+3000)/4 0005 | 高压锅 | 6800 | 3060.0000000000000000 ←(1000+500+4000+3000+6800)/5 0006 | 叉子 | 500 | 2633.3333333333333333 0007 | 擦菜板 | 880 | 2382.8571428571428571 0008 | 圆珠笔 | 100 | 2097.5000000000000000
例子2(用于排名):
先介绍 3 个专用窗口函数,用来排名的。
RANK
函数计算排序时,若是存在相同位次的记录,则会跳过以后的位次。
例:有3 条记录排在第1 位时:1 位、1 位、1 位、4 位……
DENSE_RANK
函数一样是计算排序,即便存在相同位次的记录,也不会跳过以后的位次。
例:有3 条记录排在第1 位时:1 位、1 位、1 位、2 位……
ROW_NUMBER
函数赋予惟一的连续位次。
例:有3 条记录排在第1 位时:1 位、2 位、3 位、4 位……
例子:
SELECT product_name, product_type, sale_price, RANK () OVER (ORDER BY sale_price) AS ranking, DENSE_RANK () OVER (ORDER BY sale_price) AS dense_ranking, ROW_NUMBER () OVER (ORDER BY sale_price) AS row_num FROM Product;
结果:
product_name | product_type | sale_price | ranking | dense_ranking | row_num 圆珠笔 | 办公用品 | 100 | 1 | 1 | 1 叉子 | 厨房用具 | 500 | 2 | 2 | 2 打孔器 | 办公用品 | 500 | 2 | 2 | 3 擦菜板 | 厨房用具 | 880 | 4 | 3 | 4 T恤衫 | 衣服 | 1000 | 5 | 4 | 5 菜刀 | 厨房用具 | 3000 | 6 | 5 | 6 运动T恤 | 衣服 | 4000 | 7 | 6 | 7 高压锅 | 厨房用具 | 6800 | 8 | 7 | 8
上面的 累计 和 排名,本质上都属于同一种计算逻辑,即冯·诺依曼型递归集。
若是在 SQL 里写了不少重复的 OVER(),能够提取成一个 window 变量,简化代码。
SELECT *, avg("score") OVER window_frame as "subject_avg_score", avg("score") OVER window_frame as "subject_avg_score_2", avg("score") OVER window_frame as "subject_avg_score_3" FROM "testScore" window window_frame as (PARTITION BY "subject")
ROLLUP
能够用来同时得出合计和小计。而避免用 UNION 繁琐的方式。
SELECT product_type, regist_date, SUM(sale_price) AS sum_price FROM Product GROUP BY ROLLUP(product_type, regist_date);
结果:
product_type | regist_date | sum_price |
---|---|---|
16780 | ||
厨房用具 | 11180 | |
厨房用具 | 2008-04-28 | 880 |
厨房用具 | 2009-01-15 | 6800 |
厨房用具 | 2009-09-20 | 3500 |
办公用品 | 600 | |
办公用品 | 2009-09-11 | 500 |
办公用品 | 2009-11-11 | 100 |
衣服 | 5000 | |
衣服 | 2009-09-20 | 1000 |
衣服 | 4000 |
GROUP BY ROLLUP (product_type, regist_date);
的结果等于:
① GROUP BY ()
② GROUP BY (product_type)
③ GROUP BY (product_type, regist_date)
三者的 UNION。
其中 ① 中的 GROUP BY () 表示没有聚合键,也就至关于没有 GROUP BY 子句(这时会获得所有数据的合计行的记录)。
上面结果中,第 一、二、三、七、十、12 行称为超级分组记录
(super group row)。
SELECT CASE WHEN GROUPING(product_type) = 1 THEN '商品种类 合计' ELSE product_type END AS product_type, CASE WHEN GROUPING(regist_date) = 1 THEN '登记日期 合计' ELSE CAST(regist_date AS VARCHAR(16)) END AS regist_date, SUM(sale_price) AS sum_price FROM Product GROUP BY ROLLUP(product_type, regist_date);
上面 (1)只用 ROLLUP 的例子,超级分组记录都存在 null 数据的状况,为了不阅读的混淆,SQL 提供了一个用来判断超级分组记录的 NULL 的特定函数—— GROUPING
函数。该函数在其参数列的值为超级分组记录所产生的 NULL 时返回 1 ,其余状况返回 0。
结果:
product_type | regist_date | sum_price |
---|---|---|
商品种类 合计 | 登记日期 合计 | 16780 |
厨房用具 | 登记日期 合计 | 11180 |
厨房用具 | 2008-04-28 | 880 |
厨房用具 | 2009-01-15 | 6800 |
厨房用具 | 2009-09-20 | 3500 |
办公用品 | 登记日期 合计 | 600 |
办公用品 | 2009-09-11 | 500 |
办公用品 | 2009-11-11 | 100 |
衣服 | 登记日期 合计 | 5000 |
衣服 | 2009-09-20 | 1000 |
衣服 | 4000 |
上面 (2)用 ROLLUP + GROUPING 的例子,直接把 ROLLUP 改写成 CUBE 就行:
SELECT CASE WHEN GROUPING(product_type) = 1 THEN '商品种类 合计' ELSE product_type END AS product_type, CASE WHEN GROUPING(regist_date) = 1 THEN '登记日期 合计' ELSE CAST(regist_date AS VARCHAR(16)) END AS regist_date, SUM(sale_price) AS sum_price FROM Product GROUP BY CUBE(product_type, regist_date);
GROUP BY CUBE (product_type, regist_date);
的结果等于
① GROUP BY ()
② GROUP BY (product_type)
③ GROUP BY (regist_date) ←新添的组合
④ GROUP BY (product_type, regist_date)
三者的 UNION。
CUBE 生成的 GROUP BY 组合,是 2 的 n 次方(n 是聚合键的个数)。
这就是 CUBE 如此起名的由来。
结果(第3-8行是比以前多出来的):
product_type | regist_date | sum_price |
---|---|---|
商品种类 合计 | 登记日期 合计 | 16780 |
商品种类 合计 | 2008-04-28 | 880 |
商品种类 合计 | 2009-01-15 | 6800 |
商品种类 合计 | 2009-09-11 | 500 |
商品种类 合计 | 2009-09-20 | 4500 |
商品种类 合计 | 2009-11-11 | 100 |
商品种类 合计 | 4000 | |
厨房用具 | 登记日期 合计 | 11180 |
厨房用具 | 2008-04-28 | 880 |
厨房用具 | 2009-01-15 | 6800 |
厨房用具 | 2009-09-20 | 3500 |
办公用品 | 登记日期 合计 | 600 |
办公用品 | 2009-09-11 | 500 |
办公用品 | 2009-11-11 | 100 |
衣服 | 登记日期 合计 | 5000 |
衣服 | 2009-09-20 | 1000 |
衣服 | 4000 |
由于 GROUPING SETS 会得到不固定结果,所以与 ROLLUP 或者CUBE 比起来,使用GROUPING SETS 的机会不多。
这里姑且略过。
驱动
就是应用和数据库这两个世界之间的桥梁。
如今普遍使用的驱动标准主要有 ODBC
(Open DataBase Connectivity)和 JDBC
(Java Data Base Connectivity)两种。ODBC 是1992 年微软公司发布的 DBMS 链接标准,后来逐步成为了业界标准。JDBC 是在此基础上制定出来的 Java 应用链接标准。
以 PostgreSQL 为例,它的官网列有针对各类编程语言(应用)的驱动:
https://www.postgresql.org/docs/current/external-interfaces.html
分类:
一、使用纯语言实现的 Postgresql 驱动,如 JDBC 等方式。这种链接方式不须要 libpq 库。
二、经过包装 PostgreSQL 的 C 语言接口库 libpg
实现的驱动,好比,Python 下的 psycopg 库、ODBC 、(下面要介绍的) node-postgres 等。因此在安装这些驱动以前,须要先安装 PostgreSQL 的 libpq 库。
在上面的官网资料中能够查到,node-postgres
是针对 Node.js 的驱动,官网:https://node-postgres.com/
安装:$ npm install pg
Node.js 应用通常不直接用 node-postgres,而经常使用 sequelize,但 sequelize 本质也是对 node-postgres 等一些驱动的封装。
正如 sequelize 的安装步骤:
一、先安装 sequelize
npm install --save sequelize
二、为所选数据库安装驱动程序:
# One of the following: $ npm install --save pg pg-hstore # Postgres $ npm install --save mysql2 $ npm install --save mariadb $ npm install --save sqlite3 $ npm install --save tedious # Microsoft SQL Server
有效的防护方法,就是全面改用参数化查询
。
参数化查询的原理是预处理
,先将 SQL 语句进行编译,这样注入的数据就不会被当作 SQL 语句执行,而只当作 参数值 来处理。
埃德加·弗兰克·科德(英语:Edgar Frank Codd, 1923年8月23日-2003年4月18日),下简称 Codd,是关系模型(Relational model)
和关系数据库的祖师爷。
数据库因采用了关系模型,才被称为关系数据库。
Codd 写了两篇与关系模型相关的论文。第一篇是写于1969年的《大型数据库中关系存储的可推导性、冗余与一致性》。遗憾的是这篇论文发表在 IBM 公司内部期刊 IBM Research Report 上了,所以并无引发外界的注意。
在接下来的 1970 年,Codd 又在权威学术杂志 Communications of ACM 上,以《大型共享数据库的关系模型》
为题发表了第二篇论文。至此,关系模型真正地问世了。如今人们读到的论文基本上都是这一篇。可是,就像 C.J. Date 说的那样,这篇论文充满了学术味道,并且比较偏重理论和数学,因此即便是数据库方面的专家,通常也不会去阅读。
后来,Codd 凭借在关系型数据库方面的贡献得到了 1981 年的图灵奖。
一、维基百科解释:关系模型是基于谓词逻辑和集合论的一种数据模型,主要用于关系型数据库。
谓词逻辑(准确地说是“一阶谓词逻辑”)和集合论的知识在上文都介绍了。
二、教科书《数据库系统原理》解释:关系模型是用二维表的形式表示实体和实体间联系的数据模型。(而其中的二维表即关系
)
虽然关系和表看上去很像,可是仍是有区别的:
一、关系中不容许存在重复的元组(tuple),而表中能够存在。
二、关系中的记录不存在顺序,而表存在。即:关系中的元组没有从上往下的顺序,而表中的行有从上往下的顺序;关系中的属性没有从左往右的顺序,而表中的列有从左往右的顺序。
三、关系可能须要知足范式,而表无所谓。(下面会介绍范式)
可是咱们日常的平常语言,仍是会混淆的称呼,下面是严格的对应关系:
关系(relation) | 表(table) |
---|---|
元组(tuple) | 行(row)或记录(record) |
势(cardinality) | 行数(number of rows) |
属性(attribute) | 列(column)或字段(field) |
度(degree) | 列数(number of columns) |
定义域(domain) | 列的取值集合(pool of legal values) |
关系不仅是集合,它还有许多很是有趣的性质。
其中之一就是“封闭性
”(closure property)。这个性质简单地说就是“运算的输入和输出都是关系”,换句话来讲,就是“保证关系世界永远封闭”的性质。
上面提到关系模型诞生历史,即 Codd 在 1970 年的第二篇论文里,首次出现了范式的概念,不过只有第一范式的想法(第二范式、第三范式的定义陆续出如今他以后的论文中)。
范式
是“符合某一种级别的关系模式的集合,表示一个关系内部各属性之间的联系的合理化程度”。通俗理解就是:一张数据表的表结构所符合的某种设计标准的级别。
目前关系数据库有六种范式:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)、第四范式(4NF)和第五范式(5NF,又称完美范式)。
知足最低要求的范式是第一范式(1NF)。在第一范式的基础上进一步知足更多规范要求的称为第二范式(2NF),其他范式以次类推。通常说来,数据库只需知足到第三范式 (3NF)就好了。
一、1NF
:列是最小的单元(原子性约束),不可再分。
如:一个”省市区“字段,同时存了省市区(多是用逗号分隔的字符串类型、或者是用了数组类型),应该拆分红"省"、"市"和“区”。
通常来讲,在宿主语言中能够灵活选择数组、结构体、对象等多种数据类型来表现非规范化的数据。可是在插入到数据库中时,必须将它们分解成标量值,即按照第一范式进行规范化,而后再存入数据库。
但在关系数据库诞生三十年后,SQL-99 进行了扩展,使得咱们能够定义不知足第一范式的“数组类型”。(还有后来的 JSON/JSONB 类型)这个扩展对关系模型来讲到底是好仍是坏,还不能轻易下判断。
然而在宿主语言和数据库之间传递和接收数据时,应该有不少读者由于双方支持的数据结构不一致而苦恼过吧?特别是面向对象语言和关系数据库不一致的问题,这种问题称为“阻抗不匹配”。因而可知,但愿数据库能支持在宿主语言中可用的数据结构这种需求也是有道理的。
二、2NF
:知足1NF。表中要有主键(唯一性约束),且非主键列必须彻底依赖于所有主键而非部分主键。
如:一个订单表【OrderDetail】(OrderID,ProductID,ProductName,ProductUnitPrice,Quantity),OrderID + ProductID 是主键,虽然 Quantity 是彻底依赖于 OrderID + ProductID 主键的,但 ProductName、ProductUnitPrice 只部分依赖于 ProductID 主键,因此应该把 OrderDetail 表拆分为 Order 表 和 Product 表。
三、3NF
:知足2NF。非主键列是直接依赖于主键,而不是直接依赖于非主键列。
如:仍是上面的例子, 一个订单表【OrderDetail】(OrderID,ProductID,ProductName,ProductUnitPrice,Quantity),仅 OrderID 是主键,ProductID,ProductName,ProductUnitPrice,Quantity 都确实彻底依赖 OrderID 主键,但其中,ProductName,ProductUnitPrice 是经过先依赖 ProductID,再经过 ProductID 依赖 OrderID 的方式来传递依赖的。因此仍是应该把 OrderDetail 表拆分为 Order 表 和 Product 表。
由于范式的主要目的是为了消除冗余(范式级别越高,冗余越小),因此:
好处:
下降存储成本(数据库范式是在20世纪提出的,当时的磁盘存储成本还很高。)
提升拓展性
坏处:
下降性能。没有任何冗余的表设计会产生更多的查询行为。
增长设计表结构的难度
既然范式是为了消除冗余,那么反范式
就是经过增长冗余、聚合的手段来提高性能。尤为对如今的互联网应用来讲,性能比存储成本的要求更高。
参考最近又流行的 noSQL 数据库,就是大大的冗余。
建议:仍是根据自身的业务特色在范式和反范式中找到平衡点。
与编程(面向过程的)语言不一样,在 SQL 语言中,用户不能显式地命令数据库进行排序操做。
因此,能触发排序的表明性的运算有下面这些:
GROUP BY 子句
ORDER BY 子句
聚合函数(SUM、COUNT、AVG、MAX、MIN)
DISTINCT
集合运算符(UNION、INTERSECT、EXCEPT)没加 ALL
窗口函数(RANK、ROW_NUMBER 等)
为了性能,请尽可能避免触发排序,若是不能,也尽可能针对索引字段的排序。
使用索引时,条件表达式的左侧应该是原始字段。
错误:WHERE col_1 * 1.1 > 100; 正确:WHERE col_1 > 100 / 1.1
由于 NULL 并非值 ,因此索引字段并不会索引 NULL。
此处存疑,可见这篇辩驳:http://www.javashuo.com/article/p-pfhcvviq-ck.html
例如:
<>
NOT IN
NOT EXISTS
假设存在这样顺序的一个联合索引:“col_1, col_2, col_3”。
× SELECT * FROM SomeTable WHERE col_1 = 10 AND col_3 = 500 ; × SELECT * FROM SomeTable WHERE col_2 = 100 AND col_3 = 500 ; × SELECT * FROM SomeTable WHERE col_2 = 100 AND col_1 = 10 ; ○ SELECT * FROM SomeTable WHERE col_1 = 10; ○ SELECT * FROM SomeTable WHERE col_1 = 10 AND col_2 = 100 ; ○ SELECT * FROM SomeTable WHERE col_1 = 10 AND col_2 = 100 AND col_3 = 500;
在 col_1 和 col_2 上分别创建了不一样的索引,或者创建了(col_1, col_2)这样的联合索引时,若是使用 OR 链接条件,那么要么用不到索引,要么用到了可是效率比 AND 要差不少。
WHERE col_1 > 100 OR col_2 = 'abc';
若是不管如何都要使用OR,那么有一种办法是位图索引。可是这种索引的话更新数据时的性能开销会增大,因此使用以前须要权衡一下利弊。
使用 LIKE 谓词时,只有前方一致的匹配才能用到索引。
× SELECT * FROM SomeTable WHERE col_1 LIKE '%a'; × SELECT * FROM SomeTable WHERE col_1 LIKE '%a%'; ○ SELECT * FROM SomeTable WHERE col_1 LIKE 'a%';
SQL 其中一个数学基础就是构建在集合论上。
咱们经过画维恩图
,能够很大程度地加深对 SQL 的理解。
SQL 并无提供任何用于检查集合的包含关系或者相等性的谓词。IN 谓词只能用来检查元素是否属于某个集合(∈),而不能检查集合是不是某个集合的子集(∪)。
听说,IBM 过去研制的第一个关系数据库实验系统——System R 曾经实现了用 CONTAINS 这一谓词来检查集合间的包含关系,可是后来由于性能缘由被删除掉了,直到如今也没有恢复。
而判断集合之间的包含关系,就是下面要提到的:全称量化。
“全部的 x 都知足条件P”或者“存在(至少一个)知足条件 P 的 x”。
前者称为“全称量词
”,后者称为“存在量词
”,分别记做 ∀ 、∃。
其实,全称量词的符号实际上是将字母 A 上下颠倒而造成的,存在量词则是将字母 E 左右颠倒而造成的。
“对于全部的x,……”的英语是“for All x,…”,而“存在知足……的x”的英语是“there Exists x that…”,这就是这两个符号的由来。
但惋惜,SQL 只支持 EXISTS
(存在量词),不支持 FORALL
(全称量词)。
但全称量词和存在量词只要定义了一个,另外一个就能够被推导出来。
经过德·摩根定律
,来进行 “确定⇔双重否认” 之间的转换。
即在 SQL 中,为了表达全称量化,须要将 “全部的行都知足条件 P” 这样的命题转换成 “不存在不知足条件 P 的行“,而后使用存在量词。
例子1:
查询条件为确定的:“全部科目分数都在50 分以上”,转换成它的双重否认:“没有一个科目分数不满50 分”,而后用 NOT EXISTS 来表示转换后的命题,即:
SELECT DISTINCT student_id FROM TestScores TS1 WHERE NOT EXISTS -- 不存在知足如下条件的行 ( SELECT * FROM TestScores TS2 WHERE TS2.student_id = TS1.student_id AND TS2.score < 50 -- 分数不满 50 分的科目 );
例子2:
“全部队员都处于待命状态”转化成“不存在不处于待命状态的队员”
不光能够用 NOT EXISTS ,也能够有其余方式:
-- 方法一:用谓词表达全称量化命题 SELECT team_id, member FROM Teams T1 WHERE NOT EXISTS ( SELECT * FROM Teams T2 WHERE T1.team_id = T2.team_id AND status <> '待命' ); -- 方法二:用集合表达全称量化命题(1) SELECT team_id FROM Teams GROUP BY team_id HAVING COUNT(*) = SUM( CASE WHEN status = '待命' THEN 1 ELSE 0 END ); -- 方法三:用集合表达全称量化命题(2) SELECT team_id FROM Teams GROUP BY team_id HAVING MAX(status) = '待命' AND MIN(status) = '待命'; -- 方法4、比上面方式的更直观的展现方式(但性能比上面差):列表显示各个队伍是否全部队员都在待命 SELECT team_id, CASE WHEN MAX(status) = '待命' AND MIN(status) = '待命' THEN '全都在待命' ELSE '队长!人手不够' END AS status FROM Teams GROUP BY team_id;
NOT EXISTS 写法跟 其余方法( HAVING 子句或者 ALL 谓词)的区别:
NOT EXISTS 写法可读性差
NOT EXISTS 性能更好
下面是整理的在调查集合性质时常常用到的条件。
这些条件能够在 HAVING 子句中使用,也能够经过SELECT 子句写在CASE 表达式里使用。
No | 条件表达式 | 用途 |
---|---|---|
1 | COUNT (DISTINCT col) = COUNT (col) | col 列没有重复的值 |
2 | COUNT(*) = COUNT(col) | col 列不存在NULL |
3 | COUNT(*) = MAX(col) | col 列是连续的编号(起始值是1) |
4 | COUNT(*) = MAX(col) - MIN(col) + 1 col | 列是连续的编号(起始值是任意整数) |
5 | MIN(col) = MAX(col) | col 列都是相同值,或者是NULL |
6 | MIN(col) * MAX(col) > 0 | col 列全是正数或全是负数 |
7 | MIN(col) * MAX(col) < 0 | col 列的最大值是正数,最小值是负数 |
8 | MIN(ABS(col)) = 0 | col 列最少有一个是0 |
9 | MIN(col - 常量) = - MAX(col - 常量) | col 列的最大值和最小值与指定常量等距 |
先建一张 Digits 表:
digit |
---|
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
-- 方法一:直接求(如1~542) SELECT D1.digit + (D2.digit * 10) + (D3.digit * 100) AS seq FROM Digits D1 CROSS JOIN Digits D2 CROSS JOIN Digits D3 WHERE D1.digit + (D2.digit * 10) + (D3.digit * 100) BETWEEN 1 AND 542 ORDER BY seq; -- 方法2、先生成视图待用 -- 一、生成序列视图(包含0~999) CREATE VIEW Sequence (seq) AS SELECT D1.digit + (D2.digit * 10) + (D3.digit * 100) FROM Digits D1 CROSS JOIN Digits D2 CROSS JOIN Digits D3; -- 二、从序列视图中获取1~100 SELECT seq FROM Sequence WHERE seq BETWEEN 1 AND 100 ORDER BY seq;
有了这个连续编号,咱们能够接着干不少事,具体可参考原书。
精巧的数据结构搭配笨拙的代码,远远好过笨拙的数据结构搭配精巧的代码。——大教堂与集市
即便放眼 SQL 以外的其余编程语言,各个编程语言的历史中也都一直存在着“如何对程序员隐藏地址”的课题。与 C 语言以及汇编语言相比,Pascal、Java、Perl 等新一代的语言都在努力地对用户隐藏指针。在这一点上,关系数据库与 SQL 的发展轨迹是一致的。
如 C 的指针,在 Java / Python 中被
引用
代替。引用相似指针,只是不能进行指针运算,好比不能用 p + 1 指向下一个元素。
可能会有人列举出用户能够使用的指针,好比 Oracle 中的 rowid 或 PostgreSQL 中的 oid 来反对。确实,用户能够使用这些指针,可是它们都是个别数据库厂商违反 SQL 标准而进行的扩展,而标准 SQL一直在努力摆脱指针。
例如,SQL 和数据库都在极力提高数据在表现层的抽象度,以及对用户隐藏物理层的概念。
所以放弃地址的深入意义是,经过放弃掉系统中没有意义的东西,创造出一个易于人类理解的有意义的世界。
《程序设计能从冯·诺依曼风格中解放出来吗?程序的函数风格及其代数》中提到,只要使用变量,就没法逃出地址的魔咒。反过来讲,之因此SQL 能成为不依赖于地址的自由的语言,也是由于它不使用变量。
其实 SQL 仍是有变量的,也能够理解成它是个别数据库厂商违反 SQL 标准而进行的扩展吧。
与 SQL 同样不使用变量的语言还有 Lisp。它是一种年龄仅次于 Fortran 的高级语言,已经能够称得上是编程语言中的“老将”。
SQL 很大程度上是一种声明式编程
,可是其也含有过程式编程的元素。
例如,关联子查询是为了使 SQL 可以实现相似面向过程语言中循环的功能而引入的。
SQL 在设计之初,就有意地避免了循环。因此 SQL 中没有专门的循环语句。
虽然能够使用游标实现循环,可是这样的话仍是面向过程的作法。
因此,咱们用好 SQL,就要有从面向过程思惟向声明式思惟、面向集合思惟转变的意识。
区别:
命令式编程(Imperative)
(也叫:过程式编程):详细的去命令机器怎么(How)去处理一件事情以达到你想要的结果(What)声明式编程(Declarative)
:只告诉你想要的结果(What),机器本身摸索过程(How)例如:
个人观点:
一、全部的 命令式编程 本质上也是 声明式编程。底层仍是会有交给机器本身处理的(How)步骤。
二、在命令式编程 也能够”创造”出 声明式编程,如 JavaScript 中使用 lodash 库,等于(How)步骤交给 lodash 去处理。