神奇的 SQL 之謂詞 → 難理解的 EXISTS

前言

  開心一刻html

我要飛的更高,飛的更高,啊!ide

謂詞

  SQL 中的謂詞指的是:返回值是邏輯值的函數。咱們知道函數的返回值有多是數字、字符串或者日期等等,但謂詞的返回值所有是邏輯值(TRUE/FALSE/UNKNOW),謂詞是一種特殊的函數。關於邏輯值,能夠查看:神奇的 SQL 之溫柔的陷阱 → 三值邏輯 與 NULL !函數

  SQL 中的謂詞有不少,如 =、>、<、<> 等,咱們來看看 SQL 具體有哪些經常使用的謂詞spa

  比較謂詞

    建立表與初始化數據code

-- 一、表建立並初始化數據
DROP TABLE IF EXISTS tbl_student;
CREATE TABLE tbl_student (
  id INT(8) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  sno VARCHAR(12) NOT NULL COMMENT '學號',
    name VARCHAR(5) NOT NULL COMMENT '姓名',
    age TINYINT(3) NOT NULL COMMENT '年齡',
  sex TINYINT(1) NOT NULL COMMENT '性別,1:男,2:女',
  PRIMARY KEY (id)
);
INSERT INTO tbl_student(sno,name,age,sex) VALUES
('20190607001','李小龍',21,1),
('20190607002','王祖賢',16,2),
('20190608003','林青霞',17,2),
('20190608004','李嘉欣',15,2),
('20190609005','周潤發',20,1),
('20190609006','張國榮',18,1);

DROP TABLE IF EXISTS tbl_student_class;
CREATE TABLE tbl_student_class (
  id int(8) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  sno varchar(12) NOT NULL COMMENT '學號',
  cno varchar(5) NOT NULL COMMENT '班級號',
  cname varchar(20) NOT NULL COMMENT '班級名',
  PRIMARY KEY (`id`)
) COMMENT='學生班級表';
INSERT INTO tbl_student_class VALUES 
('1', '20190607001', '0607', '影視7班'),
('2', '20190607002', '0607', '影視7班'),
('3', '20190608003', '0608', '影視8班'),
('4', '20190608004', '0608', '影視8班'),
('5', '20190609005', '0609', '影視9班'),
('6', '20190609006', '0609', '影視9班');

SELECT * FROM tbl_student;
SELECT * FROM tbl_student_class;

    相信你們對 =、>、<、<>(!=)等比較運算符都很是熟悉,它們的正式名稱就是比較謂詞,使用示例以下htm

-- 比較謂詞示例
SELECT * FROM tbl_student WHERE name = '王祖賢';
SELECT * FROM tbl_student WHERE age > 18;
SELECT * FROM tbl_student WHERE age < 18;
SELECT * FROM tbl_student WHERE age <> 18;
SELECT * FROM tbl_student WHERE age <= 18;

  LIKE

    當咱們想用 SQL 作一些簡單的模糊查詢時,都會用到 LIKE 謂詞,分爲 前一致、中一致和後一致,使用示例以下對象

-- LIKE謂詞
SELECT * FROM tbl_student WHERE name LIKE '李%';         -- 前一致
SELECT * FROM tbl_student WHERE name LIKE '%青%';        -- 中一致
SELECT * FROM tbl_student WHERE name LIKE '青%';        -- 後一致

    若是name字段上建了索引,那麼前一致會利用索引;而中一致、後一致會走全表掃描。blog

  BETWEEN

    當咱們想進行範圍查詢時,每每會用到 BETWEEN 謂詞,示例以下教程

-- BETWEEN謂詞
SELECT * FROM tbl_student WHERE age BETWEEN 15 AND 22;
SELECT * FROM tbl_student WHERE age NOT BETWEEN 15 AND 22;

    BETWEEN  和它以後的第一個 AND 組成一個範圍條件;BETWEEN 會包含臨界值 15 和 22索引

SELECT * FROM tbl_student WHERE age BETWEEN 15 AND 22;
-- 等價於
SELECT * FROM tbl_student WHERE age >= 15 AND age <= 22;

    若不想包含臨界值,那就須要這麼寫了

