SQL語句中 NOT IN 子句的“正確打開方式”


在寫SQL語句的時候,若where條件是判斷用戶不在某個集合當中,我們習慣使用 where 列名 not in (集合) 子句,這種寫法本身沒有問題,但實踐過程中卻發現很多人在寫類似的SQL語句時,寫的代碼存在隱患,而這種隱患往往難以發現。

 

1. 存在隱患的寫法

首先,我們來評估一條簡單的SQL語句的輸出結果。語句如下:

select 1 from dual where 1 not in(2, null)

簡單,輸出結果是1嘛。

錯!答案是沒有輸出結果。

為什么會這樣?數據庫管理系統在執行查詢之前,會對上面語句進行簡單的轉化,轉化之后的語句如下:

select 1 from dual where 1 != 2 and 1 != null

顯然,where后面的表達式中,1!=2的結果是true,但1!=null的結果是不確定true不確定進行與運算的結果非true,所以上述語句的沒有輸出結果。

分析完了上面這條語句,我們再來看一個問題,假設有員工表DEMPLOYEES和部門表DEPARTMENTS,寫SQL語句找出2015年沒有招人的部門。我們可以很快的寫出下面語句:

select DEPARTMENT_ID, DEPARTMENT_NAME from DEPARTMENTS where DEPARTMENT_ID not in(
    select DEPARTMENT_ID from EMPLOYEES where HIRE_DATE like '%2015%'
)

若員工表EMPLOYEES中存在還未分配部門的員工時,子查詢中的結果集中會含NULL元素,這就和開始我們評估的語句套上了,語句將無結果返回。那么,該如何寫這種形式的語句才能避免隱患呢?

2. NOT IN 子句正確的打開方式

為了避免NOT IN子句的隱患,最簡單的方式就是不使用NOT IN子句,而使用其他的形式達到相同的效果。你老是出問題,我不跟你玩行了吧!例如使用 not exists子句將上述語句改寫正確之后代碼如下:

select DEPARTMENT_ID, DEPARTMENT_NAME from DEPARTMENTS where not exists (
    select * from EMPLOYEES where EMPLOYEES.DEPARTMENT_ID = DEPARTMENTS.DEPARTMENT_ID and  HIRE_DATE like '%2015%'
)

若還是想用 NOT IN子句,怎么辦?有方法,就是對子查詢返回的結果進行處理,如果為空值NULL,則給它賦一個非空值。這種對空值NULL的處理,各類數據庫管理系統都有相應函數函數支持,例如SQL Server中的 ISNULL函數,Oracle 中的NVLNVL2COALESE,以及MySQL中的IFNULL , COALESE

以SQL Server為例,如下SQL語句也能夠像NOT EXISTS子句一樣避免隱患。

select DEPARTMENT_ID, DEPARTMENT_NAME from DEPARTMENTS where DEPARTMENT_ID != all(
    select ISNULL(DEPARTMENT_ID,'') from EMPLOYEES where HIRE_DATE like '%2015%'
)

3. 小結

在使用NOT IN子句時,若子查詢結果集中可能包含空值NULL,則代碼存在隱患,要消除這種隱患,應該對子查詢結果集中的空值進行處理。

 

附:基於SQL Server的完整實驗代碼

假設公司ERP系統中有兩個表,員工表EMPLOYEES和部門表DEPARTMENTS(如下所示),老板要找出2015年沒有招人的部門號和部門名稱。請寫出查詢的SQL語句。

員工表 EMPLOYEES

EMPLOYEE_ID EMPLOYEE_NAME HIRE_DATE DEPARTMENT_ID
170101 Bob 2016-02-02 001
170102 Alice 2015-02-05 003
170103 Tony 2016-03-04 002
170105 Aaron 2016-08-03 002
170107 Rex 2016-10-11 NULL

部門表 DEPARTMENTS

DEPARTMENT_ID DEPARTMENT_NAME MANAGER_ID
001 Administration 170101
002 IT 170103
003 Finance 170102

創建表SQL語句。

CREATE TABLE DEPARTMENTS(
    DEPARTMENT_ID    CHAR(3) PRIMARY KEY,
    DEPARTMENT_NAME VARCHAR(100),
    MANAGER_ID        CHAR(6)
);

CREATE TABLE EMPLOYEES(
    EMPLOYEE_ID        CHAR(6) PRIMARY KEY,
    EMPLOYEE_NAME    VARCHAR(30) NOT NULL,
    HIRE_DATE        DATE,
    DEPARTMENT_ID    CHAR(3),
    FOREIGN KEY(DEPARTMENT_ID) REFERENCES DEPARTMENTS(DEPARTMENT_ID)
);

insert into DEPARTMENTS values('001','Administration','170101');
insert into DEPARTMENTS values('002','IT','170103');
insert into DEPARTMENTS values('003','Finance','170102');


insert into EMPLOYEES values('170101', 'Bob', '2016-03-02', '001');
insert into EMPLOYEES values('170102', 'Alice', '2015-02-05', '003');
insert into EMPLOYEES values('170103', 'Tony', '2016-03-04', '002');
insert into EMPLOYEES values('170105', 'Aaron', '2016-08-03', '002');
insert into EMPLOYEES values('170107', 'Rex', '2016-10-11', NULL);
展開代碼

 上述題目,我們很容易產生這樣的思路,先在員工表 EMPLOYEES 中找出2015年雇佣的員工所在的部門號,作為一個部門號子集合,然后在部門表 DEPARTMENTS 中找出不在該子集中的部門號和部門ID,即為要查找的結果。於是有了如下查詢SQL語句:

select DEPARTMENT_ID, DEPARTMENT_NAME from DEPARTMENTS where DEPARTMENT_ID not in(
    select DEPARTMENT_ID from EMPLOYEES where HIRE_DATE like '%2015%'
)

然而,由於子查詢中存在一個空值,所以SQL Server數據庫管理系統執行上述語句之后將返回0條結果。而實際上我們可以從上表中看到,2015年沒有招人的部門號和部門名稱有{001, Administration } 部門,故上面的查詢語句存在BUG。

為保證查詢出我們期望的結果,這里使用SQL Server的ISNULL函數對子查詢中的空值進行處理,處理之后SQL語句為:

select DEPARTMENT_ID, DEPARTMENT_NAME from DEPARTMENTS where DEPARTMENT_ID not in(
    select ISNULL(DEPARTMENT_ID,'') from EMPLOYEES where HIRE_DATE like '%2015%'
)

SQL Server DBMS將返回給我們期望的結果:

DEPARTMENT_ID    DEPARTMENT_NAME
001          Administration
002              IT


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM