《SQL基礎教程》+《SQL進階教程》學習筆記

寫在前面:本文主要注重 SQL 的理論、主流覆蓋的功能範圍及其基本語法/用法。至於詳細的 SQL 語法/用法,由於每家 DBMS 都有些許不一樣,我會在之後專門介紹某款DBMS(例如 PostgreSQL)的時候寫到。html

第 1 章 DBMS 與 SQL


一、DBMS 是什麼 ?

數據庫管理系統(Database Management System, DBMS) 是用來管理數據庫的計算機系統。node

本文采用 PostgreSQLDBMSmysql

二、爲何要用 DBMS ?

問:爲何不用 文本文件 或者 excel(+VBA)?c++

答:DBMS 的好處有:git

  • 多人共享程序員

  • 海量存儲正則表達式

  • 可編程sql

  • 容災機制數據庫

  • ……npm

三、DBMS 分類

(1)層次數據庫(Hierarchical Database, HDB)
(2)關係數據庫(Relational Database, RDB)

1969年誕生,形式爲行列二維表,相似 excel,包括:

  • SQL(Structured Query Language,結構化查詢語言)

  • 關係數據庫管理系統(Relational Database Management System,RDBMS

如無特殊說明,本文所提到的 DBMS 都是指 RDBMS。

(3)面向對象數據庫(Object Oriented Database, OODB)
(4)XML 數據庫(XML Database, XMLDB)
(5)鍵值存儲系統(Key- Value Store, KVS)

四、DBMS 常見架構

C/S 結構。

五、搭建 DBMS 環境

參考個人文章 《PostgreSQL 安裝 & 用戶配置》

六、SQL

(1)SQL 介紹

SQL(Structured Query Language : 結構化查詢語言)是一種特定目的編程語言,用於管理關係數據庫管理系統(RDBMS)。

(2)SQL 歷史

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。

(3)SQL 分類

一、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 操做。)

(4)SQL 註釋
  • 單行註釋 —— -- 以後

  • 多行註釋 —— /**/之間

(5)SQL 書寫建議

一、關鍵字大寫(雖然 SQL 不區分關鍵字的大小寫。)

二、前置逗號

SELECT col_1
, col_2
, col_3
, col_4
FROM tbl_A;

好處:方便選中和快速添刪。

雖然分號或句號是表示語句結束的終止符,可是逗號是一種鏈接符,用於鏈接要素,從這一點來講,逗號的做用與 AND 或 OR 等是同樣的。

第2章 SQL 基礎


一、數據庫 / 表

(1)數據庫

CREATE DATABASE shop;

DROP DATABASE shop;

(2)表
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

  • select * 的星號沒法設定列的顯示順序,這時就會按照 CREATE TABLE 語句的定義對列進行排序。

  • DISTINCE 針對的是 select 後的全部列的去重,只須要在第一列的前面加就好(或者 * 前面)。所以,select DISTINCT "a","b" 不能夠寫成 select "a", DISTINCT "b"

    下面介紹的 GROUP BY 也能夠達到去重的效果。

  • 大多數人都喜歡先寫 SELECT 再寫 FROM,但推薦先寫 FROM 再寫 SELECT,由於符合 SQL 的執行順序,方便理解。

    若是把從 SELECT 子句開始寫的方法稱爲自頂向下法,那麼從 FROM 子句開始寫的方法就能夠稱爲自底向上法。

第 3 章 聚合與排序


一、對錶進行聚合查詢 —— 聚合函數

所謂聚合,就是將多行彙總爲一行

(1)經常使用聚合函數
  • COUNT:計算表中的記錄數(行數)

  • SUM:計算表中數值列中數據的合計值

  • AVG:計算表中數值列中數據的平均值

  • MAX:求出表中任意列中數據的最大值

  • MIN:求出表中任意列中數據的最小值

注意:MAX/MIN 函數和 SUM/AVG 函數有一點不一樣,那就是 SUM/ AVG 函數只能對數值類型的列使用,而 MAX/MIN 函數原則上能夠適用於任何數據類型的列。

(2)注意 NULL

聚合函數廣泛會將 NULL 排除在外( COUNT 函數例外),具體以下:

一、COUNT 函數的結果根據參數的不一樣而不一樣。COUNT(*)會獲得包含 NULL 的數據行數,而 COUINT(<列名>) 會獲得不包含 NULL的數據行數。

SELECT COUNT(DISTINCT <列名>) 能夠獲得不包含 NULL 且 不重複的數據行數。

二、SUM 函數將 NULL 忽略,也可理解成視爲 0。

三、AVG 函數將 NULL 忽略,即不參與分母也不參與分子。

二、對錶進行分組 —— GROUP BY

GROUP BY 通常習慣跟聚合函數搭配使用。

(1)進階 - GROUP BY 與 數學中的 類
  1. 它們全都是非空集合。

  2. 全部子集的並集等於劃分以前的集合。

  3. 任何兩個子集之間都沒有交集。