SELECT * FROM tbl_student WHERE age > 15 AND age < 22;

  IS NULL 和 IS NOT NULL

    NULL 的水很深,具體可看:神奇的 SQL 之溫柔的陷阱 → 三值邏輯 與 NULL !

  IN

    有這樣一個需求:查詢出年齡等於 1五、18以及20的學生,咱們會用 OR 來查

-- OR
SELECT * FROM tbl_student WHERE age = 15 OR age = 18 OR age = 20;

    用 OR 來查沒問題,可是有一點不足,若是選取的對象愈來愈多,SQL會變得愈來愈長,閱讀性會愈來愈差。因此咱們能夠用 IN 來代替

-- IN
SELECT * FROM tbl_student WHERE age IN(15,18,20);

    IN 有一種其餘謂詞沒有的使用方法:使用子查詢做爲其參數,這個在平時項目中也是用的很是多的,例如:查詢出影視7班的學生信息

-- IN實現,但不推薦
SELECT * FROM tbl_student 
WHERE sno IN (
    SELECT sno FROM tbl_student_class 
    WHERE cname = '影視7班'
); 

-- 聯表查,推薦
SELECT ts.* FROM
tbl_student_class tsc LEFT JOIN tbl_student ts ON tsc.sno = ts.sno
WHERE tsc.cname = '影視7班';

    不少狀況下,IN 是能夠用聯表查詢來替換的

EXISTS

  EXISTS也是 SQL 謂詞,但平時用的很少,不是說適用場景少,而是它很差駕馭,咱們用很差它。它用法與其餘謂詞不同,並且很差理解,另外不少狀況下咱們都用 IN 來替代它了。

  理論篇

    在真正講解 EXSITS 示例以前,咱們先來了解下理論知識:實體的階層 、全稱量化與存在量化

    實體的階層

      SQL 嚴格區分階層,不能跨階層操做。就用咱們經常使用的謂詞來舉例,一樣是謂詞,可是與 = 、BETWEEN 等相比,EXISTS 的用法仍是大不相同的。歸納來講,區別在於「謂詞的參數能夠取什麼值」;「x = y」或 「x BETWEEN y 」 等謂詞能夠取的參數是像 「21」 或者 「李小龍」 這樣的單一值,咱們稱之爲標量值,而 EXISTS 能夠取的參數到底是什麼呢?從下面這條 SQL 語句來看,EXISTS 的參數不像是單一值

SELECT * FROM tbl_student ts
WHERE EXISTS (
    SELECT * FROM tbl_student_class tsc
    WHERE ts.sno = tsc.sno
);

      咱們能夠看出 EXISTS 的參數是行數據的集合。之因此這麼說,是由於不管子查詢中選擇什麼樣的列,對於 EXISTS 來講都是同樣的。在 EXISTS 的子查詢裏, SELECT 子句的列表能夠有下面這三種寫法。

1. 通配符:SELECT *
2. 常量:SELECT '1'
3. 列名:SELECT tsc.id

      也就是說以下 3 條 SQL 查到的結果是同樣的

