大家好,我是娟姐。
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。