神奇的 SQL 之溫柔的陷阱 → 三值邏輯 與 NULL !

来源:https://www.cnblogs.com/youzhibing/archive/2019/08/13/11337745.html
-Advertisement-
Play Games

前言 開心一刻 一個中國小孩參加國外的脫口秀節目,因為語言不通,於是找了一個翻譯。 主持人問:“Who is your favorite singer ?” 翻譯:”你最喜歡哪個歌手啊 ?” 小孩興奮地回答:”Michael Jackson” 翻譯轉身對主持人說:”邁克爾-傑克遜” 主持人看著翻譯: ...


前言

  開心一刻 

     一個中國小孩參加國外的脫口秀節目,因為語言不通,於是找了一個翻譯。
    主持人問:“Who is your favorite singer ?”
    翻譯:”你最喜歡哪個歌手啊 ?”
    小孩興奮地回答:”Michael Jackson”
    翻譯轉身對主持人說:”邁克爾-傑克遜”
    主持人看著翻譯:"你說什麼 ?"     電視機前的觀眾:"我怎麼有點蒙?" 

NULL

  NULL 用於表示缺失的值或遺漏的未知數據,不是某種具體類型的值。數據表中的 NULL 值表示該值所處的欄位為空,值為 NULL 的欄位沒有值,尤其要明白的是:NULL 值與 0 或者空字元串是不同的。

  兩種 NULL

    這種說法大家可能會覺得很奇怪,因為 SQL 里只存在一種 NULL 。然而在討論 NULL 時,我們一般都會將它分成兩種類型來思考:“未知”(unknown)和“不適用”(not applicable,inapplicable)。

    以“不知道戴墨鏡的人眼睛是什麼顏色”這種情況為例,這個人的眼睛肯定是有顏色的,但是如果他不摘掉眼鏡,別人就不知道他的眼睛是什麼顏色。這就叫作未知。而“不知道冰箱的眼睛是什麼顏色”則屬於“不適用”。因為冰箱根本就沒有眼睛,所以“眼睛的顏色”這一屬性並不適用於冰箱。“冰箱的眼睛的顏色”這種說法和“圓的體積”“男性的分娩次數”一樣,都是沒有意義的。平時,我們習慣了說“不知道”,但是“不知道”也分很多種。“不適用”這種情況下的 NULL ,在語義上更接近於“無意義”,而不是“不確定”。這裡總結一下:“未知”指的是“雖然現在不知道,但加上某些條件後就可以知道”;而“不適用”指的是“無論怎麼努力都無法知道”。

    關係模型的發明者 E.F. Codd 最先給出了這種分類。下圖是他對“丟失的信息”的分類

  為什麼必須寫成“IS NULL”,而不是“= NULL”

    我相信不少人有這樣的困惑吧,尤其是相信剛學 SQL 的小伙伴。我們來看個具體的案例,假設我們有如下表以及數據