-- SELECT *
SELECT * FROM tbl_student ts
WHERE EXISTS (
    SELECT * FROM tbl_student_class tsc
    WHERE ts.sno = tsc.sno
);
-- SELECT 常量
SELECT * FROM tbl_student ts
WHERE EXISTS (
    SELECT 1 FROM tbl_student_class tsc
    WHERE ts.sno = tsc.sno
);
-- SELECT 列名
SELECT * FROM tbl_student ts
WHERE EXISTS (
    SELECT tsc.sno FROM tbl_student_class tsc
    WHERE ts.sno = tsc.sno
);
View Code

      用個圖來歸納下通常的謂詞與 EXISTS 的區別

 

      從上圖咱們知道,EXISTS 的特殊性在於輸入值的階數(輸出值和其餘謂詞同樣,都是邏輯值)。謂詞邏輯中,根據輸入值的階數對謂詞進行分類。= 或者 BETWEEEN 等輸入值爲一行的謂詞叫做「一階謂詞」,而像 EXISTS 這樣輸入值爲行的集合的謂詞叫做 「二階謂詞」。關於 「階」 ,有興趣的能夠區看個人另外一篇博客:神奇的 SQL 之層級 → 爲何 GROUP BY 以後不能直接引用原表中的列

    全稱量化和存在量化

      謂詞邏輯中有量詞(限量詞、數量詞)這類特殊的謂詞。咱們能夠用它們來表達一些這樣的命題:「全部的 x 都知足條件 P」 或者 「存在(至少一個)知足條件 P 的 x 」,前者稱爲「全稱量詞」,後者稱爲「存在量詞」,分別記做 ∀(A的下倒)、∃(E的左倒)。

      SQL 中的 EXISTS 謂詞實現了謂詞邏輯中的存在量詞,然而遺憾的是, SQL 卻並無實現全稱量詞。可是沒有全稱量詞並不算是 SQL 的致命缺陷,由於全稱量詞和存在量詞只要定義了一個,另外一個就能夠被推導出來。具體能夠參考下面這個等價改寫的規則(德·摩根定律)。

∀ x P x = ¬ ∃ x ¬P(全部的 x 都知足條件 P =不存在不知足條件 P 的 x )
∃ x P x = ¬ ∀ x ¬Px(存在 x 知足條件 P =並不是全部的 x 都不知足條件 P)

      所以在 SQL 中,爲了表達全稱量化,須要將"全部的行都知足條件P" 這樣的命題轉換成 "不存在不知足條件 P 的行"

  實踐篇

    上面的理論篇,你們看了之後可能仍是有點暈,咱們結合具體的實際案例來看看 EXISTS 的妙用

    查詢表中「不」存在的數據

      上面的 tbl_student中的學生都分配到了具體的班級,假設新來了兩個學生(劉德華、張家輝),他們暫時還未被分配到班級,咱們如何將他們查詢出來(查詢未被分配到班級的學生信息)。

-- 新來、未被分配到班級的學生
INSERT INTO tbl_student(sno,name,age,sex) VALUES
('20190610010','劉德華',55,1),
('20190610011','張家輝',46,1);

      咱們最容易想到的 SQL 確定是下面這條

-- NOT IN 實現
SELECT * FROM tbl_student WHERE sno NOT IN(SELECT sno FROM tbl_student_class);

      其實用 NOT EXISTS 也是能夠實現的

-- NOT EXISTS 實現
SELECT * FROM tbl_student ts
WHERE NOT EXISTS (
    SELECT * FROM tbl_student_class tsc WHERE ts.sno = tsc.sno
);

    全稱量化 :習慣 「確定 ⇔ 雙重否認」 之間的轉換

      EXISTS 謂詞來表達全稱量化,這是EXISTS 的用法中很具備表明性的一個用法。可是須要咱們打破常規思惟,習慣從全稱量化 「全部的行都××」 到其雙重否認 「不××的行一行都不存在」 的轉換。

      假設咱們有學生成績表:tbl_student_score

-- 學生成績表
DROP TABLE IF EXISTS tbl_student_score;
CREATE TABLE tbl_student_score (
  id INT(8) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  sno VARCHAR(12) NOT NULL COMMENT '學號',
    subject VARCHAR(5) NOT NULL COMMENT '課程',
    score TINYINT(3) NOT NULL COMMENT '分數',
  PRIMARY KEY (id)
);
INSERT INTO tbl_student_score(sno,subject,score) VALUES
('20190607001','數學',100),
('20190607001','語文',80),
('20190607001','物理',80),
('20190608003','數學',80),
('20190608003','語文',95),
('20190609006','數學',40),
('20190609006','語文',90),
('20190610011','數學',80);

SELECT * FROM tbl_student_score;

      一、查詢出「全部科目分數都在 50 分以上的學生」

        2019060700一、2019060800三、20190610011 這三個學生知足條件,咱們須要將這 3 個學生查出來,這個 SQL 該如何寫? 咱們須要轉換下命題,將查詢條件「全部科目分數都在 50 分以上」 轉換成它的雙重否認 「沒有一個科目分數不滿 50 分」,而後用 NOT EXISTS 來表示轉換後的命題