在數學(羣論)中,知足以上3 個性質的各子集稱爲「」(partition),將原來的集合分割成若干個類的操做稱爲「分類」。

因此 GROUP BY 和 下面要介紹的 PARTITION BY 都是用來劃分 類 的函數。

問:去重用 GROUP BY 仍是 DISTINCT ?

若是用了 GROUP BY 卻沒用聚合函數,多半是爲了去重,但有 DISTINCT 呀:

-- 一、DISTINCT
SELECT DISTINCT "product_type"
FROM "Product"

-- 二、GROUP BY
SELECT "product_type"
FROM "Product"
GROUP BY "product_type"

注意:上面兩種結果,都會保留 NULL 行。

答:用 DISTINCT。可讀性優先。

常見錯誤 1 一一 在 SELECT 子句中書寫了多餘的列

緣由:多餘的列並無被聚合,固然沒法顯示。

解決方案:SELECT 子句中只能存在如下三種元素:

  • 常數

  • 聚合函數

  • GROUP BY 子句中指定的列名(也就是聚合鍵)

常見錯誤 2 一一 在 GROUP BY 子句中寫了在 SELECT 裏指定的列的別名

緣由:跟執行順序有關:FROM→ WHERE→ GROUP BY→ SELECT

解決方案:其實 PostgreSQL 支持這種寫法。但推薦爲了遵循 標準 SQL ,儘可能不要這樣寫。

常見錯誤 3 一一 在 WHERE 子句中使用聚合函數

例子:

-- 錯誤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。

三、爲聚合結果指定條件 —— HAVING

(1)WHERE vs. HAVING
  • 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
(2)HAVING 也可不加 GROUP BY

HAVING 不加 GROUP BY(也可認爲是對空字段進行了 GROUP BY 操做),整張表會被視爲一個組。

SELECT '存在缺失的編號' AS gap
FROM "Product" 
HAVING COUNT(*) <> MAX("product_id");

這種狀況下,就不能在SELECT 子句裏引用原來的表裏的列了,要麼就得像示例裏同樣使用常量,要麼就得像 SELECT COUNT(*) 這樣使用聚合函數

相似於使用窗口函數時不指定 PARTITION BY 子句,就是把整個表看成一個窗口來處理。

(3)對聚合鍵的篩選是放 WHERE 仍是 HAVING ?

例子:

-- 一、放在 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 沒法使用索引。

四、對查詢結果進行排序 —— ORDER BY

(1)用法

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 裏同樣。

(2)注意 NULL

排序鍵中包含 NULL 時,會在開頭或末尾進行彙總。到底是在開頭顯示仍是在末尾顯示,並無特殊規定。每家 DBMS 可能不同。

PostgreSQL 是 ASC 在末尾,DESC 在開頭。

第 4 章 數據更新


一、數據的插入 —— INSERT

方法1:使用 VALUES 子句指定具體的數據
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 錯誤,和單一行插入相比,找出究竟是哪行哪一個地方出錯了,變得更加困難。

方法2:從其餘表中複製數據 —— INSERT.. SELECT

例如,建立了一個 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 / TRUNCATE

-- 刪除表中數據
DELETE FROM Product WHERE sale_price >= 4000;

-- 刪除表中全部數據
TRUNCATE Product;

三、數據的更新 —— UPDATE

(1)更新 單列
UPDATE Product
SET sale_price = sale_price * 10
WHERE product_type = '廚房用具';
(2)更新 多列
-- 使用逗號對列進行分隔排列
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 = '廚房用具';

四、事務

簡單來說,事務就是須要在同一個處理單元中執行的一系列更新處理的集合。

(1)寫法

一、事務開始語句

  • 標準 SQL —— 無

  • SQL Server、PostgreSQL —— BEGIN TRANSACTION

  • MySQL —— START TRANSACTION

  • Oracle、DB2 —— 無


二、DML 語句


三、事務結束語句

COMMIT —— 提交處理

一旦提交,就沒法恢復到事務開始前的狀態了。

請在執行 DELETE 語句時尤爲當心。

ROLLBACK —— 取消處理


四、自動提交模式

自動提交模式 —— 每條 SQL 語句就是一個事務。

幾乎全部的數據庫產品的事務都默認開啓了自動提交模式。

(2)事務的 ACID 特性

DBMS 的事務都遵循四種特性,將這四種特性的首字母結合起來統稱爲 ACID 特性。這是全部 DBMS 都必須遵照的規則。

一、原子性(Atomicity)

原子性是指在事務結東時,其中所包含的更新處理要麼所有執行,要麼徹底不執行,也就是要麼佔有一切要麼一無全部。

二、一致性(Consistency)

一致性指的是事務中包含的處理要知足數據庫提早設置的約束(即從一個正確的狀態到另外一個正確的狀態)。

