一個 Sql 語句引發的詭異事件

大家好,我是娟姐。

01

App內測的一個下午,測試組的小美女就向我反饋了一個事情。一個身在外地的小哥哥新註冊的賬號,他和小美女並無交集,但是他倆卻莫名成爲好友了。老闆聽聞後,直呼詭異。

“在兩點到四點之間,我都在忙別的事情,並未使用App。但當我打開App時,我發現他居然成了我的好友?並且給我發了信息!”,小美女盯着我的眼睛說。

“有沒有其他人使用過你的賬號?”

“沒有,如果有的話,我的賬號會被擠掉的!”,小美女十分肯定。

“你倆誰加的誰呢?”

“他收到了我的好友請求,但是我並不認識他,也不知道他的手機號!”

“你的意思是,你沒有發好友請求,但是他接收到了你的好友請求,並且同意了?”

“是的,我沒有發好友請求,他接收到了我的好友請求,並且同意了。”

“好的,我來調查一下吧!”

我的心裏也充滿疑惑,但是猜不出具體原因出在哪裏,還是調查之後再下結論吧。

02

打開服務器端的接口日誌,發現了同意好友請求的記錄和通知,但是沒有看到加好友的請求記錄。查看源碼,在申請加好友時並沒有打印相關日誌。

打開路由日誌,在路由日誌裏還是隻有同意好友請求的路由地址,沒有申請加好友的路由地址。

這麼說,這條加好友請求不是人爲發起的,而是無中生有冒出來的?

這時開發的同事給我發了一個圖片,申請加好友的一個公共方法,被三處調用。

其中兩處在一個方法裏,是 if 和 else 的關係,這個方法是通過人爲發起的,也就是用戶需要先知道對方的手機號碼,或者通過羣聊,才能申請加好友。

還有一處是自動生成好友請求的業務邏輯,這段業務邏輯就不說了,涉及到商業機密,初步定位問題就出在這裏。

自動生成好友請求的業務邏輯非常簡單,第一個判斷語句前有一個Sql查詢語句引起了我的注意,在 left join 的 on 後面,and 了好幾個查詢條件。

03

現在來模擬一下這個Sql語句。

1、先建立兩個表

一個是 user_info 用戶表,另一個是 user_addr 用戶收貨地址表,一個用戶可以有多個收貨地址。

用戶表中,有三個字段,uid 是用戶主鍵不可重複,name是用戶名可以重複,is_delete 是刪除標識。

在收貨地址表中,有七個字段,其中 user_uid 是來自用戶表的外鍵,id 爲本表的主鍵。

2、兩張表分表錄入幾條數據

三個用戶數據,李四的用戶數據已被標識爲刪除。

張三有三個收貨地址,一個是自己的,一個是張父的,最後一個是張姐的。其他幾人暫無收貨地址。

3、此次查詢的要求是,查看數據庫裏是否還有一個叫“張父”的收貨地址,並且把這個數據所屬的用戶找出來


select A.*,B.user_link,B.is_delete from user_info as A
left join user_addr as B on A.uid= B.user_uid and B.is_delete = 0 and B.user_link = '張父'
where A.is_delete = 0 ;

執行結果如下圖所示,好傢伙,把沒有關係的 老王 也扯了進來。估計老王一臉的黑線。

04

也有同學說,我可以不用左連接,可以用內連接呀!

關於內連接和左連接,MySql 的官方文檔是這麼描述的:

在MySQL中,JOIN、CROSS JOIN 和 INNER JOIN 在語法上是等價的(它們可以相互替換)。在標準SQL中,它們是不相等的。INNER JOIN與ON子句一起使用,否則使用CROSS JOIN。

關於 逗號,內連接、左連接和右連接,它們還有其他區別嗎?Mysql 的官方文檔是這麼描述的:

在沒有連接條件的情況下,INNER JOIN 和 逗號 在語義上是等價的:它們都在指定的表之間產生笛卡爾積(也就是說,第一個表中的每一行都與第二個表中的每一行連接)。

但是,逗號操作符的優先級低於INNER JOIN、CROSS JOIN、LEFT JOIN等。如果在 存在連接條件時 將逗號連接與其他連接類型混合使用,則可能會出現“on子句”中未知列“col_name”形式的錯誤。

與ON一起使用的查詢條件 是可以在WHERE子句中使用的任何形式的條件表達式。通常,ON子句用於指定如何連接表的條件,WHERE子句限制在結果集中包含哪些行。

劃重點:
ON 子句用於指定如何連接表的條件,WHERE 子句限制在結果集中包含哪些行。

這麼說,限制條件要加在 where 後面,兩表之間的連接關係才放在 ON 後面。

調整後的Sql語句爲:

select A.*,B.user_link,B.is_delete from user_info as A
left join user_addr as B on A.uid= B.user_uid and B.is_delete = 0
where A.is_delete = 0 and  B.user_link = '張父';

查詢結果如下圖所示,終於查到了我們想要的數據。

可能有同學覺得,where後面的限制條件太多了,挪幾個放在ON後面吧,反正都一樣。錯了,還真不一樣。哪些能放在ON後面,哪些不能放在ON後面,還需要仔細掂量一下。

05

如果我把張父的收貨地址刪除,並且只查詢未刪除的,收貨地址表的is_delete放在哪裏一樣嗎?

select A.*,B.user_link,B.is_delete from user_info as A
left join user_addr as B on A.uid= B.user_uid and B.is_delete = 0
where A.is_delete = 0 and  B.user_link = '張父';

select A.*,B.user_link,B.is_delete from user_info as A
left join user_addr as B on A.uid= B.user_uid 
where A.is_delete = 0 and B.is_delete = 0 and B.user_link = '張父';

這次查詢結果卻是一致的,爲什麼呢?

在使用 left join 時,on 和 where 條件的區別如下:

  • on 條件是在生成臨時表時使用的條件,它不管on中的條件是否爲真,都會返回左邊表中的記錄。

  • where 條件是在臨時表生成好後,再對臨時表進行過濾的條件。這時已經沒有left join的含義(必須返回左邊表的記錄)了,條件不爲真的就全部過濾掉。

這樣就解釋得通了,不管 on 中的條件是否爲真,都會返回主表中的記錄。把where 條件註釋掉再執行:

select A.*,B.user_link,B.is_delete from user_info as A
left join user_addr as B on A.uid= B.user_uid and B.is_delete = 0;
# where A.is_delete = 0 and  B.user_link = '張父';

select A.*,B.user_link,B.is_delete from user_info as A
left join user_addr as B on A.uid= B.user_uid 
# where A.is_delete = 0 and B.is_delete = 0 and B.user_link = '張父';

第一個sql語句在生成臨時表的時候,把收貨地址表已刪除的數據過濾掉了。排除刪除的收穫地址,用戶表和收穫地址表進行了笛卡爾積關聯。


第二個sql語句在生成臨時表的時候,沒有把收貨地址表已刪除的數據過濾掉,返回了一個完整的笛卡爾積數據集。

再來執行一下那個有錯誤的 Sql 語句,把 where 條件註釋掉。

select A.*,B.user_link,B.is_delete from user_info as A
left join user_addr as B on A.uid= B.user_uid and B.is_delete = 0 and B.user_link = '張父'
# where A.is_delete = 0 ;

這個 sql 語句在生成臨時表的時候,就把聯繫人爲 ‘張父’的數據過濾了出來,由於是臨時表,又是笛卡爾積關聯,所以另外兩個沒有收穫地址的朋友也會被關聯出來。所以呢,問題就出在這裏。

以上便是這次詭異事件的調查結果,一個Sql語句寫法不當所導致的bug。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章