sql語法巧用之not取反

  資料庫的重要性和通用性都不用說了,什麼sql的通用性,sql優化之類的也不必說了,咱們今天來聊聊另一個有意思的話題:如何取一個篩選的反面案例。

 

1. 舉幾個正反案例的例子

  為了讓大家理解我們的假設場景,什麼叫做正反案例?比如:

  0. 正向案例為:取出年齡為空的用戶,那麼反面案例為:取出年齡不為空的用戶;
  1. 正向案例為:取出年齡大於25的用戶,那麼反面案例則為:取出年齡小於等於25的用戶;
  2. 正向案例為:取出姓名為男的用戶,那麼反面案例則為:取出性別不等於男的用戶;
  3. 正向案例為:取出薪資在1000-2000之間的用戶,那麼反面案例為:取出薪資小於1000或者大於2000的用戶;
  4. 正向案例為:取出年齡大於25且性別為男的用戶,那麼反面案例為:取出年齡小於25或者性別不為男的用戶;
  5. 正向案例為:取出年齡大於25且為男性或者薪資大於2000的用戶,那麼反面案例為:取出年齡小於25或者性別不為男的用戶且薪資小於等於2000的用戶;

  相信大家都理解了,其實就相當於取反義詞。也就是說輸入是一個正向規則,我們需要輸出一個反向規則。當然一個前提是咱們使用sql語言。

  從前到後,我們可以理解為一個實現難度的提升,比如第一個 ‘is null’ 的反義詞則是 ‘is not null’,第二個 ‘>’ 的反義詞則是 ‘<=’。這些簡單的是單個規則的表述。

  但到第4個案例,就涉及到區間了,相當於有組合詞了,即 ‘between 1000 and 2000’, 反義詞則需要向兩邊取值了即: ‘<= 100 or >= 2000’;

  第6個則更複雜,涉及三個變數,即 ‘age > 25 and sex = ‘男’ or salary > 2000′, 反義詞則需要考慮到優先順序的問題了。

  至於更複雜的咱們就不說了。

 

2. 正面硬剛反義詞

  通過以上案例,相信大家已經明白我在說什麼了。沒錯,就是求反義詞。具體應用場景是啥呢?舉個例子,用戶配置了一個基礎規則,然後其他地方可以引用,正向引用,則是條件為真,反向引用則是條件為假。

  不管怎麼樣,考題就是如何求解一個條件的反向表示?

  正向解題思路是啥呢?首先,如果想要自行求反解,那麼第一步就是必須要先理解正向表達的語義,即你至少得有分詞、構建語法樹、理解語義的過程。

  這樣做完之後,至少你可以做一些事了,就像前面幾個簡單的單條件配置,為空的反義詞就是不為空,中間加個 not 就可以了,則可以直接套用固定反轉即可。抽象點說就是,根據一個固定的規則映射字典,就可以找到反義詞了。

  但是,針對有多個條件表達的情況,則會複雜起來,先來看有兩個條件連接為’且’的表達,那麼求反就不能通過單個字典映射進行處理了。但仍然可以拆解為兩個求反操作,即’條件1求反’ 或者 ‘條件2求反’。

  而針對兩個條件連接為’或’的表達,則需要對單個未反,然後用’且’連接,即 ‘條件1求反’ 且 ‘條件2求反’。

  以上,仍然停留在比較簡單的場景,即只有1個條件或者2個條件的情況下,而更多的是,可能3個、4個、10個甚至更多個,甚至還有'()’括弧的場景,多層嵌套,這樣的求反,其實就相當複雜了。但到底能不能實現求反呢?理論上可行的,實際上不管條件有多少個,在sql的表達中,都是一個個的bool表達式,然後使用’and’/’or’ 連接,而且更重要的,不管有多少個’and’/’or’, 最終總要一個個計算,所以我們只需要一直拆解條件表達式,直到它是一個原子表達式,然後再套用字典轉換,就可以做到求反的效果了。當然了,這個實現應該還是一個很複雜的過程,而且不一定適用,咱們就只給出一些偽程式碼供參考了。 

表達式求反函數(入參: 原始表達式) {
    分詞;
    語法樹構建;
    語義解析構造優先順序的bool表達式樹;
    
    複雜條件求反 -> 代入現有解析好的bool表達式;
}

複雜條件求反(入參: bool表達式) {
    if 原子表達式:
        return 字典映射求反表達式;
        
    if 當前連接符是 'and':
        複雜條件求反 -> 代入左邊的bool表達式;
        複雜條件求反 -> 代入左邊的bool表達式;
        return 以上兩個結果用'or'連接;
    
    if 當前連接符是 'or':
        複雜條件求反 -> 代入左邊的bool表達式;
        複雜條件求反 -> 代入左邊的bool表達式;
        return 以上兩個結果用'and'連接;
}

字典映射求反表達式(入參: bool表達式) {
    為空 -> 不為空;
    等於 -> 不等於;
    大於 -> 小於等於;
    in -> not..in..
    ...
}

  可以看出,應該還是可行的,但是對於像優先順序,括弧,四則運算之類的處理,那應該是相當的複雜的。對於非專業搞資料庫開發,或者編譯器的同學而言,應該是非常之難的。具體咱也不知道,看你咯。

 

