資料庫的重要性和通用性都不用說了,什麼sql的通用性,sql優化之類的也不必說了,咱們今天來聊聊另一個有意思的話題:如何取一個篩選的反面案例。
為了讓大家理解我們的假設場景,什麼叫做正反案例?比如:
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', 反義詞則需要考慮到優先順序的問題了。
至於更復雜的咱們就不說了。
通過以上案例,相信大家已經明白我在說什麼了。沒錯,就是求反義詞。具體應用場景是啥呢?舉個例子,使用者設定了一個基礎規則,然後其他地方可以參照,正向參照,則是條件為真,反向參照則是條件為假。
不管怎麼樣,考題就是如何求解一個條件的反向表示?
正向解題思路是啥呢?首先,如果想要自行求反解,那麼第一步就是必須要先理解正向表達的語意,即你至少得有分詞、構建語法樹、理解語意的過程。
這樣做完之後,至少你可以做一些事了,就像前面幾個簡單的單條件設定,為空的反義詞就是不為空,中間加個 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.. ... }
可以看出,應該還是可行的,但是對於像優先順序,括號,四則運算之類的處理,那應該是相當的複雜的。對於非專業搞資料庫開發,或者編譯器的同學而言,應該是非常之難的。具體咱也不知道,看你咯。
我們知道,一個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測試, https://www.sqlite.org/download.html 。參考下載連結: https://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語法是可以覆蓋結果取反的場景的。
雖然not語法看起來沒啥問題,但是在官方的介紹裡,貌似並沒有找到相應的章節描述,為什麼呢?不得而知。
那麼sql的not在底層是怎麼實現的呢?兩個思路吧:一是像咱們前面提到的進行反面條件轉換,得到後再進行執行;二是直接計算的時候,先正向計算出結果,然後遇到not之後,當作一個運運算元,直接將結果取反,從而決定結果是拋棄還是保留。
因為資料庫底層都是是根據規則依次計算結果判定,所以最合適的方式是正向計算結果,然後遇到not進行true/false反轉,這是其正常的執行引擎流程。但具體是否如此,暫不得而知,待以後有機會再研究研究。
通過本文的介紹,在以後的工作中,咱們也可以多了一個選擇了,雖然少見,但不排除遇見。希望能為大家多一點參考。