DROP TABLE IF EXISTS t_sample_null;
CREATE TABLE t_sample_null (
    id INT(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
    name VARCHAR(50) NOT NULL COMMENT '名稱',
    remark VARCHAR(500) COMMENT '備註',
    primary key(id)
) COMMENT 'NULL樣例';

INSERT INTO t_sample_null(name, remark)
VALUES('zhangsan', '張三'),('李四', NULL);

    我們要查詢備註為 NULL 的記錄(為 NULL 這種叫法本身是不對的,只是我們日常中已經叫習慣了,具體往下看),怎麼查,很多新手會寫出這樣的 SQL

-- SQL 不報錯,但查不出結果
SELECT * FROM t_sample_null WHERE remark = NULL;

    執行時不報錯,但是查不出我們想要的結果, 這是為什麼了 ? 這個問題我們先放著,我們往下看

三值邏輯

  這個三值邏輯不是三目運算,指的是三個邏輯值,有人可能有疑問了,邏輯值不是只有真(true)和假(false)嗎,哪來的第三個? 說這話時我們需要註意所處的環境,在主流的編程語言中(C、JAVA、Python、JS等)中,邏輯值確實只有 2 個,但在 SQL 中卻存在第三個邏輯值:unknown。這有點類似於我們平時所說的:對、錯、不知道。

  邏輯值 unknown 和作為 NULL 的一種的 UNKNOWN (未知)是不同的東西。前者是明確的布爾型的邏輯值,後者既不是值也不是變數。為了便於區分,前者採用小寫字母 unknown ,後者用大寫字母 UNKNOWN 來表示。為了讓大家理解兩者的不同,我們來看一個 x=x 這樣的簡單等式。x 是邏輯值 unknown 時,x=x 被判斷為 true ,而 x 是 UNKNOWN 時被判斷為 unknown 

-- 這個是明確的邏輯值的比較
unknown = unknown → true

-- 這個相當於NULL = NULL
UNKNOWN = UNKNOWN → unknown

   三值邏輯的邏輯值表

    NOT

    AND

    OR

    圖中藍色部分是三值邏輯中獨有的運算,這在二值邏輯中是沒有的。其餘的 SQL 謂詞全部都能由這三個邏輯運算組合而來。從這個意義上講,這個幾個邏輯表可以說是 SQL 的母體(matrix)。
    NOT 的話,因為邏輯值表比較簡單,所以很好記;但是對於 AND 和 OR,因為組合出來的邏輯值較多,所以全部記住非常困難。為了便於記憶,請註意這三個邏輯值之間有下麵這樣的優先順序順序。
      AND 的情況: false > unknown > true
      OR 的情況: true > unknown > false

    優先順序高的邏輯值會決定計算結果。例如 true AND unknown ,因為 unknown 的優先順序更高,所以結果是 unknown 。而 true OR unknown 的話,因為 true 優先順序更高,所以結果是 true 。記住這個順序後就能更方便地進行三值邏輯運算了。特別需要記住的是,當 AND 運算中包含 unknown 時,結果肯定不會是 true (反之,如果AND 運算結果為 true ,則參與運算的雙方必須都為 true )。

-- 假設 a = 2, b = 5, c = NULL,下列表達式的邏輯值如下

a < b AND b > c  → unknown
a > b OR b < c   → unknown
a < b OR b < c   → true
NOT (b <> c)     → unknown

  “IS NULL” 而非 “= NULL”

    我們再回到問題:為什麼必須寫成“IS NULL”,而不是“= NULL”

    對 NULL 使用比較謂詞後得到的結果總是 unknown 。而查詢結果只會包含 WHERE 子句里的判斷結果為 true 的行,不會包含判斷結果為 false 和 unknown 的行。不只是等號,對 NULL 使用其他比較謂詞,結果也都是一樣的。所以無論 remark 是不是 NULL ,比較結果都是 unknown ,那麼永遠沒有結果返回。以下的式子都會被判為 unknown

-- 以下的式子都會被判為 unknown
= NULL
> NULL
< NULL
<> NULL
NULL = NULL

    那麼,為什麼對 NULL 使用比較謂詞後得到的結果永遠不可能為真呢?這是因為,NULL 既不是值也不是變數。NULL 只是一個表示“沒有值”的標記,而比較謂詞只適用於值。因此,對並非值的 NULL 使用比較謂詞本來就是沒有意義的。“列的值為 NULL ”、“NULL 值” 這樣的說法本身就是錯誤的。因為 NULL不是值,所以不在定義域(domain)中。相反,如果有人認為 NULL 是值,那麼我們可以倒過來想一下:它是什麼類型的值?關係資料庫中存在的值必然屬於某種類型,比如字元型或數值型等。所以,假如 NULL 是值,那麼它就必須屬於某種類型。

    NULL 容易被認為是值的原因有兩個。第一個是高級編程語言裡面,NULL 被定義為了一個常量(很多語言將其定義為了整數0),這導致了我們的混淆。但是,SQL 里的 NULL 和其他編程語言里的 NULL 是完全不同的東西。第二個原因是,IS NULL 這樣的謂詞是由兩個單詞構成的,所以我們容易把 IS 當作謂詞,而把 NULL 當作值。特別是 SQL 里還有 IS TRUE 、IS FALSE 這樣的謂詞,我們由此類推,從而這樣認為也不是沒有道理。但是正如講解標準 SQL 的書里提醒人們註意的那樣,我們應該把 IS NULL 看作是一個謂詞。因此,寫成 IS_NULL 這樣也許更合適。

溫柔的陷阱

  比較謂詞和 NULL

    排中律不成立

      排中律指同一個思維過程中,兩個相互矛盾的思想不能同假,必有一真,即“要麼A要麼非A”

      假設我們有學生表:t_student

DROP TABLE IF EXISTS t_student;
CREATE TABLE t_student (
    id INT(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
    name VARCHAR(50) NOT NULL COMMENT '名稱',
    age INT(3) COMMENT '年齡',
    remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '備註',
    primary key(id)
) COMMENT '學生信息';

INSERT INTO t_student(name, age)
VALUE('zhangsan', 25),('wangwu', 60),('bruce', 32),('yzb', NULL),('boss', 18);

SELECT * FROM t_student;
View Code

      表中數據 yzb 的 age 是 NULL,也就是說 yzb 的年齡未知。在現實世界里,yzb 是 20 歲,或者不是 20 歲,二者必居其一,這毫無疑問是一個真命題。那麼在 SQL 的世界里了,排中律還適用嗎? 我們來看一個 SQL 

SELECT * FROM t_student
WHERE age = 20 OR age <> 20;

      咋一看,這不就是查詢表中全部記錄嗎? 我們來看下實際結果

      yzb 沒查出來,這是為什麼了?我們來分析下,yzb 的 age 是 NULL,那麼這條記錄的判斷步驟如下

-- 1. 約翰年齡是 NULL (未知的 NULL !)
SELECT *
FROM t_student
WHERE age = NULL
OR age <> NULL;

-- 2. 對 NULL 使用比較謂詞後,結果為unknown
SELECT *
FROM t_student
WHERE unknown
OR unknown;

-- 3.unknown OR unknown 的結果是unknown (參考三值邏輯的邏輯值表)
SELECT *
FROM t_student
WHERE unknown;

      SQL 語句的查詢結果里只有判斷結果為 true 的行。要想讓 yzb 出現在結果里,需要添加下麵這樣的 “第 3 個條件”

-- 添加 3 個條件:年齡是20 歲,或者不是20 歲,或者年齡未知
SELECT * FROM t_student
WHERE age = 20 
    OR age <> 20
    OR age IS NULL;

    CASE 表達式和 NULL

      簡單 CASE 表達式如下

CASE col_1
    WHEN = 1 THEN 'o'
    WHEN NULL THEN 'x'
END

      這個 CASE 表達式一定不會返回 ×。這是因為,第二個 WHEN 子句是 col_1 = NULL 的縮寫形式。正如我們所知,這個式子的邏輯值永遠是 unknown ,而且 CASE 表達式的判斷方法與 WHERE 子句一樣,只認可邏輯值為 true 的條件。正確的寫法是像下麵這樣使用搜索 CASE 表達式

CASE WHEN col_1 = 1 THEN 'o'
    WHEN col_1 IS NULL THEN 'x'
END

  NOT IN 和 NOT EXISTS 不是等價的

    我們在對 SQL 語句進行性能優化時,經常用到的一個技巧是將 IN 改寫成 EXISTS ,這是等價改寫,並沒有什麼問題。但是,將 NOT IN 改寫成 NOT EXISTS 時,結果未必一樣。

    我們來看個例子,我們有如下兩張表:t_student_A 和 t_student_B,分別表示 A 班學生與 B 班學生 

DROP TABLE IF EXISTS t_student_A;
CREATE TABLE t_student_A (
    id INT(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
    name VARCHAR(50) NOT NULL COMMENT '名稱',
    age INT(3) COMMENT '年齡',
    city VARCHAR(50) NOT NULL COMMENT '城市',
    remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '備註',
    primary key(id)
) COMMENT '學生信息';

INSERT INTO t_student_A(name, age, city)
VALUE
('zhangsan', 25,'深圳市'),('wangwu', 60, '廣州市'),
('bruce', 32, '北京市'),('yzb', NULL, '深圳市'),
('boss', 43, '深圳市');

DROP TABLE IF EXISTS t_student_B;
CREATE TABLE t_student_B (
    id INT(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
    name VARCHAR(50) NOT NULL COMMENT '名稱',
    age INT(3) COMMENT '年齡',
    city VARCHAR(50) NOT NULL COMMENT '城市',
    remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '備註',
    primary key(id)
) COMMENT '學生信息';

INSERT INTO t_student_B(name, age, city)
VALUE
('馬化騰', 45, '深圳市'),('馬三', 25, '深圳市'),
('馬雲', 43, '杭州市'),('李彥巨集', 41, '深圳市'),
('年輕人', 25, '深圳市');

SELECT * FROM t_student_A;
SELECT * FROM t_student_B;
View Code

    需求:查詢與 A  班住在深圳的學生年齡不同的 B 班學生,也就說查詢出 :馬化騰 和 李彥巨集,這個 SQL 該如何寫,像這樣?

-- 查詢與 A  班住在深圳的學生年齡不同的 B 班學生 ?
SELECT * FROM t_student_B
WHERE age NOT IN (
    SELECT age FROM t_student_A 
    WHERE city = '深圳市'
);

    我們來看下執行結果

    我們發現結果是空,查詢不到任何數據,這是為什麼了 ?這裡 NULL 又開始作怪了,我們一步一步來看看究竟發生了什麼

-- 1. 執行子查詢,獲取年齡列表
SELECT * FROM t_student
WHERE age NOT IN(43, NULL, 25);

-- 2. 用NOT 和IN 等價改寫NOT IN
SELECT * FROM t_student
WHERE NOT age IN (43, NULL, 25);

-- 3. 用OR 等價改寫謂詞IN
SELECT * FROM t_student
WHERE NOT ( (age = 43) OR (age = NULL) OR (age = 25) );

-- 4. 使用德· 摩根定律等價改寫
SELECT * FROM t_student
WHERE NOT (age = 43) AND NOT(age = NULL) AND NOT (age = 25);

-- 5. 用<> 等價改寫 NOT 和 =
SELECT * FROM t_student
WHERE (age <> 43) AND (age <> NULL) AND (age <> 25);

-- 6. 對NULL 使用<> 後,結果為 unknown
SELECT * FROM t_student
WHERE (age <> 43) AND unknown AND (age <> 25);

-- 7.如果 AND 運算里包含 unknown,則結果不為true(參考三值邏輯的邏輯值表)
SELECT * FROM t_student
WHERE false 或 unknown;
View Code

    可以看出,在進行了一系列的轉換後,沒有一條記錄在 WHERE 子句里被判斷為 true 。也就是說,如果 NOT IN 子查詢中用到的表裡被選擇的列中存在 NULL ,則 SQL 語句整體的查詢結果永遠是空。這是很可怕的現象!

    為了得到正確的結果,我們需要使用 EXISTS 謂詞

-- 正確的SQL 語句:馬化騰和李彥巨集將被查詢到
SELECT * FROM t_student_B B
WHERE NOT EXISTS ( 
    SELECT * FROM t_student_A A
    WHERE B.age = A.age
    AND A.city = '深圳市' 
);

    執行結果如下

    同樣地,我們再來一步一步地看看這段 SQL 是如何處理年齡為 NULL 的行的

-- 1. 在子查詢里和 NULL 進行比較運算,此時 A.age 是 NULL
SELECT * FROM t_student_B B
WHERE NOT EXISTS ( 
    SELECT * FROM t_student_A A
    WHERE B.age = NULL
    AND A.city = '深圳市' 
);

-- 2. 對NULL 使用“=”後,結果為 unknown
SELECT * FROM t_student_B B
WHERE NOT EXISTS ( 
    SELECT * FROM t_student_A A
    WHERE unknown
    AND A.city = '深圳市' 
);

-- 3. 如果AND 運算里包含 unknown,結果不會是true
SELECT * FROM t_student_B B
WHERE NOT EXISTS ( 
    SELECT * FROM t_student_A A
    WHERE false 或 unknown
);

-- 4. 子查詢沒有返回結果,因此相反地,NOT EXISTS 為 true
SELECT * FROM t_student_B B
WHERE true;
View Code

    也就是說,yzb 被作為 “與任何人的年齡都不同的人” 來處理了。EXISTS 只會返回 true 或者false,永遠不會返回 unknown。因此就有了 IN 和 EXISTS 可以互相替換使用,而 NOT IN和 NOT EXISTS 卻不可以互相替換的混亂現象。

  還有一些其他的陷阱,比如:限定謂詞和 NULL、限定謂詞和極值函數不是等價的、聚合函數和 NULL 等等。

總結

  1、NULL 用於表示缺失的值或遺漏的未知數據,不是某種具體類型的值,不能對其使用謂詞

  2、對 NULL 使用謂詞後的結果是 unknown,unknown 參與到邏輯運算時,SQL 的運行會和預想的不一樣

  3、 IS NULL 整個是一個謂詞,而不是:IS 是謂詞,NULL 是值;類似的還有 IS TRUE、IS FALSE

  4、要想解決 NULL 帶來的各種問題,最佳方法應該是往表裡添加 NOT NULL 約束來儘力排除 NULL

    我的項目中有個硬性規定:所有欄位必須是 NOT NULL,建表的時候就加上此約束

參考

  《SQL進階教程》


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文用的是Oracle 10g資料庫,利用PL/SQL Developer的集成開發環境(安裝可以自行百度)Oracle資料庫 > 資料庫實例 > 表空間(邏輯單位)(用戶) > 數據文件(物理單位)可以理解為下麵地球 > 一個國家 > 省份(邏輯單位)(公民) > 山川河流(物理單位)通常情況下, ...
  • 對於所有的需求,當你不知道怎麼處理的時候,你就先用最簡單的方法,或者說的明白一點,用最原始的方法,先實現業務需求再說。 一、對提現隊列數據表“ims_checkout_task”進行彙總統計,按月彙總統計每個月的提現總額,提現總次數。 1、SQL操作如下: 2、資料庫返回如下: 3、關鍵詞:case ...
  • 【發現問題】 【問題分析】 Ⅰ、在前端界面查詢,發現了庫存中存在這樣的數量值。但是在資料庫中查詢時顯示正常。即6.999999999999997 為 7。 Ⅱ、至於這種小數產生,我以為是oracle存儲過程計算的時候也會失真?後來發現我這是由於其他問題造成的。 🌂對於前端和資料庫的查詢結果不一致, ...
  • 二、主從搭建 2.1測試目標 測試postgresql主從搭建安裝過程 2.2環境準備 實例級別的複製 流複製主庫可讀寫,但從庫只允許查詢不允許寫人, 而邏輯複製的從庫可讀寫 流複製實驗環境 主機 主機名 Ip地址 操作系統 Postgresql版本 主節點 pgsql 192.168.231.13 ...
  • ...
  • 1 大數據概述大數據特性:4v volume velocity variety value 即大量化、快速化、多樣化、價值密度低 數據量大:大數據摩爾定律 快速化:從數據的生成到消耗,時間視窗小,可用於生成決策的時間非常少;1秒定律,這和傳統的數據挖掘技術有著本質區別(谷歌的dremel可以在1秒內... ...
  • 前面配置了ACFS用於ogg目錄。 上傳軟體並解壓安裝: unzip 123014_fbo_ggs_Linux_x64_shiphome.zip cd fbo_ggs_Linux_x64_shiphome/cd Disk1/./runInstaller 添加環境變數 su - oracle cat ...
  • [學習筆記] collect: 收集一個彈性分散式數據集的所有元素到一個數組中,這樣便於我們觀察,畢竟分散式數據集比較抽象。Spark的collect方法,是Action類型的一個運算元,會從遠程集群拉取數據到driver端。最後,將大量數據 彙集到一個driver節點上,將數據用數組存放,占用了jv ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...