3. sql語法巧用

  我們知道,一個sql的bool表達式,有true/false之分,正常情況下都是以 true 作為判斷條件的。比如 is null 為true, 那麼 is not null 就為false。 =1為true, 那麼 !=false, in 為true, 那麼 not in 就為false。between 為true, 那麼 not between 就為false.

  雖然情況很多,但是我們已經看到,sql中天然就有一個詞代表了取反的意思。只是好像只有特定的場景下才可以使用not關鍵詞。好像有點失望了。

  為什麼不試一試呢?比如 x=1 的反義詞是否可以是 not x = 1 ? 為測試方便,我們直接使用記憶體資料庫sqllite測試, //www.sqlite.org/download.html  。參考下載鏈接: //www.sqlite.org/2022/sqlite-tools-win32-x86-3390400.zip

  接下來我們用兩張表測試下。

-- 新建測試表1
create table test1 (
    id int, name varchar(50), 
    age int 
) 
-- comment '用戶基礎資訊表';
-- 新建測試表2
create table test2 (
    uid int, 
    salary double, 
    company varchar(50)
) 
-- comment '用戶工作資訊表';
-- 插入測試數據
insert into test1 (id, name, age) values (1, 'zhangsan', 18);
insert into test1 (id, name, age) values (2, 'lisi', 20);
insert into test1 (id, name, age) values (3, 'wanger', 30);
insert into test2 (uid, salary, company) values (1, 1000.1, 'axxx');
insert into test2 (uid, salary, company) values (2, 2000.1, 'bxxx');
insert into test2 (uid, salary, company) values (3, 3000.1, 'cxxx');

  接下來我們用not語法和非not語法測試下。

sqlite> select * from test1 where name = 'zhangsan';
1|zhangsan|18
sqlite> select * from test1 where name != 'zhangsan';
2|lisi|20
3|wanger|30
sqlite> select * from test1 where not (name = 'zhangsan');
2|lisi|20
3|wanger|30

  看起來語法是支援的,而且兩個語法的簡單語句執行結果居然是一樣的。接下來我們測試稍微複雜點的:

sqlite> select * from test1 where name = 'zhangsan' or name = 'lisi';
1|zhangsan|18
2|lisi|20
sqlite> select * from test1 where name != 'zhangsan' and name != 'lisi';
3|wanger|30
sqlite> select * from test1 where not( name = 'zhangsan' or name = 'lisi');
3|wanger|30

  看起來多個條件的連接not語法也是支援的,而且結果也是正確的呢。我們來測試一個三條件的語句:

sqlite> select * from test1 where name = 'zhangsan' or name = 'lisi' and age = 20;
1|zhangsan|18
2|lisi|20
sqlite> select * from test1 where name != 'zhangsan' and (name != 'lisi' or age != 20);
3|wanger|30
sqlite> select * from test1 where not (name = 'zhangsan' or name = 'lisi' and age = 20);
3|wanger|30
sqlite> select * from test1 where age > 20;
3|wanger|30
sqlite> select * from test1 where age <= 20;
1|zhangsan|18
2|lisi|20
sqlite> select * from test1 where not( age > 20 );
1|zhangsan|18
2|lisi|20

  好吧,看起來單表的操作並沒有問題。會不會是因為單表簡單的原因?我們試試多表join的:

sqlite> select t1.name,t1.age, t2.salary from test1 t1 left join test2 t2 on t1.id = t2.uid where t1.age >= 20 and t2.salary > 2000;
lisi|20|2000.1
wanger|30|3000.1
sqlite> select t1.name,t1.age, t2.salary from test1 t1 left join test2 t2 on t1.id = t2.uid where t1.age < 20 or t2.salary <= 2000;
zhangsan|18|1000.1
sqlite> select t1.name,t1.age, t2.salary from test1 t1 left join test2 t2 on t1.id = t2.uid where not (t1.age >= 20 and t2.salary > 2000);
zhangsan|18|1000.1

  是了,沒問題,語法支援,結果正確。換成其他的sql類資料庫做同樣的測試,仍然一致。基本可以確定,not語法是可以覆蓋結果取反的場景的。

 

4. not語法的底層原理

  雖然not語法看起來沒啥問題,但是在官方的介紹里,貌似並沒有找到相應的章節描述,為什麼呢?不得而知。

  那麼sql的not在底層是怎麼實現的呢?兩個思路吧:一是像咱們前面提到的進行反麵條件轉換,得到後再進行執行;二是直接計算的時候,先正向計算出結果,然後遇到not之後,當作一個運算符,直接將結果取反,從而決定結果是拋棄還是保留。

  因為資料庫底層都是是根據規則依次計算結果判定,所以最合適的方式是正向計算結果,然後遇到not進行true/false反轉,這是其正常的執行引擎流程。但具體是否如此,暫不得而知,待以後有機會再研究研究。

  通過本文的介紹,在以後的工作中,咱們也可以多了一個選擇了,雖然少見,但不排除遇見。希望能為大家多一點參考。