-- 沒有一個科目分數不滿 50 分
SELECT DISTINCT sno
FROM tbl_student_score tss1
WHERE NOT EXISTS -- 不存在知足如下條件的行
(    SELECT * FROM tbl_student_score tss2
    WHERE tss2.sno = tss1.sno
    AND tss2.score < 50    -- 分數不滿50 分的科目
);

      二、查詢出「數學分數在 80 分以上(包含80)且語文分數在 50 分以上(包含)的學生」

        結果應該是學號分別爲 2019060700一、20190608003 的學生。像這樣的需求,咱們在實際業務中應該會常常遇到,可是乍一看可能會以爲不太像是全稱量化的條件。若是改爲下面這樣的說法,可能咱們一會兒就能明白它是全稱量化的命題了。

"某個學生的全部行數據中,若是科目是數學,則分數在 80 分以上;若是科目是語文,則分數在 50 分以上。"

        咱們再轉換成它雙重否認:某個學生的全部行數據中,若是科目是數學,則分數不低於 80;若是科目是語文,則分數不低於 50 ;咱們能夠按照以下順序寫出咱們想要的 SQL

-- 一、CASE 表達式,確定
CASE WHEN subject = '數學' AND score >= 80 THEN 1
        WHEN subject = '語文' AND score >= 50 THEN 1
        ELSE 0 
END;

-- 二、CASE 表達式,單重否認(加上 NOT EXISTS纔算雙重)
CASE WHEN subject = '數學' AND score < 80 THEN 1
        WHEN subject = '語文' AND score < 50 THEN 1
    ELSE 0 
END;

-- 三、結果包含了 20190610011 的 SQL 
SELECT DISTINCT sno
FROM tbl_student_score tss1
WHERE subject IN ('數學', '語文')
AND NOT EXISTS
(
    SELECT *FROM tbl_student_score tss2
    WHERE tss2.sno = tss1.sno
    AND 1 = CASE WHEN subject = '數學' AND score < 80 THEN 1
                        WHEN subject = '語文' AND score < 50 THEN 1
                        ELSE 0 
                    END
);

-- 四、20190610011 沒有語文成績,剔除掉
SELECT sno
FROM tbl_student_score tss1
WHERE subject IN ('數學', '語文')
AND NOT EXISTS
(
    SELECT * FROM tbl_student_score tss2
    WHERE tss2.sno = tss1.sno
    AND 1 = CASE WHEN subject = '數學' AND score < 80 THEN 1
                        WHEN subject = '語文' AND score < 50 THEN 1
                        ELSE 0 
                        END
)
GROUP BY sno
HAVING COUNT(*) = 2; -- 必須兩門科目都有分數

    關於 EXISTS 的案例有不少,這裏就再也不舉例了,有興趣的小夥伴能夠看看:SQL 中的 EXISTS 到底作了什麼?

    若是你們想掌握 EXISTS,但願你們多看看 EXISTS 的案例,看多了你就會發現其中的通性:哪些場景適合用 EXISTS。

總結

  一、SQL 中的謂詞分兩種:一階謂詞和二階謂詞(EXISTS),區別主要在於接收的參數不一樣,一階謂詞接收的是 行,而二階謂詞接收的是 行的集合;

  二、SQL 中沒有與全稱量詞至關的謂詞,可使用 NOT EXISTS 代替;

  三、EXISTS 之因此難用(不是很差用,而是不會用),主要是全稱量詞的命題轉換(確定 ⇔ 雙重否認)比較難(樓主也懵!)。實際工做中每每會捨棄 EXISTS,尋找它的替代方式,多是 SQL 的替代,也多是業務方面的轉換,因此說,EXISTS 掌握不了不要緊,固然,能掌握那是最好了;

參考

  《SQL基礎教程》

  《SQL進階教程》

相關文章
相關標籤/搜索