一致性也稱爲完整性

關於一致性的解釋,其實網上有好幾種版本,更多討論見:如何理解數據庫事務中的一致性的概念?

三、隔離性(Isolation)

隔離性指的是保證不一樣事務之間互不干擾的特性。

四、持久性(Durability)

持久性也能夠稱爲耐久性,指的是在事務(不管是提交仍是回滾)結束後,DBMS 可以保證該時間點的數據狀態會被保存的特性。即便因爲系統故障致使數據丟失,數據庫也必定能經過某種手段進行恢復(如經過執行日誌恢復)。

第 5 章 複雜查詢


一、視圖

(1)視圖跟表的區別

使用視圖時並不會將數據保存到存儲設備之中(正常的表),並且也不會將數據保存到其餘任何地方。實際上視圖保存的是 SELECT 語句,咱們從視圖中讀取數據時,視圖會在內部執行該 SELECT 語句並建立出張臨時表

那麼視圖和表到底有什麼不一樣呢?區別只有一個,那就是「是否保存了實際的數據」。

(2)優勢

一、因爲視圖無需保存數據,所以能夠節省存儲設備的容量。

二、因爲視圖保存的只是 SELECT 語句,所以表中的數據更新以後,視圖也會自動更新,很是靈活方便。

三、能夠將頻繁使用的 SELECT 語句保存成視圖,這樣就不用每次都從新書寫了。

(3)建立視圖
CREATE VIEW ProductSum (product_type, cnt_product)
AS
SELECT product_type, COUNT(*)
FROM Product
GROUP BY product_type;

注意:

一、其實能夠在視圖的基礎上再建立新的視圖,可是咱們仍是應該儘可能避免。這是由於對多數 DBMS 來講,多重視圖會下降 SQL 的性能。推薦使用僅使用單一視圖

二、定義視圖時不能使用 ORDER BY 子句(也沒有意義,由於」表「數據原本就沒有順序的概念)。

但在 PostgreSQL 中能夠。

(4)使用視圖

一、查詢 —— SELECT

SELECT product_type, cnt_product
FROM ProductSum;

二、(同步)更新 —— INSERT、DELETE、UPDATE

視圖和表會同時進行更新

注意:經過彙總獲得的視圖沒法進行更新,好比視圖存在:

  • SELECT 子句中未使用DISTINCT

  • FROM 子句中只有一張表

  • 未使用 GROUP BY 子句

  • 未使用 HAVING 子句

PostgreSQL 若是要同步更新,須要事先執行一些語句,有點麻煩,這裏略過不贅述了。

(5)刪除視圖
DROP VIEW ProductSum;

-- 若是刪除多重視圖,可能會由於關聯致使刪除失敗,這時能夠使用 CASCADE 
DROP VIEW ProductSum CASCADE;
(6)拓展 - 物化視圖

上面的視圖有個問題,若是沒有通過深刻思考就定義複雜的視圖,可能會帶來巨大的性能問題。特別是視圖的定義語句中包含如下運算的時候:

  • 聚合函數(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;

其他方法與普通視圖相似,不贅述了。

二、子查詢

(1)子查詢 概述

子查詢 就是將用來定義視圖的 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 關鍵字,該關鍵字有時也能夠省略

(2)標量子查詢

標量子查詢 必須返回表中某一行的某一列的值

標量就是單一的意思,在數據庫以外的領域也常用。

應用:因爲返回的是單一的值,所以標量子查詢能夠用在 = 或者 <> 這樣須要單一值的比較運算符之中。

-- 錯誤寫法:在 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);
(3)關聯子查詢【重難點】

問:關聯子查詢 和 非關聯子查詢的區別:

答:

  • 非關聯子查詢:先執行內層查詢,再執行外層查詢

  • 關聯子查詢:先執行外層查詢,再執行內層查詢(內層查詢必須引用外層查詢的變量)


例子:選取出 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
);

關聯子查詢的缺點:

  • 可讀性差

  • 性能未必好

第 6 章 函數、謂詞、CASE 表達式


爲何把這三塊合併成一章,由於 謂詞 和 CASE 表達式 本質上也是函數。

一、函數

函數大體能夠分爲如下幾種:

一、算術函數(用來進行數值計算的函數)

二、字符串函數(用來進行字符串操做的函數)

三、日期函數(用來進行日期操做的函數)

