MySQL性能優化:MySQL中的隱式轉換造成的索引失效


數據庫優化是一個任重而道遠的任務,想要做優化必須深入理解數據庫的各種特性。在開發過程中我們經常會遇到一些原因很簡單但造成的后果卻很嚴重的疑難雜症,這類問題往往還不容易定位,排查費時費力最后發現是一個很小的疏忽造成的,又或者是因為不了解某個技術特性產生的。

於數據庫層面,最常見的恐怕就是索引失效了,且一開始因為數據量小還不易被發現。但隨着業務的拓展數據量的提升,性能問題慢慢的就體現出來了,處理不及時還很容易造成雪球效應,最終導致數據庫卡死甚至癱瘓。造成索引失效的原因可能有很多種,相關技術博客已經有太多了,今天我要記錄的是隱式轉換造成的索引失效


數據准備

首先使用存儲過程生成1000萬條測試數據,
測試表一共建立了7個字段(包括主鍵),num1num2保存的是和ID一樣的順序數字,其中num2是字符串類型。
type1type2保存的都是主鍵對5的取模,目的是模擬實際應用中常用類似type類型的數據,但是type2是沒有建立索引的。
str1str2都是保存了一個20位長度的隨機字符串,str1不能為NULLstr2允許為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分鍾才跑完(實際時間跟你電腦硬件配置有關)。這里貼幾條生成的數據,大致長這樣。

img

SQL測試

首發地址:https://www.guitu18.com/post/2019/11/24/61.html

先來看這組SQL,一共四條,我們的測試數據表num1int類型,num2varchar類型,但是存儲的數據都是跟主鍵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的執行計划數據:

img

可以看到,124三條SQL都能使用到索引,連接類型都為ref,掃描行數都為1,所以效率非常高。再看看第三條SQL,沒有用上索引,所以為全表掃描,rows直接到達1000萬了,所以性能差別才那么大。

仔細觀察你會發現,34兩條SQL查詢的字段num2varchar類型的,查詢條件等號右邊加引號的第4條SQL是用到索引的,那么是查詢的數據類型和字段數據類型不一致造成的嗎?如果是這樣那12兩條SQL查詢的字段num1int類型,但是第2條SQL查詢條件右邊加了引號為什么還能用上索引呢。

查閱MySQL相關文檔發現是隱式轉換造成的,看一下官方的描述:

官方文檔: 12.2 Type Conversion in Expression Evaluation

當操作符與不同類型的操作數一起使用時,會發生類型轉換以使操作數兼容。某些轉換是隱式發生的。例如,MySQL會根據需要自動將字符串轉換為數字,反之亦然。以下規則描述了比較操作的轉換方式:

  1. 兩個參數至少有一個是NULL時,比較的結果也是NULL,特殊的情況是使用<=>對兩個NULL做比較時會返回1,這兩種情況都不需要做類型轉換
  2. 兩個參數都是字符串,會按照字符串來比較,不做類型轉換
  3. 兩個參數都是整數,按照整數來比較,不做類型轉換
  4. 十六進制的值和非數字做比較時,會被當做二進制串
  5. 有一個參數是TIMESTAMPDATETIME,並且另外一個參數是常量,常量會被轉換為timestamp
  6. 有一個參數是decimal類型,如果另外一個參數是decimal或者整數,會將整數轉換為decimal后進行比較,如果另外一個參數是浮點數,則會把decimal轉換為浮點數進行比較
  7. 所有其他情況下,兩個參數都會被轉換為浮點數再進行比較

根據官方文檔的描述,我們的第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;進行查詢:

img

從結果可以看到,后面插入的三條數據也都匹配上了。那么這個字符串隱式轉換的規則是什么呢?為什么num2='10000a''010000''10000'這三種情形都能匹配上呢?查閱相關資料發現規則如下:

  1. 不以數字開頭的字符串都將轉換為0。如'abc''a123bc''abc123'都會轉化為0
  2. 以數字開頭的字符串轉換時會進行截取,從第一個字符截取到第一個非數字內容為止。比如'123abc'會轉換為123'012abc'會轉換為012也就是12'5.3a66b78c'會轉換為5.3,其他同理。

現對以上規則做如下測試驗證:

img

如此也就印證了之前的查詢結果了。

再次寫一條SQL查詢str1字段:SELECT * FROM `test1` WHERE str1 = 1234;

img

分析和總結

通過上面的測試我們發現MySQL使用操作符的一些特性:

  1. 當操作符左右兩邊的數據類型不一致時,會發生隱式轉換
  2. 當where查詢操作符左邊為數值類型時發生了隱式轉換,那么對效率影響不大,但還是不推薦這么做。
  3. 當where查詢操作符左邊為字符類型時發生了隱式轉換,那么會導致索引失效,造成全表掃描效率極低。
  4. 字符串轉換為數值類型時,非數字開頭的字符串會轉化為0,以數字開頭的字符串會截取從第一個字符到第一個非數字內容為止的值為轉化結果。

所以,我們在寫SQL時一定要養成良好的習慣,查詢的字段是什么類型,等號右邊的條件就寫成對應的類型。特別當查詢的字段是字符串時,等號右邊的條件一定要用引號引起來標明這是一個字符串,否則會造成索引失效觸發全表掃描。


碼海無涯,不進則退,日積跬步,以至千里。本博客所寫內容僅為個人在學習和研究MySQL過程中的一些心得體會及總結筆記,僅代表個人觀點。本次測試使用的MySQL版本是 5.7.26,隨着MySQL版本的更新某些特性可能會發生改變,本文不代表所述觀點和結論於MySQL所有版本均准確無誤,版本差異請自行甄別。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM