MySQL中隱式轉(zhuǎn)換的踩坑記錄以及解決方法分享
本來(lái)是一個(gè)平靜而美好的下午,其他部門(mén)的同事要一份數(shù)據(jù)報(bào)表臨時(shí)匯報(bào)使用,因?yàn)橄到y(tǒng)目前沒(méi)有這個(gè)維度的功能,所以需要寫(xiě)個(gè)SQL馬上出一下,一個(gè)同事接到這個(gè)任務(wù),于是開(kāi)始在測(cè)試環(huán)境拼裝這條 SQL,剛過(guò)了幾分鐘,同事已經(jīng)自信的寫(xiě)好了這條SQL,于是拿給DBA,到線上跑一下,用客戶端工具導(dǎo)出Excel 就好了,畢竟是臨時(shí)方案嘛。
就在SQL執(zhí)行了之后,意外發(fā)生了,先是等了一下,發(fā)現(xiàn)還沒(méi)執(zhí)行成功,猜測(cè)可能是數(shù)據(jù)量大的原因,但是隨著時(shí)間滴滴答答流逝,逐漸意識(shí)到情況不對(duì)了,一看監(jiān)控,CPU已經(jīng)上去了,但是線上數(shù)據(jù)量雖然不小,也不至于跑成這樣吧,眼看著要跑死了,趕緊把這個(gè)事務(wù)結(jié)束掉了。
什么原因呢?查詢(xún)的條件和 join 連接的字段基本都有索引,按道理不應(yīng)該這樣啊,于是趕緊把SQL拿下來(lái),也沒(méi)看出什么問(wèn)題,于是限制查詢(xún)條數(shù)再跑了一次,很快出結(jié)果了,但是結(jié)果卻大跌眼鏡,出來(lái)的查詢(xún)結(jié)果并不是預(yù)期的。
經(jīng)過(guò)一番檢查之后,最終發(fā)現(xiàn)了問(wèn)題所在,是 join 連接中有一個(gè)字段寫(xiě)錯(cuò)了,因?yàn)檫@兩個(gè)字段有一部分名稱(chēng)是相同的,于是智能的 SQL 客戶端給出了提示,順手就給敲上去了。但是接下來(lái),更讓人迷惑了,因?yàn)橐B接的字段是 int 類(lèi)型,而寫(xiě)錯(cuò)的這個(gè)字段是 varchar 類(lèi)型,難道不應(yīng)該報(bào)錯(cuò)嗎?怎么還能正常執(zhí)行,并且還有預(yù)期外的查詢(xún)結(jié)果?
難道是 MySQL 有 bug 了,必須要研究一下了。
復(fù)現(xiàn)當(dāng)時(shí)的情景
假設(shè)有兩張表,這兩張表的結(jié)構(gòu)和數(shù)據(jù)是下面這樣的。
第一張 user
表。
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) COLLATE utf8_bin DEFAULT NULL, `age` int(3) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin; INSERT INTO `user` VALUES (1, '張三', 28, '2022-09-06 07:40:56', '2022-09-06 07:40:59');
第二張 order
表
CREATE TABLE `order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) DEFAULT NULL, `order_code` varchar(64) COLLATE utf8_bin DEFAULT NULL, `money` decimal(20,0) DEFAULT NULL, `title` varchar(255) COLLATE utf8_bin DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin; INSERT INTO `order` VALUES (1, 2, '1d90530e-6ada-47c1-b2fa-adba4545aabd', 100, 'xxx購(gòu)買(mǎi)兩件商品', '2022-09-06 07:42:25', '2022-09-06 07:42:27');
目的是查看所有用戶的 order 記錄,假設(shè)數(shù)據(jù)量比較少,可以直接查,不考慮性能問(wèn)題。
本來(lái)的 SQL 語(yǔ)句應(yīng)該是這樣子的,查詢(xún) order
表中用戶iduser_id
在user
表的記錄。
select o.* from `user` u left JOIN `order` o on u.id = o.user_id;
但是呢,因?yàn)槭侄?,?on 后面的條件寫(xiě)成了 u.id = o.order_code
,完全關(guān)聯(lián)錯(cuò)誤,這兩個(gè)字段完全沒(méi)有聯(lián)系,而且u.id
是 int 類(lèi)型,o.order_code
是varchar
類(lèi)型。
select o.* from `user` u left JOIN `order` o on u.id = o.order_code;
這樣的話, 當(dāng)我們執(zhí)行這條語(yǔ)句的時(shí)候,會(huì)不會(huì)查出數(shù)據(jù)來(lái)呢?
我的第一感覺(jué)是,不僅不會(huì)查出數(shù)據(jù),而且還會(huì)報(bào)錯(cuò),因?yàn)檫B接的這兩個(gè)字段類(lèi)型都不一樣,值更不一樣。
結(jié)果卻被啪啪打臉,不僅沒(méi)有報(bào)錯(cuò),而且還查出了數(shù)據(jù)。
可以把這個(gè)問(wèn)題簡(jiǎn)化一下,簡(jiǎn)化成下面這條語(yǔ)句,同樣也會(huì)出現(xiàn)問(wèn)題。
select * from `order` where order_code = 1;
明明這條記錄的 order_code 字段的值是 1d90530e-6ada-47c1-b2fa-adba4545aabd
,怎么用 order_code=1
的條件就把它給查出來(lái)了。
根源所在
相信有的同學(xué)已經(jīng)猜出來(lái)了,這里是 MySQL 進(jìn)行了隱式轉(zhuǎn)換,由于查詢(xún)條件后面跟的查詢(xún)值是整型的,所以 MySQL 將 order_code
字段進(jìn)行了字符串到整數(shù)類(lèi)型的轉(zhuǎn)換,而轉(zhuǎn)換后的結(jié)果正好是 1
。
通過(guò) cast
函數(shù)轉(zhuǎn)換驗(yàn)證一下結(jié)果。
select cast('1d90530e-6ada-47c1-b2fa-adba4545aabd' as unsigned);
再用兩條 SQL 看一下字符串到整數(shù)類(lèi)型轉(zhuǎn)換的規(guī)則。
select cast('223kkk' as unsigned); select cast('k223kkk' as unsigned);
223kkk
轉(zhuǎn)換后的結(jié)果是 223
,而k223kkk
轉(zhuǎn)換后的結(jié)果是0??偨Y(jié)一下,轉(zhuǎn)換的規(guī)則是:
1、從字符串的左側(cè)開(kāi)始向右轉(zhuǎn)換,遇到非數(shù)字就停止;
2、如果第一個(gè)就是非數(shù)字,最后的結(jié)果就是0;
隱式轉(zhuǎn)換的規(guī)則
當(dāng)操作符與不同類(lèi)型的操作數(shù)一起使用的時(shí)候,就會(huì)發(fā)生隱式轉(zhuǎn)換。
例如算數(shù)運(yùn)算符的前后是不同類(lèi)型時(shí),會(huì)將非數(shù)字類(lèi)型轉(zhuǎn)換為數(shù)字,比如 '5a'+2,就會(huì)將5a
轉(zhuǎn)換為數(shù)字類(lèi)型,然后和2相加,最后的結(jié)果就是 7 。
再比如 concat
函數(shù)是連接兩個(gè)字符串的,當(dāng)此函數(shù)的參數(shù)出現(xiàn)非字符串類(lèi)型時(shí),就會(huì)將其轉(zhuǎn)換為字符串,例如concat(88,'就是發(fā)'),最后的結(jié)果就是 88就是發(fā)。
MySQL 官方文檔有以下幾條關(guān)于隱式轉(zhuǎn)換的規(guī)則:
1、兩個(gè)參數(shù)至少有一個(gè)是 NULL 時(shí),比較的結(jié)果也是 NULL,例外是使用 <=> 對(duì)兩個(gè) NULL 做比較時(shí)會(huì)返回 1,這兩種情況都不需要做類(lèi)型轉(zhuǎn)換;
也就是兩個(gè)參數(shù)中如果只有一個(gè)是NULL,則不管怎么比較結(jié)果都是 NULL,而兩個(gè) NULL 的值不管是判斷大于、小于或等于,其結(jié)果都是1。
2、兩個(gè)參數(shù)都是字符串,會(huì)按照字符串來(lái)比較,不做類(lèi)型轉(zhuǎn)換;
3、兩個(gè)參數(shù)都是整數(shù),按照整數(shù)來(lái)比較,不做類(lèi)型轉(zhuǎn)換;
4、十六進(jìn)制的值和非數(shù)字做比較時(shí),會(huì)被當(dāng)做二進(jìn)制字符串;
例如下面這條語(yǔ)句,查詢(xún) user 表中name字段是 0x61 的記錄,0x
是16進(jìn)制寫(xiě)法,其對(duì)應(yīng)的字符串是英文的 'a',也就是它對(duì)應(yīng)的 ASCII 碼。
select * from user where name = 0x61;
所以,上面這條語(yǔ)句其實(shí)等同于下面這條
select * from user where name = 'a';
可以用 select 0x61;
驗(yàn)證一下。
5、有一個(gè)參數(shù)是 TIMESTAMP 或 DATETIME,并且另外一個(gè)參數(shù)是常量,常量會(huì)被轉(zhuǎn)換為 時(shí)間戳;
例如下面這兩條SQL,都是將條件后面的值轉(zhuǎn)換為時(shí)間戳再比較了,只不過(guò)
6、有一個(gè)參數(shù)是 decimal 類(lèi)型,如果另外一個(gè)參數(shù)是 decimal 或者整數(shù),會(huì)將整數(shù)轉(zhuǎn)換為 decimal 后進(jìn)行比較,如果另外一個(gè)參數(shù)是浮點(diǎn)數(shù)(一般默認(rèn)是 double),則會(huì)把 decimal 轉(zhuǎn)換為浮點(diǎn)數(shù)進(jìn)行比較;
在不同的數(shù)值類(lèi)型之間,總是會(huì)向精度要求更高的那一個(gè)類(lèi)型轉(zhuǎn)換,但是有一點(diǎn)要注意,在MySQL 中浮點(diǎn)數(shù)的精度只有53 bit,超過(guò)53bit之后的話,如果后面1位是1就進(jìn)位,如果是0就直接舍棄。所以超大浮點(diǎn)數(shù)在比較的時(shí)候其實(shí)只是取的近似值。
7、所有其他情況下,兩個(gè)參數(shù)都會(huì)被轉(zhuǎn)換為浮點(diǎn)數(shù)再進(jìn)行比較;
如果不符合上面6點(diǎn)規(guī)則,則統(tǒng)一轉(zhuǎn)成浮點(diǎn)數(shù)再進(jìn)行運(yùn)算
避免進(jìn)行隱式轉(zhuǎn)換
我們?cè)谄綍r(shí)的開(kāi)發(fā)過(guò)程中,盡量要避免隱式轉(zhuǎn)換,因?yàn)橐坏┌l(fā)生隱式轉(zhuǎn)換除了會(huì)降低性能外, 還有很大可能會(huì)出現(xiàn)不期望的結(jié)果,就像我最開(kāi)始遇到的那個(gè)問(wèn)題一樣。
之所以性能會(huì)降低,還有一個(gè)原因就是讓本來(lái)有的索引失效。
select * from `order` where order_code = 1;
order_code 是 varchar 類(lèi)型,假設(shè)我已經(jīng)在 order_code 上建立了索引,如果是用“=”做查詢(xún)條件的話,應(yīng)該直接命中索引才對(duì),查詢(xún)速度會(huì)很快。但是,當(dāng)查詢(xún)條件后面的值類(lèi)型不是 varchar,而是數(shù)值類(lèi)型的話,MySQL 首先要對(duì) order_code 字段做類(lèi)型轉(zhuǎn)換,轉(zhuǎn)換為數(shù)值類(lèi)型,這時(shí)候,之前建的索引也就不會(huì)命中,只能走全表掃描,查詢(xún)性能指數(shù)級(jí)下降,搞不好,數(shù)據(jù)庫(kù)直接查崩了。
到此這篇關(guān)于MySQL中隱式轉(zhuǎn)換的踩坑記錄以及解決方法分享的文章就介紹到這了,更多相關(guān)MySQL隱式轉(zhuǎn)換內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MySQL 隨機(jī)函數(shù)獲取數(shù)據(jù)速度和效率分析
最近做項(xiàng)目,需要做一個(gè)從mysql數(shù)據(jù)庫(kù)中隨機(jī)取幾條數(shù)據(jù)出來(lái)??偹苤?,order by rand 會(huì)死人的。。因?yàn)楸救藢?duì)大數(shù)據(jù)量方面的只是了解的很少,無(wú)解,去找百度老師。。搜索結(jié)果千篇一律。特發(fā)到這里來(lái),供大家學(xué)習(xí),需要的朋友可以參考下2016-11-11MySQL之使用WITH子句和臨時(shí)表達(dá)式進(jìn)行數(shù)據(jù)分析和篩選方式
這篇文章主要介紹了MySQL之使用WITH子句和臨時(shí)表達(dá)式進(jìn)行數(shù)據(jù)分析和篩選方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-04-04用MySQL創(chuàng)建數(shù)據(jù)庫(kù)和數(shù)據(jù)庫(kù)表代碼
了解了一些最基本的操作命令后,我們?cè)賮?lái)學(xué)習(xí)如何創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)和數(shù)據(jù)庫(kù)表。2008-10-10MySQL?使用觸發(fā)器記錄用戶的操作日志問(wèn)題
使用?MySQL?觸發(fā)器可以記錄哪些用戶、什么時(shí)間對(duì)數(shù)據(jù)表進(jìn)行了增、刪、改操作。如果執(zhí)行刪除操作,則記錄刪除之前的數(shù)據(jù)記錄;如果執(zhí)行更新操作,記錄更新之前的數(shù)據(jù)記錄,這篇文章主要介紹了MySQL?使用觸發(fā)器記錄用戶的操作日志,需要的朋友可以參考下2022-12-12mysql數(shù)據(jù)庫(kù)遷移數(shù)據(jù)目錄至另一臺(tái)服務(wù)器詳細(xì)步驟
MySQL數(shù)據(jù)庫(kù)轉(zhuǎn)移到新服務(wù)器是指將現(xiàn)有的MySQL數(shù)據(jù)庫(kù)遷移至一個(gè)新的服務(wù)器環(huán)境中,下面這篇文章主要給大家介紹了關(guān)于mysql數(shù)據(jù)庫(kù)遷移數(shù)據(jù)目錄至另一臺(tái)服務(wù)器的詳細(xì)步驟,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-07-07