四、轉換函數(用來轉換數據類型和值的函數

五、聚合函數(用來進行數據聚合的函數)

(1)COALESC —— 將 NULL 轉換爲其餘值
SELECT 
    COALESCE ( NULL, 1 ) AS col_1,
    COALESCE ( NULL, 'test', NULL ) AS col_2,
    COALESCE ( NULL, NULL, '2009-11-01' ) AS col_3;

二、謂詞(運算符)與 NULL【重難點】

(1)三值邏輯

普通語言裏的布爾型只有 truefalse 兩個值,這種邏輯體系被稱爲二值邏輯

而 SQL 語言裏,除此以外還有第三個值 unknown,這種邏輯體系被稱爲三值邏輯(three-valued logic)。關係數據庫裏引進了NULL,因此不得不一樣時引進第三個布爾值(可是 unknown 值不能被直接引用,直接使用的只能是 NULL)。

歷史上最先提出三值邏輯(three-valued-logic)體系的是波蘭的著名邏輯學家盧卡西維茨(Jan Lukasiewicz, 1878—1956)。在二十世紀二十年代,他定義了「真」和「假」以外的第三個邏輯值「可能」。

(2)謂詞邏輯

謂詞邏輯中,原子命題分解成個體詞謂詞。 個體詞是能夠獨立存在的事或物,包括現實物、精神物和精神事三種。謂詞則是用來刻劃個體詞的性質的詞,即刻畫事和物之間的某種關係表現的詞。如「蘋果」是一個現實物個體詞,"蘋果能夠吃"是一個原子命題,「能夠吃」是謂詞,刻劃「蘋果」的一個性質,即與動物或人的一個關係。

因此,在謂詞邏輯中,謂詞的做用是,「判斷(個體詞)是否存在知足某種條件」,且返回真值(在三值邏輯裏,即 TRUE/ FALSE/ UNKNOWN)。

在邏輯中,真值(truth value),又稱邏輯值(logical value),是指示一個陳述在什麼程度上是真的。在計算機編程上多稱作布林值、布爾值。

拓展 —— 排中律(Law of Excluded Middle)就是指不承認中間狀態,對命題真僞的斷定黑白分明。是否認可這必定律被認爲是古典邏輯學非古典邏輯學的分界線。如,約翰的年齡,在現實世界中,「要麼是20 歲,要麼不是20 歲」——這樣的常識在三值邏輯裏卻未必正確,也有可能未知,即 unknown。故,在 SQL 的世界裏,排中律是不成立的。

謂詞邏輯的出現具備劃時代的意義,緣由就在於爲命題分析提供了函數式的方法。因此謂詞能夠通俗理解爲函數,區別在於返回值

  • 函數的返回值有多是數字、字符串或者日期等

  • 謂詞的返回值全都是真值

(3)謂詞邏輯在 SQL 中的應用

表經常被認爲是行的集合,但從謂詞邏輯的觀點看,也能夠認爲是命題的集合。

一樣,如 WHERE 子句,其實也能夠當作是由多個謂詞組合而成的新謂詞。只有能讓WHERE 子句的返回值爲真的命題,才能從表(命題的集合)中查詢到

(4)SQL 中的謂詞 —— 比較謂詞

=<>>=><=<

不等於也能夠寫做 != ,可是爲了兼容性,仍是推薦使用 標準sql 裏的 <>

(5)SQL 中的謂詞 —— 其餘謂詞

一、LIKE


二、BETWEEN

如:WHERE sale_price BETWEEN 100 AND 1000;

左閉右閉


三、IS NULLIS NOT NULL

判斷是否爲 NULL 就不要用 <> 了,而是用這個。


四、限定謂詞 - INANY

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 具有有差集運算的功能。


七、NOTANDOR

NOT 運算符用來否認某一條件,可是不能濫用。不然會下降可讀性。

(6)拓展 - N 階謂詞的劃分

謂詞邏輯中,根據輸入值的階數(order)對謂詞進行分類。

= 或者 BETWEEEN 等大多數輸入值爲一行的謂詞叫做「一階謂詞」,

而像 IN(ANY)、ALL 、EXISTS 還有 HAVING 這樣輸入值爲行的集合的謂詞叫做「二階謂詞」。

二階謂詞通常都習慣跟 關聯子查詢 搭配使用。

二階謂詞,如 IN 和 EXISTS 和 HAVING 在不少狀況下都是能夠互換的,

三階謂詞=輸入值爲「集合的集合」的謂詞

四階謂詞=輸入值爲「集合的集合的集合」的謂詞

咱們能夠像上面這樣無限地擴展階數,可是SQL 裏並不會出現三階以上的狀況,因此不用太在乎。

使用過List、Hakell 等函數式語言或者Java 的讀者可能知道「高階函數」這一律念。它指的是不以通常的原子性的值爲參數,而以函數爲參數的函數

(7)運算符

上面從謂詞的角度分類,這裏咱們按照運算符的角度來劃分的話:

一、算術運算符

+-*/%


二、比較運算符

即上面介紹的 比較謂詞。


三、邏輯運算符

即上面介紹的 其餘謂詞。


四、其餘運算符

||:拼接字符串


運算符的優先級:(圓括號)> 算術運算符 > 比較運算符 > 邏輯運算符。

其中,邏輯運算符中的優先級:NOT > AND > OR。

(8)特殊的 NULL

一、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
(9)避免使用 NULL

從上面的敘述,你能夠看出 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 的產生,並在不得不使用時適當使用

(10)拓展 —— EXISTS vs. IN

注意:因爲有 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 表達式理解成一種函數,運行後會返回值)

