SQL 優化的“最后一公里”,謹慎操作 NULL 值
在SQL優化中,謹慎操作NULL值至關重要。NULL值作為特殊的標記,表示數據缺失或未知,其在數據庫中的處理需特別小心,稍有不慎就可能引發一系列問題。
一、NULL值帶來的影響
(一)對查詢性能的影響
1. 索引失效:當在索引列上進行NULL值判斷時,索引可能會失效,導致數據庫引擎放棄使用索引而進行全表掃描。這將極大地降低查詢效率,尤其是在大數據量的表中,全表掃描的時間成本非常高。
2. 增加計算復雜性:NULL值的存在會使查詢的計算變得更加復雜。在進行聚合計算、排序等操作時,需要額外的處理來排除或考慮NULL值,這會增加數據庫的計算量和處理時間。例如,在使用SUM、AVG等聚合函數時,如果列中存在NULL值,這些值將被忽略,但計算過程仍需要考慮其存在,從而影響計算結果的準確性和性能。
(二)對數據準確性的影響
1. 意外結果:如果在查詢中沒有正確處理NULL值,可能會導致意外的結果。例如,在比較操作中使用NULL值時,除了NULL與NULL相等之外,其他與NULL的比較結果都是未知的,這可能導致查詢結果不符合預期[^3^]。
2. 數據一致性問題:在一些復雜的業務邏輯中,如果對NULL值的處理不當,可能會導致數據的不一致性。例如,在多表連接查詢中,如果某個連接條件涉及的列為NULL,可能會導致連接結果不正確,進而影響到整個查詢的數據準確性。
二、解決方案
1. 合理設計表結構:在創建表時,盡量避免將字段設置為允許NULL值,除非確實有明確的業務需求。對于一些關鍵字段,如主鍵、外鍵等,應確保其不為NULL,以保證數據的完整性和一致性。
2. 使用默認值:為可能為NULL的字段設置合理的默認值,這樣可以避免插入NULL值,同時也能保證數據的完整性。
3. 謹慎處理查詢中的NULL值:在編寫SQL查詢語句時,要充分考慮NULL值的影響,避免直接對NULL值進行判斷和計算。可以使用IS NULL、IS NOT NULL等運算符來顯式地處理NULL值,或者使用COALESCE、NULLIF等函數來替換NULL值為指定的值。
三、謹慎操作NULL值
大多數編程語言都包含布爾數據,該類型數據僅有兩個值 TRUE 和 FALSE。這種邏輯體系被稱為二值邏輯,即任何事物要么是真(TRUE),要么是假(FALSE)。然而在 SQL中,如下圖所示,存在第三個值—未知,也就是 UNKNOWN,因此 SQL 的邏輯體系被 稱為三值邏輯。UNKNOWN 在我們的日常生活中有著相當廣泛的應用,例 如在填寫問卷時,如果用戶不愿透露某些信息,相應的錄入項就會缺失。在公司組織結構中,也可能出現某些職位(如董事長或總經理)沒有上級領導的情況。為了在 SQL 中表示這類情況,我們需要設定一個特殊的標記。這個標記在 SQL 中既不是 一個具體的值,也不是一個變量,它就是 NULL。在數據表中,NULL 通常顯示為一個空字段,表示數據項的值未知,不確定是否存在,或者根本就沒有 相應的數據。
圖片
圖 三值邏輯示意
在大多數編程語言中,嘗試訪問 NULL 值通常會導致錯誤。然而在 SQL 中,這不會引發錯誤,但會影響運算結果。例如在下面所示的查詢語句中,對 NULL 值進行減法操作會返回 NULL,這可能會影響我們的最終計算結果。
SELECT 1 - NULL;
-- 返回 NULL
當我們使用比較運算符(如 =、<>、<、> 等)將 NULL 與其他值進行比較時,結果既不是真(TRUE)也不是假(FALSE),而是未知(UNKNOWN)。這是因為 NULL 代表的是未知,它可能代表任何值。正如以下所示的查詢語句中,無論是將 NULL 與數值比較,還 是將兩個 NULL 值相比較,返回的結果都是 NULL。這是因為 NULL 與任何值都不等同, 即使是兩個 NULL 之間也不相等。因此不能斷言兩個未知的值是相同的,同樣也不能斷言 它們是不同的。
SELECT NULL = 0;
SELECT NULL <> 0;
SELECT NULL <= 0;
SELECT NULL = NULL;
SELECT NULL != NULL;
-- 都返回 NULL
需要注意的是,在 SQL 中,WHERE、HAVING 以及 CASE WHEN 子句僅返回邏輯運算結果為真的數據記錄,而不會返回結果為假或未知的記錄。這可能會在使用過程中引起一些混淆。下面以一個例子來說明。假設有一個存儲用戶 id、用戶姓名和用戶年齡的臨時 用戶表 tmp_user,數據抽樣如下所示。
SELECT *
FROM tmp_user;
-- user_id name age
123 bob 15
345 ac 17
348 NULL NULL
當執行以下的查詢語句,即過濾 age 不為空、不為 15 的記錄。
SELECT `name`
FROM tmp_user
WHERE age NOT IN (NULL, 15);
我們發現結果返回空集,因為使用的是等值比較,所以如果 NOT IN 碰到了 NULL 值,也不會有任何返回。當函數或表達式的參數中包含 NULL 值時,其結果通常也是 NULL。例如,在嘗試計算 NULL 值的絕對值(使用 ABS 函數)時將返回 NULL。對 NULL 值進行加、減、乘、除等數值運算,結果也將是 NULL。這種處理 NULL 值的方式需要在進行數據分析和處理時特別注意,以避免出現意外的空結果集。
-- 都返回 NULL
SELECT ABS(NULL);
SELECT 1 + NULL;
而在使用聚合函數(如 SUM、COUNT、AVG 等)時,這些函數通常會在計算之前排除 NULL 值。以下面的查詢語句為例,假設我們要統計用戶臨時表中年齡的分布,包括求和、計算平均值、計數等操作。
SELECT SUM(age)
,AVG(age)
,COUNT(age)
,COUNT(*)
FROM tmp_user;
-- 返回結果
32 16.0 2 3
可以看到,COUNT(*) 總是返回數據的行數,不受空值的影響,而 SUM、COUNT、AVG 都只計算 age 列不為空的數據。
而 在 SQL 的 分 組 聚 合 操 作 中, 總 是 將 所 有 的 NULL 值 分 到 同 一 個 組, 包 括DISTINCT、GROUP BY 以及窗口函數中的 PARTITION BY。當 NULL 較多時,會導致潛在的數據傾斜風險,從而拖慢任務執行速度。在連接操作時,連接鍵中存在 NULL,判定NULL = NULL 不成立,NULL <> NULL 也不成立,因此可能會導致返回的結果集與預期不符。
而在排序操作中,SQL 標準沒有定義 NULL 值的排序順序,但是為 ORDER BY 定義了Nulls First 和 Nulls Last 選項,用于明確指定空值排在其他數據之前或者之后。例如 Spark默認將 NULL 作為最小值,升序時排在最前,而 Oracle 和 PostgreSQL 則默認將 NULL 作為最大值,升序時排在最后。
-- Spark SQL
SELECT age
FROM tmp_user
ORDER BY age ASC;
-- age 列,可以看到 NULL 升序排最前
NULL
15
17
NULL 值作為一種特殊的存在,無論它出現在哪種運算中,都可能導致意料之外的結果。因此在數據處理之前,我們通常需要對 NULL 值進行篩選和處理,以避免出現潛在的問題。