資料庫優化是一個任重而道遠的任務,想要做優化必須深入理解資料庫的各種特性。在開發過程中我們經常會遇到一些原因很簡單但造成的後果卻很嚴重的疑難雜症,這類問題往往還不容易定位,排查費時費力最後發現是一個很小的疏忽造成的,又或者是因為不瞭解某個技術特性產生的。 於資料庫層面,最常見的恐怕就是索引失效了,且 ...
資料庫優化是一個任重而道遠的任務,想要做優化必須深入理解資料庫的各種特性。在開發過程中我們經常會遇到一些原因很簡單但造成的後果卻很嚴重的疑難雜症,這類問題往往還不容易定位,排查費時費力最後發現是一個很小的疏忽造成的,又或者是因為不瞭解某個技術特性產生的。
於資料庫層面,最常見的恐怕就是索引失效了,且一開始因為數據量小還不易被髮現。但隨著業務的拓展數據量的提升,性能問題慢慢的就體現出來了,處理不及時還很容易造成雪球效應,最終導致資料庫卡死甚至癱瘓。造成索引失效的原因可能有很多種,相關技術博客已經有太多了,今天我要記錄的是隱式轉換造成的索引失效。
數據準備
首先使用存儲過程生成1000萬條測試數據,
測試表一共建立了7個欄位(包括主鍵),num1
和num2
保存的是和ID
一樣的順序數字,其中num2
是字元串類型。
type1
和type2
保存的都是主鍵對5的取模,目的是模擬實際應用中常用類似type類型的數據,但是type2
是沒有建立索引的。
str1
和str2
都是保存了一個20位長度的隨機字元串,str1
不能為NULL
,str2
允許為NULL
,相應的生成測試數據的時候我也會在str2
欄位生產少量NULL
值(每100條數據產生一個NULL
值)。
-- 創建測試數據表
DROP TABLE IF EXISTS test1;
CREATE TABLE `test1` (
`id` int(11) NOT NULL,
`num1` int(11) NOT NULL DEFAULT '0',
`num2` varchar(11) NOT NULL DEFAULT '',
`type1` int(4) NOT NULL DEFAULT '0',
`type2` int(4) NOT NULL DEFAULT '0',
`str1` varchar(100) NOT NULL DEFAULT '',
`str2` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `num1` (`num1`),
KEY `num2` (`num2`),
KEY `type1` (`type1`),
KEY `str1` (`str1`),
KEY `str2` (`str2`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 創建存儲過程
DROP PROCEDURE IF EXISTS pre_test1;
DELIMITER //
CREATE PROCEDURE `pre_test1`()
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
WHILE i < 10000000 DO
SET i = i + 1;
SET @str1 = SUBSTRING(MD5(RAND()),1,20);
-- 每100條數據str2產生一個null值
IF i % 100 = 0 THEN
SET @str2 = NULL;
ELSE
SET @str2 = @str1;
END IF;
INSERT INTO test1 (`id`, `num1`, `num2`,
`type1`, `type2`, `str1`, `str2`)
VALUES (CONCAT('', i), CONCAT('', i),
CONCAT('', i), i%5, i%5, @str1, @str2);
-- 事務優化,每一萬條數據提交一次事務
IF i % 10000 = 0 THEN
COMMIT;
END IF;
END WHILE;
END;
// DELIMITER ;
-- 執行存儲過程
CALL pre_test1();
數據量比較大,還涉及使用MD5
生成隨機字元串,所以速度有點慢,稍安勿躁,耐心等待即可。
1000萬條數據,我用了33分鐘才跑完(實際時間跟你電腦硬體配置有關)。這裡貼幾條生成的數據,大致長這樣。
SQL測試
先來看這組SQL,一共四條,我們的測試數據表num1
是int
類型,num2
是varchar
類型,但是存儲的數據都是跟主鍵id
一樣的順序數字,兩個欄位都建立有索引。
1: SELECT * FROM `test1` WHERE num1 = 10000;
2: SELECT * FROM `test1` WHERE num1 = '10000';
3: SELECT * FROM `test1` WHERE num2 = 10000;
4: SELECT * FROM `test1` WHERE num2 = '10000';
這四條SQL都是有針對性寫的,12查詢的欄位是int類型,34查詢的欄位是varchar
類型。12或34查詢的欄位雖然都相同,但是一個條件是數字,一個條件是用引號引起來的字元串。這樣做有什麼區別呢?先不看下邊的測試結果你能猜出這四條SQL的效率順序嗎?
經測試這四條SQL最後的執行結果卻相差很大,其中124三條SQL基本都是瞬間出結果,大概在0.001~0.005秒,在千萬級的數據量下這樣的結果可以判定這三條SQL性能基本沒差別了。但是第三條SQL,多次測試耗時基本在4.5~4.8秒之間。
為什麼34兩條SQL效率相差那麼大,但是同樣做對比的12兩條SQL卻沒什麼差別呢?查看一下執行計劃,下邊分別1234條SQL的執行計劃數據:
可以看到,124三條SQL都能使用到索引,連接類型都為ref
,掃描行數都為1,所以效率非常高。再看看第三條SQL,沒有用上索引,所以為全表掃描,rows
直接到達1000萬了,所以性能差別才那麼大。
仔細觀察你會發現,34兩條SQL查詢的欄位num2
是varchar
類型的,查詢條件等號右邊加引號的第4條SQL是用到索引的,那麼是查詢的數據類型和欄位數據類型不一致造成的嗎?如果是這樣那12兩條SQL查詢的欄位num1
是int
類型,但是第2條SQL查詢條件右邊加了引號為什麼還能用上索引呢。
查閱MySQL相關文檔發現是隱式轉換造成的,看一下官方的描述:
官方文檔: 12.2 Type Conversion in Expression Evaluation
當操作符與不同類型的操作數一起使用時,會發生類型轉換以使操作數相容。某些轉換是隱式發生的。例如,MySQL會根據需要自動將字元串轉換為數字,反之亦然。以下規則描述了比較操作的轉換方式:
- 兩個參數至少有一個是
NULL
時,比較的結果也是NULL
,特殊的情況是使用<=>
對兩個NULL
做比較時會返回1
,這兩種情況都不需要做類型轉換- 兩個參數都是字元串,會按照字元串來比較,不做類型轉換
- 兩個參數都是整數,按照整數來比較,不做類型轉換
- 十六進位的值和非數字做比較時,會被當做二進位串
- 有一個參數是
TIMESTAMP
或DATETIME
,並且另外一個參數是常量,常量會被轉換為timestamp
- 有一個參數是
decimal
類型,如果另外一個參數是decimal
或者整數,會將整數轉換為decimal
後進行比較,如果另外一個參數是浮點數,則會把decimal
轉換為浮點數進行比較- 所有其他情況下,兩個參數都會被轉換為浮點數再進行比較
根據官方文檔的描述,我們的第23兩條SQL都發生了隱式轉換,第2條SQL的查詢條件num1 = '10000'
,左邊是int
類型右邊是字元串,第3條SQL相反,那麼根據官方轉換規則第7條,左右兩邊都會轉換為浮點數再進行比較。
先看第2條SQL:SELECT * FROM `test1` WHERE num1 = '10000';
左邊為int類型10000
,轉換為浮點數還是10000
,右邊字元串類型'10000'
,轉換為浮點數也是10000
。兩邊的轉換結果都是唯一確定的,所以不影響使用索引。
第3條SQL:SELECT * FROM `test1` WHERE num2 = 10000;
左邊是字元串類型'10000'
,轉浮點數為10000是唯一的,右邊int
類型10000
轉換結果也是唯一的。但是,因為左邊是檢索條件,'10000'
轉到10000
雖然是唯一,但是其他字元串也可以轉換為10000
,比如'10000a'
,'010000'
,'10000'
等等都能轉為浮點數10000
,這樣的情況下,是不能用到索引的。
關於這個隱式轉換我們可以通過查詢測試驗證一下,先插入幾條數據,其中num2='10000a'
、'010000'
和'10000'
:
INSERT INTO `test1` (`id`, `num1`, `num2`, `type1`, `type2`, `str1`, `str2`) VALUES ('10000001', '10000', '10000a', '0', '0', '2df3d9465ty2e4hd523', '2df3d9465ty2e4hd523');
INSERT INTO `test1` (`id`, `num1`, `num2`, `type1`, `type2`, `str1`, `str2`) VALUES ('10000002', '10000', '010000', '0', '0', '2df3d9465ty2e4hd523', '2df3d9465ty2e4hd523');
INSERT INTO `test1` (`id`, `num1`, `num2`, `type1`, `type2`, `str1`, `str2`) VALUES ('10000003', '10000', ' 10000', '0', '0', '2df3d9465ty2e4hd523', '2df3d9465ty2e4hd523');
然後使用第三條SQL語句SELECT * FROM `test1` WHERE num2 = 10000;
進行查詢:
從結果可以看到,後面插入的三條數據也都匹配上了。那麼這個字元串隱式轉換的規則是什麼呢?為什麼num2='10000a'
、'010000'
和'10000'
這三種情形都能匹配上呢?查閱相關資料發現規則如下:
- 不以數字開頭的字元串都將轉換為
0
。如'abc'
、'a123bc'
、'abc123'
都會轉化為0
; - 以數字開頭的字元串轉換時會進行截取,從第一個字元截取到第一個非數字內容為止。比如
'123abc'
會轉換為123
,'012abc'
會轉換為012
也就是12
,'5.3a66b78c'
會轉換為5.3
,其他同理。
現對以上規則做如下測試驗證:
如此也就印證了之前的查詢結果了。
再次寫一條SQL查詢str1欄位:SELECT * FROM `test1` WHERE str1 = 1234;
分析和總結
通過上面的測試我們發現MySQL使用操作符的一些特性:
- 當操作符左右兩邊的數據類型不一致時,會發生隱式轉換。
- 當where查詢操作符左邊為數值類型時發生了隱式轉換,那麼對效率影響不大,但還是不推薦這麼做。
- 當where查詢操作符左邊為字元類型時發生了隱式轉換,那麼會導致索引失效,造成全表掃描效率極低。
- 字元串轉換為數值類型時,非數字開頭的字元串會轉化為
0
,以數字開頭的字元串會截取從第一個字元到第一個非數字內容為止的值為轉化結果。
所以,我們在寫SQL時一定要養成良好的習慣,查詢的欄位是什麼類型,等號右邊的條件就寫成對應的類型。特別當查詢的欄位是字元串時,等號右邊的條件一定要用引號引起來標明這是一個字元串,否則會造成索引失效觸發全表掃描。
碼海無涯,不進則退,日積跬步,以至千里。本博客所寫內容僅為個人在學習和研究MySQL過程中的一些心得體會及總結筆記,僅代表個人觀點。本次測試使用的MySQL版本是 5.7.26
,隨著MySQL版本的更新某些特性可能會發生改變,本文不代表所述觀點和結論於MySQL所有版本均準確無誤,版本差異請自行甄別。
首發地址:https://www.guitu18.com/post/2019/11/24/61.html