編程語言中的 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正確。

案例 一、用一條 SQL 語句進行不一樣條件的統計

統計不一樣縣的男女比例(「縣名」的列爲: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 子句進行條件分支。

案例 二、在 CASE 表達式中使用聚合函數

std_id ( 學號) 、club_id ( 社團ID) 、club_name ( 社團名) 、main_club_flg ( 主社團標誌)

  1. 獲取只加入了一個社團的學生的社團ID。

  2. 獲取加入了多個社團的學生的主社團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 子句進行條件分支。

案例 三、用 CHECK 約束定義多個列的條件關係

假設某公司規定 「女性員工的工資必須在 20 萬日元如下」

CONSTRAINT check_salary CHECK
( 
    CASE WHEN sex = '2' THEN 
        CASE WHEN salary <= 200000 THEN 1 
        ELSE 0 
        END
    ELSE 1 
    END = 1 
)
案例 四、在 UPDATE 語句裏進行條件分支,避免屢次循環更新的出錯

例子1:屢次循環更新

例如你要:

  1. 對當前工資爲 30 萬日元以上的員工,降薪 10%。

  2. 對當前工資爲 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');

第 7 章 集合運算【重難點】


面嚮對象語言以對象的方式來描述世界,而面向集合語言SQL 以集合的方式來描述世界。

一、集合運算符(以集合爲單位)

  • 表的加法(並集) —— UNION (UNION ALL)

  • 表的減法(差集) —— EXCEPT (EXCEPT ALL)

  • 表的(交集) —— INTERSECT (INTERSECT ALL)

表的乘法、除法下面會提到。

上面運算符後加了 ALL 的表示算出結果後,不會除去重複記錄。

加了 ALL 就不會爲了除去重複行而發生排序,因此性能會有提高。


注意事項 ① —— 做爲運算對象的記錄的列數必須相同

注意事項 ② —— 做爲運算對象的記錄中列的類型必須一致

注意事項 ③ —— ORDER BY 子句只能在最最後使用一次

注意事項 ④ —— UNION 和 INTERSECT 都具備冪等性,而 EXCEPT 不具備

(1)應用 - 刪除重複行
-- 方法一 : 經過 集合運算符 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
);

二、聯結(以列爲單位)

聯結其實屬於 表的乘法(笛卡爾積)。

(1)內聯結(INNER JOIN)

它是應用最普遍的聯結。

例子:

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;
(2)外聯結 (OUTER JOIN)

外聯結分:

  • LEFT OUTER JOIN —— 簡寫 LEFT JOIN

  • RIGHT OUTER JOIN —— 簡寫 RIGHT JOIN

外聯結指定主表的關鍵字是 LEFT 和 RIGHT。最終的結果中會包含主表的全部數據

平時仍是習慣用左聯結多一些。左聯結有一個優點:通常狀況下表頭都出如今左邊(筆者沒碰見過表頭出如今右邊的狀況)。使用左邊的表做爲主表的話,SQL 就能和執行結果在格式上保持一致。這樣一來,在看到 SQL 語句時,咱們很容易就能想象出執行結果的格式。

(3)交叉聯結(CROSS JOIN)

例子:

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 子句,這是由於交叉聯結是對兩張表中的所有記錄進行交叉組合,所以結果中的記錄數一般是兩張錶行數的乘積(笛卡兒積)

交叉聯結在實際業務中幾乎並不會使用,那爲何還要在這裏進行介紹呢?這是由於交叉聯結是全部聯結
運算的基礎
。內聯結是交叉聯結的一部分,「內」也能夠理解爲「包含在交叉聯結結果中的部分」。相反,外聯結的「外」能夠理解爲「交叉聯結結果以外的部分」。

(4)全外聯結(FULL OUTER JOIN)

全外聯結 = 左外聯結 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;
(5)多表聯結
-- ……
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 實現集合的除法。

所以,必須本身實現。方法比較多,其中具備表明性的:

  1. 嵌套使用 NOT EXISTS。

  2. 使用 HAVING 子句轉換成一對一關係。

  3. 把除法變成減法。

四、進階 - 自鏈接 與 非等值鏈接

(1)自鏈接

針對相同的表進行的鏈接被稱爲「自鏈接」(self join)。

原書 《SQL 基礎教程》裏都叫 xx 聯結,到《SQL 進階教程》又都變成了 xx 鏈接。

可見 聯結 和 鏈接 能夠通用。 本文又跟着使用混亂,請諒解。

應用1:可重排列、去重排列、組合

假若有 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

香蕉 蘋果 橘子

(2)非等值鏈接

上面 應用1 裏的 二、三、4 都是使用除 「=」 之外的其餘比較運算符,如 「<、>、<>」,這樣進行的鏈接稱爲 "非等值鏈接"。

應用2:刪除重複行

假若有個 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 
);
應用3:查找局部不一致的列

假若有個 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;
應用4:排序

方法一:用窗口函數

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;

此處蘊含了遞歸集合的思想。

第 8 章 SQL 高級處理


本章介紹的 窗口函數 和 GROUPING 運算符都是爲了實現 OLAP 用途而添加的功能,是 SQL 裏比較新的功能。

截止到 2016 年 5 月,Oracle、SQL Server、DB二、PostgreSQL 的最新版本都已經支持這些功能了,但MySQL 的最新版本 5.7 仍是不支持這些功能。

OLAP 是 OnLine Analytical Processing 的簡稱,意思是對數據庫數據進行實時分析處理,用來諸如生成報表。例如,市場分析、建立財務報表、建立計劃等平常性商務工做。

一、窗口函數

下面會結合原書 + 我以前的一篇文章《 PostgreSQL 窗口函數 ( Window Functions ) 如何使用?》+ 本身的理解,梳理下。

(1)窗口函數和聚合的區別

窗口函數跟聚合仍是挺像的,但區別是:

窗口函數不會像聚合同樣將參與計算的行合併成一行輸出,而是將計算出來的結果帶回到了計算行上。

(2)用法

完整示例:

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
(3)應用場景

例子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

上面的 累計 和 排名,本質上都屬於同一種計算邏輯,即馮·諾依曼型遞歸集。

(4)提取 OVER 變量

若是在 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")

二、GROUPING 運算符

一、ROLLUPーー同時得出合計和小計 ( GROUPING 函數ーー讓 NULL 更加容易分辨 )

ROLLUP 能夠用來同時得出合計和小計。而避免用 UNION 繁瑣的方式。

(1)只用 ROLLUP
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)。

(2)用 ROLLUP + GROUPING
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
二、CUBE——用數據來搭積木

上面 (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——取得指望的積木

由於 GROUPING SETS 會得到不固定結果,所以與 ROLLUP 或者CUBE 比起來,使用GROUPING SETS 的機會不多。

這裏姑且略過。

第 9 章 經過應用程序鏈接數據庫


一、驅動

驅動就是應用數據庫這兩個世界之間的橋樑。

二、驅動標準

如今普遍使用的驅動標準主要有 ODBC(Open DataBase Connectivity)和 JDBC(Java Data Base Connectivity)兩種。ODBC 是1992 年微軟公司發佈的 DBMS 鏈接標準,後來逐步成爲了業界標準。JDBC 是在此基礎上制定出來的 Java 應用鏈接標準。

三、PostgreSQL <=> Node.js

(1)針對 PostgreSQL 的驅動

以 PostgreSQL 爲例,它的官網列有針對各類編程語言(應用)的驅動:

https://www.postgresql.org/docs/current/external-interfaces.html

分類:

一、使用純語言實現的 Postgresql 驅動,如 JDBC 等方式。這種鏈接方式不須要 libpq 庫。

二、經過包裝 PostgreSQL 的 C 語言接口庫 libpg 實現的驅動,好比,Python 下的 psycopg 庫、ODBC 、(下面要介紹的) node-postgres 等。因此在安裝這些驅動以前,須要先安裝 PostgreSQL 的 libpq 庫。

(2)node-postgres

在上面的官網資料中能夠查到,node-postgres 是針對 Node.js 的驅動,官網:https://node-postgres.com/

安裝:$ npm install pg

(3)Sequelize

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

10、安全


一、SQL注入(SQL injection)

有效的防護方法,就是全面改用參數化查詢

參數化查詢的原理是預處理,先將 SQL 語句進行編譯,這樣注入的數據就不會被當作 SQL 語句執行,而只當作 參數值 來處理。

更多討論見:知乎 - 爲何參數化SQL查詢能夠防止SQL注入?

11、進階 - 關係數據庫的理論世界


一、關係模型 與 關係

(1)誕生

埃德加·弗蘭克·科德(英語: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 年的圖靈獎

(2)什麼是關係模型(關係)

一、維基百科解釋:關係模型是基於謂詞邏輯集合論的一種數據模型,主要用於關係型數據庫。

謂詞邏輯(準確地說是「一階謂詞邏輯」)集合論的知識在上文都介紹了。

二、教科書《數據庫系統原理》解釋:關係模型是用二維表的形式表示實體和實體間聯繫的數據模型。(而其中的二維表即關係

(3)關係跟表的區別?

雖然關係和表看上去很像,可是仍是有區別的:

一、關係中不容許存在重複的元組(tuple),而表中能夠存在。

二、關係中的記錄不存在順序,而表存在。即:關係中的元組沒有從上往下的順序,而表中的行有從上往下的順序;關係中的屬性沒有從左往右的順序,而表中的列有從左往右的順序。

三、關係可能須要知足範式,而表無所謂。(下面會介紹範式)


可是咱們日常的平常語言,仍是會混淆的稱呼,下面是嚴格的對應關係:

關係(relation) 表(table)
元組(tuple) 行(row)或記錄(record)
勢(cardinality) 行數(number of rows)
屬性(attribute) 列(column)或字段(field)
度(degree) 列數(number of columns)
定義域(domain) 列的取值集合(pool of legal values)
(4)關係的性質

關係不僅是集合,它還有許多很是有趣的性質

其中之一就是「封閉性」(closure property)。這個性質簡單地說就是「運算的輸入和輸出都是關係」,換句話來講,就是「保證關係世界永遠封閉」的性質。

二、範式(NF)

(1)誕生

上面提到關係模型誕生歷史,即 Codd 在 1970 年的第二篇論文裏,首次出現了範式的概念,不過只有第一範式的想法(第二範式、第三範式的定義陸續出如今他以後的論文中)。

(2)什麼是範式?

範式是「符合某一種級別的關係模式的集合,表示一個關係內部各屬性之間的聯繫的合理化程度」。通俗理解就是:一張數據表的表結構所符合的某種設計標準的級別


目前關係數據庫有六種範式:第一範式(1NF)、第二範式(2NF)、第三範式(3NF)、巴斯-科德範式(BCNF)、第四範式(4NF)和第五範式(5NF,又稱完美範式)。

知足最低要求的範式是第一範式(1NF)。在第一範式的基礎上進一步知足更多規範要求的稱爲第二範式(2NF),其他範式以次類推。通常說來,數據庫只需知足到第三範式 (3NF)就好了。

(3)前三個範式

一、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 表。

(4)範式的利弊

由於範式的主要目的是爲了消除冗餘(範式級別越高,冗餘越小),因此:

好處:

  • 下降存儲成本(數據庫範式是在20世紀提出的,當時的磁盤存儲成本還很高。)

  • 提升拓展性

壞處:

  • 下降性能。沒有任何冗餘的表設計會產生更多的查詢行爲。

  • 增長設計表結構的難度

(5)反範式設計

既然範式是爲了消除冗餘,那麼反範式就是經過增長冗餘、聚合的手段來提高性能。尤爲對如今的互聯網應用來講,性能比存儲成本的要求更高。

參考最近又流行的 noSQL 數據庫,就是大大的冗餘。


建議:仍是根據自身的業務特色在範式和反範式中找到平衡點

12、進階 - 性能優化


一、儘可能避免排序

與編程(面向過程的)語言不一樣,在 SQL 語言中,用戶不能顯式地命令數據庫進行排序操做。

因此,能觸發排序的表明性的運算有下面這些:

  • GROUP BY 子句

  • ORDER BY 子句

  • 聚合函數(SUM、COUNT、AVG、MAX、MIN)

  • DISTINCT

  • 集合運算符(UNION、INTERSECT、EXCEPT)沒加 ALL

  • 窗口函數(RANK、ROW_NUMBER 等)

爲了性能,請儘可能避免觸發排序,若是不能,也儘可能針對索引字段的排序。

二、沒有用到索引的幾種狀況

(1)在索引字段上進行運算

使用索引時,條件表達式的左側應該是原始字段。

錯誤:WHERE col_1 * 1.1 > 100;
正確:WHERE col_1 > 100 / 1.1
(2)使用 IS NULL / IS NOT NULL 謂詞

由於 NULL 並非值 ,因此索引字段並不會索引 NULL。

此處存疑,可見這篇辯駁:http://www.javashuo.com/article/p-pfhcvviq-ck.html

(3)使用否認形式

例如:

  • <>

  • NOT IN

  • NOT EXISTS

(4)使用聯合索引時,列的順序錯誤

假設存在這樣順序的一個聯合索引:「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;
(5)使用 OR

在 col_1 和 col_2 上分別創建了不一樣的索引,或者創建了(col_1, col_2)這樣的聯合索引時,若是使用 OR 鏈接條件,那麼要麼用不到索引,要麼用到了可是效率比 AND 要差不少

WHERE col_1 > 100 OR col_2 = 'abc';

若是不管如何都要使用OR,那麼有一種辦法是位圖索引。可是這種索引的話更新數據時的性能開銷會增大,因此使用以前須要權衡一下利弊。

(6)使用 LIKE 謂詞進行後方一致或中間一致的匹配

使用 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%';

十3、進階 - SQL 與 集合論


SQL 其中一個數學基礎就是構建在集合論上。

咱們經過畫維恩圖,能夠很大程度地加深對 SQL 的理解。

一、全稱量化和存在量化

(1)判斷集合之間的包含關係

SQL 並無提供任何用於檢查集合的包含關係或者相等性的謂詞。IN 謂詞只能用來檢查元素是否屬於某個集合(∈),而不能檢查集合是不是某個集合的子集(∪)。

聽說,IBM 過去研製的第一個關係數據庫實驗系統——System R 曾經實現了用 CONTAINS 這一謂詞來檢查集合間的包含關係,可是後來由於性能緣由被刪除掉了,直到如今也沒有恢復。

而判斷集合之間的包含關係,就是下面要提到的:全稱量化。

(2)全稱量詞和存在量詞

「全部的 x 都知足條件P」或者「存在(至少一個)知足條件 P 的 x」。

前者稱爲「全稱量詞」,後者稱爲「存在量詞」,分別記做 ∀ 、∃。

其實,全稱量詞的符號實際上是將字母 A 上下顛倒而造成的,存在量詞則是將字母 E 左右顛倒而造成的。
「對於全部的x,……」的英語是「for All x,…」,而「存在知足……的x」的英語是「there Exists x that…」,這就是這兩個符號的由來。

但惋惜,SQL 只支持 EXISTS(存在量詞),不支持 FORALL(全稱量詞)。

但全稱量詞和存在量詞只要定義了一個,另外一個就能夠被推導出來。

(3)全稱量化 ⇔ 存在量化

經過德·摩根定律,來進行 「確定⇔雙重否認」 之間的轉換。

即在 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 列的最大值和最小值與指定常量等距

十4、其餘知識點


一、生成連續編號

先建一張 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;

有了這個連續編號,咱們能夠接着幹不少事,具體可參考原書。

二、想盡一切辦法隱藏地址

精巧的數據結構搭配笨拙的代碼,遠遠好過笨拙的數據結構搭配精巧的代碼。——大教堂與集市

(1)你們一塊兒擺脫地址

即便放眼 SQL 以外的其餘編程語言,各個編程語言的歷史中也都一直存在着「如何對程序員隱藏地址」的課題。與 C 語言以及彙編語言相比,Pascal、Java、Perl 等新一代的語言都在努力地對用戶隱藏指針。在這一點上,關係數據庫與 SQL 的發展軌跡是一致的。

如 C 的指針,在 Java / Python 中被引用代替。

引用相似指針,只是不能進行指針運算,好比不能用 p + 1 指向下一個元素。

可能會有人列舉出用戶能夠使用的指針,好比 Oracle 中的 rowid 或 PostgreSQL 中的 oid 來反對。確實,用戶能夠使用這些指針,可是它們都是個別數據庫廠商違反 SQL 標準而進行的擴展,而標準 SQL一直在努力擺脫指針。

(2)爲何要隱藏地址

例如,SQL 和數據庫都在極力提高數據在表現層的抽象度,以及對用戶隱藏物理層的概念。

所以放棄地址的深入意義是,經過放棄掉系統中沒有意義的東西,創造出一個易於人類理解的有意義的世界。

(3)隱藏地址要怎麼作

《程序設計能從馮·諾依曼風格中解放出來嗎?程序的函數風格及其代數》中提到,只要使用變量,就沒法逃出地址的魔咒。反過來講,之因此SQL 能成爲不依賴於地址的自由的語言,也是由於它不使用變量。

其實 SQL 仍是有變量的,也能夠理解成它是個別數據庫廠商違反 SQL 標準而進行的擴展吧。

與 SQL 同樣不使用變量的語言還有 Lisp。它是一種年齡僅次於 Fortran 的高級語言,已經能夠稱得上是編程語言中的「老將」。

三、SQL 是一種什麼語言

SQL 很大程度上是一種聲明式編程,可是其也含有過程式編程的元素。

例如,關聯子查詢是爲了使 SQL 可以實現相似面向過程語言中循環的功能而引入的。

SQL 在設計之初,就有意地避免了循環。因此 SQL 中沒有專門的循環語句。

雖然能夠使用遊標實現循環,可是這樣的話仍是面向過程的作法。

因此,咱們用好 SQL,就要有從面向過程思惟向聲明式思惟、面向集合思惟轉變的意識。

四、[拓展] 命令式編程 vs 聲明式編程

區別:

  • 命令式編程(Imperative)(也叫:過程式編程):詳細的去命令機器怎麼(How)去處理一件事情以達到你想要的結果(What)
  • 聲明式編程(Declarative):只告訴你想要的結果(What),機器本身摸索過程(How)

例如:

  • c / c++,JAVA,JavaScript 等都是命令式編程語言。
  • SQL、正則表達式,或者邏輯語言(如 Prolog)等都是聲明式語言。

個人觀點:

一、全部的 命令式編程 本質上也是 聲明式編程。底層仍是會有交給機器本身處理的(How)步驟。

二、在命令式編程 也能夠」創造」出 聲明式編程,如 JavaScript 中使用 lodash 庫,等於(How)步驟交給 lodash 去處理。

相關文章
相關標籤/搜索