C-10.索引最佳化與查詢最佳化
都有那些維度可以進行資料庫調優?簡言之:
- 索引失效,沒有充分利用到索引 -- 索引建立
- 關聯查詢太多JOIN(設計缺陷或不得已的需求) -- SQL最佳化
- 伺服器調優及各個引數設定(緩衝,執行緒數等) -- 調整my.cnf
- 資料過多 -- 分庫分表
關於資料庫調優的知識點非常分散。不同的DBMS,不同的公司,不同的職位,不同的專案遇到的問題都不盡相同。
雖然SQL查詢最佳化的技術有很多,但是大方向上完全可以分成物理查詢最佳化
和邏輯查詢最佳化
兩大塊。
- 物理查詢最佳化是透過
索引
和表連線方式
等技術來進行最佳化,這裡重點需要掌握索引的使用。 - 邏輯查詢最佳化就是透過SQL
等價變換
提示查詢效率,直白一點就是說,換一種查詢寫法執行效率可能更高。
1.資料準備
學院表
50萬條,班級表1萬條。
步驟1,建表。
CREATE TABLE `class` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`className` VARCHAR(30) DEFAULT NULL,
`address` VARCHAR(40) DEFAULT NULL,
`monitor` INT NULL ,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `student` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`stuno` INT NOT NULL ,
`name` VARCHAR(20) DEFAULT NULL,
`age` INT(3) DEFAULT NULL,
`classId` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
#CONSTRAINT `fk_class_id` FOREIGN KEY (`classId`) REFERENCES `t_class` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
步驟2,設定引數。
set global log_bin_trust_function_creators=1;#命令開啟:允許建立函式設定
步驟3,建立函式。
#隨機產生字串
DELIMITER //
CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
BEGIN
DECLARE chars_str VARCHAR(100) DEFAULT
'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
DECLARE return_str VARCHAR(255) DEFAULT '';
DECLARE i INT DEFAULT 0;
WHILE i < n DO
SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
SET i = i + 1;
END WHILE;
RETURN return_str;
END //
DELIMITER ;
#用於隨機產生多少到多少的編號 隨機產生班級編號
DELIMITER //
CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11)
BEGIN
DECLARE i INT DEFAULT 0;
SET i = FLOOR(from_num +RAND()*(to_num - from_num+1)) ;
RETURN i;
END //
DELIMITER ;
步驟4:建立儲存過程
#建立往stu表中插入資料的儲存過程
DELIMITER //
CREATE PROCEDURE insert_stu( START INT , max_num INT )
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0; #設定手動提交事務
REPEAT #迴圈
SET i = i + 1; #賦值
INSERT INTO student (stuno, name ,age ,classId ) VALUES
((START+i),rand_string(6),rand_num(1,50),rand_num(1,1000));
UNTIL i = max_num
END REPEAT;
COMMIT; #提交事務
END //
DELIMITER ;
#假如要刪除
#drop PROCEDURE insert_stu;
#執行儲存過程,往class表新增隨機資料
DELIMITER //
CREATE PROCEDURE `insert_class`( max_num INT )
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO class ( classname,address,monitor ) VALUES
(rand_string(8),rand_string(10),rand_num(1,100000));
UNTIL i = max_num
END REPEAT;
COMMIT;
END //
DELIMITER ;
#假如要刪除
#drop PROCEDURE insert_class;
步驟5:呼叫儲存過程
#執行儲存過程,往class表新增1萬條資料
CALL insert_class(10000);
#執行儲存過程,往stu表新增50萬條資料
CALL insert_stu(100000,500000);
步驟6:建立刪除某表上索引儲存過程
#建立儲存過程
DELIMITER //
CREATE PROCEDURE `proc_drop_index`(dbname VARCHAR(200),tablename VARCHAR(200))
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE ct INT DEFAULT 0;
DECLARE _index VARCHAR(200) DEFAULT '';
DECLARE _cur CURSOR FOR SELECT index_name FROM
information_schema.STATISTICS WHERE table_schema=dbname AND table_name=tablename AND
seq_in_index=1 AND index_name <>'PRIMARY' ;
#每個遊標必須使用不同的declare continue handler for not found set done=1來控制遊標的結束
DECLARE CONTINUE HANDLER FOR NOT FOUND set done=2 ;
#若沒有資料返回,程式繼續,並將變數done設為2
OPEN _cur;
FETCH _cur INTO _index;
WHILE _index<>'' DO
SET @str = CONCAT("drop index " , _index , " on " , tablename );
PREPARE sql_str FROM @str ;
EXECUTE sql_str;
DEALLOCATE PREPARE sql_str;
SET _index='';
FETCH _cur INTO _index;
END WHILE;
CLOSE _cur;
END //
DELIMITER ;
#執行儲存過程
CALL proc_drop_index("dbname","tablename");
2.索引失效案例
MySQL中提高效能
的一個最有效的方式就是對資料表設計合理的索引
。索引提供了高效訪問資料的方法,並且加快查詢的速度,因此索引對查詢的速度有著至關重要的影響。
- 使用索引可以
快速定位
表中的某條資料,從而提高資料庫查詢的資料,提高資料庫的效能。 - 如果查詢沒有使用索引,查詢語句就會
掃描表中的所有記錄
。在資料量大的情況下,這樣查詢的速度會很慢。
大多數情況下(預設)採用B+樹
來構建索引。只是空間列型別的索引使用R-
樹,並且MEMORY表還支援hash索引
。
其實,用不用索引,最終都是最佳化器說了算。最佳化器是基於什麼的最佳化器?基於cost開銷(CostBaseOptimizer)
,它不是基於規則(Rule-BaseOptimizer)
,也不是基於語義
。怎樣開銷小,就怎麼來。另外,SQL語句是否使用索引,跟資料庫版本、資料量、資料選擇度都有關係。
2.1 全值匹配
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4;
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND NAME = 'abcd';
建立索引前執行
mysql> SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND NAME = 'abcd';
Empty set, 1 warning (0.09 sec)
建立索引
#建立索引
CREATE INDEX idx_age ON student(age);
CREATE INDEX idx_age_classid ON student(age,classId);
CREATE INDEX idx_age_classid_name ON student(age,classId,NAME);
建立索引後執行
mysql> SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND NAME = 'abcd';
Empty set, 1 warning (0.00 sec)
2.2 最佳左字首規則
在MySQL建立聯合索引時,會遵守最佳左字首匹配原則,即最左優先,在檢索資料時從聯合索引的最左邊開始匹配。
#1.只能使用上 idx_age索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name = 'abcd' ;
#2.不能使用上索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.classid=1 AND student.name = 'abcd';
#3.使用idx_age_classid_name索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student
WHERE classid=4 AND student.age=30 AND student.name = 'abcd';
原因分析:
因為在構建聯合索引對應的B+樹時,每個頁(包括葉子和非葉子)中,的一條資料儲存的列的值的順序,是定義索引的順序,所以只有where子句中,存在聯合索引的最左側的列,才可能使用的上該聯合索引。
對於上述第一條SQL的分析,為什麼只能用idx_age索引,也是同理,對於idx_age_classid_name索引,age列和name列中有個classid列,如果只出現了age列,和name列,就會導致中間斷了。
也就是說,聯合索引(a,b,c),最佳順序就是 a = ? and b = ? and c = ?。這樣是最完美的使用聯合索引的方式,但是隻有a,c的話,就會導致無法使用該索引。因為使用a = ? 確定幾條資料後,必須使用b列去過濾資料了,但是此時where子句中沒有b的條件,所以就無法使用此索引樹,去確定滿足後面條件的資料了,非要使用此索引的話,可能會導致回表次數過多,執行效率低。
對於第三條SQL,能使用上idx_age_classid_name索引的分析,因為有查詢最佳化器的存在,雖然在where子句中寫的條件的順序是,先classid 後age後name,但是查詢最佳化器,會做一個操作,嘗試將條件子句的順序,轉換成聯合索引定義的順序,從而完美的使用的聯合索引。
#刪除這兩個索引
DROP INDEX idx_age ON student;
DROP INDEX idx_age_classid ON student;
#再次執行該語句
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name = 'abcd' ;
#這裡不展示具體執行結果,直接說結論
#使用上了idx_age_classid_name索引,但是key_len的長度是5
#而age列是int型別,且可以為null 4 + 1剛好是5B
#也就是說,該條語句只使用了聯合索引的age列進行過濾,然後根據對應的id值,進行回表,在use where進行過濾。符合上面的原因分析
結論:MySQL可以為一張表的多個欄位建立索引,一個索引可以包括16個欄位。對於多列索引,過濾條件要使用索引必須按照索引的建立順序,依次滿足,一但跳過某個欄位,索引後面的欄位都無法使用。如果查詢條件中沒有使用這些欄位中第1欄位時,聯合索引將失效。也即不會被使用。
擴充套件Alibaba《Java開發手冊》
索引檔案具有B-Tree的最左匹配特性,如果左邊的之值未確定,那麼無法使用該索引。
2.3 主鍵插入順序
對於一個使用InnoDB儲存引擎的表來說,在我們沒有顯式的建立索引時,表中的資料實際上都是儲存在聚簇索引的葉子節點的。而記錄又是儲存在資料頁中的,資料頁和記錄又是按照記錄主鍵值從小到大的順序進行排序,所以如果我們插入的記錄的主鍵值是依次增大的話,那我們每插滿一個資料頁就換到下一個資料頁繼續插,而如果我們插入的主鍵值忽大忽小的話,就比較麻煩了,假設某個資料頁儲存的記錄已經滿了,它儲存的主鍵值在1~100之間:
如果此時再插入一條主鍵為9
的記錄,那它插入的位置就如下圖:
可這個資料頁已經滿了,再插進來咋辦呢?我們需要把當前頁面分裂
成兩個頁面,把本頁中的一些記錄移動到新建立的這個頁中。頁面分裂和記錄移位意味著什麼?意味著: 效能損耗 !所以如果我們想盡量避免這樣無謂的效能損耗,最好讓插入的記錄的主鍵值依次遞增
,這樣就不會發生這樣的效能損耗了。所以我們建議:讓主鍵具有AUTO_INCREMENT
,讓儲存引擎自己為表生成主鍵,而不是我們手動插入 ,
我們自定義的主鍵列 id 擁有AUTO_INCREMENT
屬性,在插入記錄時儲存引擎會自動為我們填入自增的主鍵值。這樣的主鍵佔用空間小,順序寫入,減少頁分裂。
2.4 計算、函式導致索引失效
CREATE INDEX idx_name ON student(`name`);
#此語句比下一條要好!(能夠使用上索引)
#執行結果使用上索引 idx_name
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%';
#未使用索引 type列的值是ALL 全表掃描
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc';
#原因,在使用完函式後,MySQL只能根據函式的結果,去和給定的值,對比。所以無法使用上索引
CREATE INDEX idx_sno ON student(stuno);
#未使用索引 type列的值是ALL 全表掃描
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno+1 = 900001;
#原因,在進行計算後,MySQL只能根據計算的結果,去和給定的值,對比。所以無法使用上索引
#執行結果使用上索引 idx_sno
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno = 900000;
2.5 型別轉換導致索引失效
#無法使用idx_name索引 type列是ALL 全表掃描
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME = 123;
#相當於,MySQL,對於型別不匹配的,會嘗試使用隱式的函式轉換成目標型別,這樣就會導致無法使用索引。
#使用idx_name索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME = '123';
2.6 範圍條件右邊的列索引失效
右邊,是指在聯合索引的列的右邊的列,而不是where子句中的右邊的列無法被使用。
SHOW INDEX FROM student;
#根據資料庫和表名刪除除主鍵外的索引
CALL proc_drop_index('atguigudb3','student');
#建立聯合索引
CREATE INDEX idx_age_classId_name ON student(age,classId,`name`);
#會使用上idx_age_classId_name 但是key_len的長度是10
#age和classId都是int型別,都可以為null 所以是 4 + 1 + 4 + 1=10B 也就是在聯合索引中,未使用上name列
#1.
EXPLAIN SELECT SQL_NO_CACHE * FROM student
WHERE student.age=30 AND student.classId > 20 AND student.name = 'abc' ;
#建立一個age,name,classId的索引
CREATE INDEX idx_age_name_cid ON student(age,`name`,classId);
#可以使用idx_age_name_cid索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student
WHERE student.age=30 AND student.name = 'abc' AND student.classId>20;
#此時執行第一條語句,也會使用idx_age_name_cid,因為mysql會自動改變條件條件子句的順序
應用開發中範圍查詢,例如:金額查詢,日期查詢往往都是範圍查詢。應該把查詢條件放在where語句最後。(建立的聯合索引中,務必把範圍涉及到的欄位寫在最後)
2.7 不等於(!= 或 <>)索引失效
CREATE INDEX idx_name ON student(NAME);
#不能使用idx_name索引 type列的值是ALL
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name <> 'abc' ;
#不能使用idx_name索引 type列的值是ALL
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name != 'abc' ;
2.8 is null可以使用索引,is not null無法使用索引
#可以使用idx_age_classId_name索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NULL;
#無法使用索引 type列的值是ALL
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NOT NULL;
結論:最好在設計資料表的時候就將欄位設定為 NOT NULL約束,比如你可以將INT型別的欄位預設值設定為0。將字元型別的預設值設定為空字串('‘)。
擴充:同理,在查詢中使用not like 也無法使用索引,導致全表掃描。
2.9 like以萬用字元%開頭索引失效
在使用LIKE關鍵字進行查詢的sql中,如果匹配字串"%"開頭的,索引就會失效。只有"%"不在第一個位置,索引才會起作用。
#使用idx_name索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME LIKE 'ab%';
#無法使用idx_name索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME LIKE '%ab%';
擴充:Alibaba《Java開發手冊》
【強制】頁面搜尋嚴禁左模糊或者全模糊,如果需要請走搜尋引擎來解決。
2.10 OR前後存在非索引的列,索引失效
在WHERE子句中,如果在OR前的條件列進行了索引,而在OR後的條件列沒有進行索引,那麼索引會失效。也就是說,OR前後的兩個條件中的列都是索引時,查詢中才使用索引。
因為OR的含義就是兩個只要滿足一個即可,因此只有一個條件列進行了索引是沒有意義的,只要有條件列沒有進行索引,就會進行全表掃描,因此索引的條件列也會失效。
查詢語句使用OR關鍵字的情況:
CALL proc_drop_index('atguigudb3','student');
CREATE INDEX idx_age ON student(age);
#type列的值是ALL
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR classid = 100;
CREATE INDEX idx_cid ON student(classid);
#建立該索引後,執行,type 值是index_merge索引合併
2.11 資料庫和表的字符集統一使用utf8mb4
統一使用utf8mb4( 5.5.3版本以上支援)相容性更好,統一字符集可以避免由於字符集轉換產生的亂碼。不同的字符集
進行比較前需要進行轉換
會造成索引失效。
2.12 建議
一般性建議:
-
對子單列索引,儘量選擇針對當前query過濾性更好的索引。
-
在選擇組合索引的時候,當前query中過濾性最好的欄位在索引欄位順序中,位置越靠前越好。
-
在選擇組合索引的時候,儘量選擇能夠包含當前query中的where子句中更多欄位的索引。
-
在選擇組合索引的時候,如果某個欄位可能出現範圍查詢時,儘量把這個欄位放在索引次序的最後面。
總之,書寫SQL語句時,儘量避免造成索引失效的情況。
3.關聯查詢最佳化
3.1 資料準備
#分類
CREATE TABLE IF NOT EXISTS `type` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
);
#圖書
CREATE TABLE IF NOT EXISTS `book` (
`bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`bookid`)
);
#向分類表中新增20條記錄
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
#向圖書表中新增20條記錄
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
3.2 左外連線
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card
指向結果看,type都是ALL
新增索引最佳化
CREATE INDEX idx_book_card ON book(card); #被驅動表建立索引,避免全表掃描
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
可以看到第二行的 type 變為了 ref,rows 也變成了1最佳化比較明顯。這是由左連線特性決定的。LEFT JOIN條件用於確定如何從右表搜尋行,左邊一定都有,所以右邊是我們的關鍵點,一定需要建立索引
。這是因為,在外連線中的特性是,左外連線中,左表是主表,左表中的資料是一定要儲存的,所以,就必須對左表進行全表掃描。而從表的連線欄位建立索引的話,就可以使用索引,去最佳化使用主表的資料,在從表中查詢的這一步驟。
CREATE INDEX idx_type_card ON `type`(card); #驅動表的連線列,建立索引
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
從結果看,type表雖然也使用了,索引,但是rows的行數是20,也就是說,還是相當於掃描了全表,不過使用索引最佳化了這一步。
DROP INDEX idx_book_card ON book;#移除被驅動表card列索引
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
被驅動表book變回了ALL全表掃描。
3.3 採用內連線
DROP INDEX idx_type_card ON type;#移除type表的card列索引
使用inner join,內連線,沒有主從表之分。由select查詢最佳化器自己根據查詢成本,選擇驅動表和被驅動表。
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;
CREATE INDEX idx_book_card ON book(card);#book表新增card列索引,最佳化
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card=book.card;
CREATE INDEX idx_type_card ON type(card);#type表新增card列索引,最佳化
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card=book.card;
DROP INDEX idx_type_card ON book;
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card=book.card;
結論:對於內連線來講,如果表的連線條件中只能有一個欄位有索引,則有索引的欄位所在的表會被作為被驅動表出現。
#再次新增book表card列的索引
CREATE INDEX idx_book_card ON book(card);
#向type表中新增資料(20條資料)
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO `type`(card) VALUES(FLOOR(1 + (RAND() * 20)));
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card = book.card;
結論:對於內連線來說,在兩個表的連線條件都存在索引的情況下,會選擇小表作為驅動表。“小表驅動大表”。
3.4 JOIN語句原理
join方式連線多個表,本質就是各個表之間資料的迴圈匹配。MySQL5.5版本之前,MySQL只支援一種表間關聯方式,就是巢狀迴圈(Nested Loop Join)。如果關聯表的資料量很大,則join關聯的執行時間會非常長。在MySQL5.5以後的版本中,MySQL透過引入BNLJ演算法來最佳化巢狀執行。
1.驅動表和被驅動表
- 內連線
SELECT * FROM A JOIN B ON ...
A一定是驅動表嗎?不一定,最佳化器會根據你查詢語句做最佳化,決定先查哪張表。先查詢的表就是驅動表,反之就是被驅動表。使用explain關鍵字檢視。
- 外連線
SELECT * FROM A LEFT JOIN B ON ...
#或
SELECT * FROM A RIGHT JOIN B ON ...
#4)JOIN的底層原理
CREATE TABLE a(f1 INT, f2 INT, INDEX(f1))ENGINE=INNODB;
CREATE TABLE b(f1 INT, f2 INT)ENGINE=INNODB;
INSERT INTO a VALUES(1,1),(2,2),(3,3),(4,4),(5,5),(6,6);
INSERT INTO b VALUES(3,3),(4,4),(5,5),(6,6),(7,7),(8,8);
#測試1
EXPLAIN SELECT * FROM a LEFT JOIN b ON(a.f1=b.f1) WHERE (a.f2=b.f2);
結論,從結果看,b是驅動表,a是被驅動表。這是底層將sql語句改寫成內連線,這是因為WHERE (a.f2=b.f2) a,b都是隻有兩個欄位,都相等就是內連線。所以,外連線也不一定主表就是驅動表。當然很少上述這種情況。
2.Simple Nested-Loop Join(簡單巢狀迴圈連線)
在連線條件上都無索引的情況下,演算法相當簡單,從表A中取一條資料,遍歷B表,將匹配成功的記錄,當到臨時表,以此類推,驅動表A的每一張表與被驅動表進行判斷。
這種方式效率很低,上述表A資料100條,B資料1000條計算,A*B=10萬次。
A代表A表的條數,B代表B表的條數(條數是指,滿足ON條件且滿足WHERE條件的行數)
開銷統計 | SNLJ |
---|---|
外表掃描次數 | 1 |
內表掃描次數 | A |
讀取記錄數 | A + B*A |
JOIN比較次數 | B * A |
回表讀取記錄次數 | 0 |
當然mysql不會使用這中方式進行表的連線,後面出現了Nested-Loop Join最佳化演算法。
3.Index Nested-Loop Join(索引巢狀迴圈連線)
Index Nested-Loop Join其最佳化的思路主要是為了減少內層表資料的匹配次數
,所以要求被驅動表上的連線條件列上必須有索引
才行。透過外層表匹配條件直接與內層表索引進行匹配,避免和內層表的每條記錄去進行比較,這樣極大的減少了對內層表的匹配次數。
驅動表中的每條記錄透過被驅動表的索引進行訪問,因為索引查詢的成本是比較固定的,故mysql最佳化器都傾向於使用記錄數少的表作為驅動表(外表)。
A代表A表的條數,B代表B表的條數(條數是指,滿足ON條件且滿足WHERE條件的行數)
開銷統計 | SNLJ | INLJ |
---|---|---|
外表掃描次數 | 1 | 1 |
內表掃描次數 | A | 0 |
讀取記錄數 | A + B*A | A + B(匹配) |
JOIN比較次數 | B * A | A*Index(索引數的層數) |
回表讀取記錄次數 | 0 | B(匹配的記錄條數) |
4.Block Nested-Loop Join(塊巢狀迴圈連線)
如果存在索引,那麼會使用index的方式進行join,如果join的列沒有索引,被驅動表要掃描的次數太多了。每次訪問被驅動表,其表中的記錄都會被載入到記憶體中,然後再從驅動表中取一條與其匹配,匹配結束後清除記憶體,然後再從驅動表中載入一條記錄,然後把被驅動表的記錄在載入到記憶體匹配,這樣週而復始,大大增加了IO的次數。為了減少被驅動表的IO次數,就出現了Block Nested-Loop Join的方式。
不再是逐條獲取驅動表的資料,而是一塊一塊的獲取,引入了join buffer緩衝區
,將驅動表join相關的部分資料列(大小受join buffer的限制)快取到join buffer中,然後全表掃描被驅動表,被驅動表的每一條記錄一次性和join buffer中的所有驅動表記錄進行匹配(記憶體中操作),將簡單巢狀迴圈中的多次比較合併成一次,降低了被驅動表的訪問頻率。
注意:
這裡快取的不只是關聯表的列, select後面的列也會快取起來。
在一個有N個join關聯的sql中會分配N-1個join buffer。所以查詢的時候儘量減少不必要的欄位,可以讓join buffer中可以存放更多的列。
驅動表中的每條記錄透過被驅動表的索引進行訪問,因為索引查詢的成本是比較固定的,故mysql最佳化器都傾向於使用記錄數少的表作為驅動表(外表)。
A代表A表的條數,B代表B表的條數(條數是指,滿足ON條件且滿足WHERE條件的行數)
開銷統計 | SNLJ | INLJ | BNLJ |
---|---|---|---|
外表掃描次數 | 1 | 1 | 1 |
內表掃描次數 | A | 0 | (A * used_column_size) / join_buffer_size + 1(如果能整除不加1) |
讀取記錄數 | A + B*A | A + B(匹配) | A + B * (A * used_column_size / join_buffer_size) |
JOIN比較次數 | B * A | A*Index(索引數的層數) | B * A |
回表讀取記錄次數 | 0 | B(匹配的記錄條數) | 0 |
引數設定
- block_nested_loop
SHOW VARIABLES LIKE '%optimizer_switch%';#指令檢視預設是開啟的
- join_buffer_size
驅動表能不能一次載入完,要看join buffer能不能儲存所有的資料,預設情況下join_buffer_size=256k
。
mysql> SHOW VARIABLES LIKE 'join_buffer_size';
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| join_buffer_size | 262144 |
+------------------+--------+
1 row in set (0.01 sec)
join_buffer_size的最大值在32位系統可以申請4G,而在64位操做系統下可以申請大於4G的Join Buffer空間(64位Windows除外,其大值會被截斷為4GB併發出警告)。
5.Join小結
1、整體效率比較:INLJ > BNLJ > SNLJ
2、永遠用小結果集驅動大結果集(其本質就是減少外層迴圈的資料數量) (小的度量單位指的是錶行數*每行大小)
select t1.b, t2.* from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=100;#推薦
#straight_join查詢最佳化器不對主表和從表做修改,左邊的一定是驅動表
select t1.b, t2.* from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=100;#不推薦
#上述原因,是因為選擇了查詢的列中,t1表只用了b列,而t2表用來所有的列,
#選擇t1做驅動表的話,通用的join_buffer_size大小下,儲存的t1表的條數就多,記憶體迴圈,也即是查詢被驅動表的次數就少
3、為被驅動表匹配的條件增加索引(減少內層表的迴圈匹配次數)
4、增大join buffer size的大小(一次快取的資料越多,那麼內層包的掃表次數就越少)
5、減少驅動表不必要的欄位查詢(欄位越少,join buffer所快取的資料就越多)
6.Hash Join
從MySQL的8.0.20版本開始將廢棄BNLJ,因為從MySQL8.0.18版本開始就加入了hash join預設都會使用hash join
- Nested Loop:
對於被連線的資料子集較小的情況,Nested Loop是個較好的選擇。 - Hash Join是做大資料集連線時的常用方式,最佳化器使用兩個表中較小(相對較小)的表利用Join Key在記憶體中建立雜湊表,然後掃描較大的表並探測雜湊表,找出與Hash表匹配的行。
- 這種方式適用於較小的表完全可以放於記憶體中的情況,這樣總成本就是訪問兩個表的成本之和。
- 在表很大的情況下並不能完全放入記憶體,這時最佳化器會將它分割成
若干不同的分割槽
,不能放入記憶體的部分就把該分割槽寫入磁碟的臨時段,此時要求有較大的臨時段從而儘量提高I/O的效能。 - 它能夠很好的工作於沒有索引的大表和並行查詢的環境中,並提供最好的效能。大多數人都說它是Join的重型升降機。Hash Join只能應用於等值連線(如WHERE A.COL1=B.COL2),這是由Hash的特點決定的。
3.5 小結
- 保證被驅動表的JOIN欄位已經建立了索引。
- 需要JOIN的欄位,資料型別保持絕對一致。
- LEFT JOIN時,選擇將小表作為驅動表,
大表作為被驅動表
。減少外層迴圈的次數。 - INNER JOIN時,MySQL會自動將
小結果集的表選為驅動表
。選擇相信MySQL的最佳化策略。 - 能夠直接多表關聯的儘量直接關聯,不用子查詢。(減少查詢的次數)
- 不建議使用子查詢,建議將子查詢SQL拆開結合程式多次查詢,或使用JOIN來替代子查詢。
- 衍生表建不了索引。
4.子查詢最佳化
MySQL從4.1版本開始支援子查詢,使用子查詢可以進行SELECT語句的巢狀查詢,即一個SELECT查詢的結果作為另一個SELECT語句的條件。子查詢可以一次性完成很多邏輯上需要多個步驟才能完成的SQL操作
。
子查詢是 MySQL 的一項重要的功能,可以幫助我們透過一個 SQL 語句實現比較複雜的查詢。但是,子查詢的執行效率不高。
原因:
① 執行子查詢時,MySQL需要為內層查詢語句的查詢結果建立一個臨時表
,然後外層查詢語句從臨時表中查詢記錄。查詢完畢後,再撤銷這些臨時表
。這樣會消耗過多的CPU和IO資源,產生大量的慢查詢。
② 子查詢的結果集儲存的臨時表,不論是記憶體臨時表還是磁碟臨時表都不會存在索引
,所以查詢效能會受到一定的影響。
③ 對於返回結果集比較大的子查詢,其對查詢效能的影響也就越大。
在MySQ中,可以使用連線(JOIN)查詢來替代子查詢。連線查詢不需要建立臨時表
,其速度比子查詢要快
,如果查詢中使用索引的話,效能就會更好。
#建立班級表中班長的索引
CREATE INDEX idx_monitor ON class(monitor);
#查詢班長的資訊
EXPLAIN SELECT * FROM student a
WHERE a.`stuno` IN (
SELECT monitor
FROM class c
WHERE monitor IS NOT NULL
);
#子查詢轉成多表聯查
EXPLAIN SELECT a.* FROM student a JOIN class c
ON a.`stuno` = c.`monitor`
WHERE c.`monitor` IS NOT NULL;
#查詢不為班長的資訊
EXPLAIN SELECT * FROM student stu1
WHERE stu1.`stuno` NOT IN (
SELECT monitor
FROM class c
WHERE monitor IS NOT NULL
);
EXPLAIN SELECT SQL_NO_CACHE a.* FROM student a LEFT OUTER JOIN class b
ON a.`stuno` = b.`monitor`
WHERE b.`monitor` IS NULL;
結論:儘量不要使用NOT IN或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xxx IS NULL替代
5.排序最佳化
5.1 排序最佳化
問題:在WHERE條件欄位上加索引但是為什麼在ORDER BY欄位上還要加索引呢?
回答:
在MySQL中,支援兩種排序方式,分別是FileSort
和Index
排序。
- lndex排序中,索引可以保證資料的有序性,不需要再進行排序,效率更高。
- FileSort 排序則一般在記憶體中進行排序,佔用CPU較多。如果待排結果較大,會產生臨時檔案I/O到磁碟進行排序的情況,效率較低。
最佳化建議:
- 1.SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中
避免全表掃描
,在 ORDER BY 子句避免使用 FileSort 排序
。當然,某些情況下全表掃描,或者 FileSort 排序不一定比索引慢。但總的來說,我們還是要避免,以提高查詢效率。 - 2.儘量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 後面是相同的列就使用單索引列;如果不同就使用聯合索引。
- 3.無法使用 Index 時,需要對
FileSort
方式進行調優。
5.2 測試
#刪除class表和student表的非主鍵索引
CALL proc_drop_index('atguigudb3','class');
CALL proc_drop_index('atguigudb3','student');
SHOW INDEX FROM class;
SHOW INDEX FROM student;
#過程一:
#無索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid;
#無索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid LIMIT 10;
#過程二:order by時不limit,索引失效
#建立索引
CREATE INDEX idx_age_classid_name ON student (age,classid,NAME);
#不限制,索引失效 是因為,沒有limit限制,對錶的每一條資料都需要回表
#MySQL最佳化器覺得使用上索引 成本太高,不如filesort
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid;
#覆蓋索引 不用回表 MySQL最佳化器覺得使用上索引 成本低
#EXPLAIN SELECT SQL_NO_CACHE age,classid FROM student ORDER BY age,classid;
#增加limit過濾條件,使用上索引了。只用對10條,回表,最佳化器覺得成本低,可以使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age,classid LIMIT 10;
#過程三:order by時順序錯誤,索引失效
#建立索引age,classid,stuno
CREATE INDEX idx_age_classid_stuno ON student (age,classid,stuno);
#以下哪些索引失效?
EXPLAIN SELECT * FROM student ORDER BY classid LIMIT 10;#×
EXPLAIN SELECT * FROM student ORDER BY classid,NAME LIMIT 10; #×
EXPLAIN SELECT * FROM student ORDER BY age,classid,stuno LIMIT 10;#√
EXPLAIN SELECT * FROM student ORDER BY age,classid LIMIT 10;#√
EXPLAIN SELECT * FROM student ORDER BY age LIMIT 10;#√
#過程四:order by時規則不一致, 索引失效 (順序錯,不索引;方向反,不索引)
EXPLAIN SELECT * FROM student ORDER BY age DESC, classid ASC LIMIT 10;#×
EXPLAIN SELECT * FROM student ORDER BY classid DESC, NAME DESC LIMIT 10;#×
EXPLAIN SELECT * FROM student ORDER BY age ASC,classid DESC LIMIT 10;#×
#這個可以使用上索引,因為在索引樹中都是asc,而order by中都是降序的,反而可以使用上,倒著查詢索引即可了
EXPLAIN SELECT * FROM student ORDER BY age DESC, classid DESC LIMIT 10;
#過程五:無過濾,不索引
#先過濾,在排序,即便組合索引中的列,在order by中,但是可能也是用不上
#這是因為最佳化器覺得,age=const(常量)過濾後,就只有很少的行資料,再進行索引的查詢和回表,還不如
#直接filesort
#這裡,本來應該使用上索引idx_age_classid_name 但是再explain的結果中key_len列是5
#即是,只是用到了age列的索引,因為正常age 是可以為null索引長度4 + 1 + classid可以為null + 4 + 1
# 加上name列可以為null 20 * 3 + 1 + 2(變長欄位,描述真實的長度資訊的位元組空間) = 4 +1 + 4 +1 + 63 = 73
EXPLAIN SELECT * FROM student WHERE age = 45 ORDER BY classid;
EXPLAIN SELECT * FROM student WHERE age = 45 ORDER BY classid,NAME;
#使用不上索引
EXPLAIN SELECT * FROM student WHERE classid = 45 ORDER BY age;
#使用上了索引idx_age_classid_name 原因同上,不加limit需要回表太多,不如全表掃描
EXPLAIN SELECT * FROM student WHERE classid = 45 ORDER BY age LIMIT 10;
CREATE INDEX idx_cid ON student(classid);
EXPLAIN SELECT * FROM student WHERE classid=45 ORDER BY age;
#實戰:測試filesort和index排序
CALL proc_drop_index('atguigudb3','student');
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
#方案一: 為了去掉filesort我們可以把索引建成
CREATE INDEX idx_age_name ON student(age,NAME);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
#方案二:
CREATE INDEX idx_age_stuno_name ON student(age,stuno,NAME);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
DROP INDEX idx_age_stuno_name ON student;
CREATE INDEX idx_age_stuno ON student(age,stuno);
#以上只是sql語句和對於執行計劃的解釋,並未展示結果,
5.3 案例實戰
#實戰:測試filesort和index排序
CALL proc_drop_index('atguigudb3','student');
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
#方案一: 為了去掉filesort我們可以把索引建成
CREATE INDEX idx_age_name ON student(age,NAME);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
#方案二:
CREATE INDEX idx_age_stuno_name ON student(age,stuno,NAME);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
DROP INDEX idx_age_stuno_name ON student;
CREATE INDEX idx_age_stuno ON student(age,stuno);
結論:
兩個索引同時存在,mysql自動選擇最優的方案。(對於這個例子,mysql選擇idx_age_stuno_name)。但是, 隨著資料量的變化,選擇的索引也會隨之變化的 。
當【範圍條件】和【group by或者order by】的欄位出現二選一時,優先觀察條件欄位的過濾數量,如果過濾的資料足夠多,而需要排序的資料並不多時,優先把索引放在範圍欄位上。反之,亦然。
5.4 filesort演算法:雙路排序和單路排序
filesort有兩種演算法
雙路排序 (慢)
MySQL 4.1之前是使用雙路排序
,字面意思就是兩次掃描磁碟,最終得到資料, 讀取行指標和order by列 ,對他們進行排序,然後掃描已經排序好的列表,按照列表中的值重新從列表中讀取對應的資料輸出。- 從磁碟取排序欄位,在buffer進行排序,再從
磁碟取其他欄位
。
單路排序 (快)
從磁碟讀取查詢需要的所有列
,按照order by列在buffer對它們進行排序,然後掃描排序後的列表進行輸出, 它的效率更快一些,避免了第二次讀取資料。並且把隨機IO變成了順序IO,但是它會使用更多的空間, 因為它把每一行都儲存在記憶體中了。
結論及引申出的問題
-
由於單路是後出的,總體而言好過雙路
-
但是用單路有問題
-
在sort_buffer中,單路比多路要
多佔用很多空間
,因為單路是把所有欄位都取出,所以有可能取出的資料的總大小超出了sort_buffer
的容量,導致每次只能取sort_buffer容量大小的資料,進行排序(建立tmp檔案,多路合併),排完再取sort_buffer容量大小,再排......從而多次l/O。 -
單路本來想省一次l/o操作,
反而導致了大量的I/0操作
,反而得不償失。
-
最佳化策略
1.嘗試提高sort_buffer_size
- 不管用哪種演算法,提高這個引數都會提高效率,要根據系統的能力去提高,因為這個引數是針對每個程序(connection)的1M-8M之間調整。MySQL5.7,InnoDB儲存引擎預設值是1048576位元組,1MB
mysql> show variables like 'sort_buffer_size';
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| sort_buffer_size | 262144 |
+------------------+--------+
1 row in set (0.01 sec)
2.嘗試提高max_length_for_sort_data
- 提高這個引數,會增加用單路排序的機率。
mysql> show variables like 'max_length_for_sort_data';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| max_length_for_sort_data | 4096 |
+--------------------------+-------+
1 row in set (0.00 sec)
- 但是如果設的太高,資料總容量超出sort_buffer_size的機率就增大,明顯症狀是高的磁碟Io活動和低的處理器使用率。如果需要返回的列的總長度大於max_length_for_sort_data,使用雙路演算法,否則使用單路演算法。1024-8192位元組之間調整
3. Order by 時select * 是一個大忌。最好只Query需要的欄位。
- 當Query的欄位大小總和小於
max_length_for_sort_data
,而且排序欄位不是TEXTIBLOB型別時,會用改進後的演算法――單路排序,否則用老演算法――多路排序。 - 兩種演算法的資料都有可能超出sort_buffer_size的容量,超出之後,會建立tmp檔案進行合併排序導致多次I/O,但是用單路排序演算法的風險會更大一些,所以要
提高sort_buffer_size
。
6.GROUP BY最佳化
- group by 使用索引的原則幾乎跟order by一致 ,group by 即使沒有過濾條件用到索引,也可以直接使用索引。
- group by 先排序再分組,遵照索引建的最佳左字首法則。
- 當無法使用索引列,增大
max_length_for_sort_data
和sort_buffer_size
引數的設定。 - where效率高於having,能寫在where限定的條件就不要寫在having中了。
- 減少使用order by,和業務溝通能不排序就不排序,或將排序放到程式端去做。Order by、groupby、distinct這些語句較為耗費CPU,資料庫的CPU資源是極其寶貴的。
- 包含了order by、group by、distinct這些查詢的語句,where條件過濾出來的結果集請保持在1000行以內,否則SQL會很慢。
7.最佳化分頁查詢
一般分頁查詢時,透過建立覆蓋索引能夠比較好地提高效能。一個常見又非常頭疼的問題就是limit 2000000,10,此時需要MySQL排序前2000000-2000010的記錄,其他記錄丟失,查詢排序的代價非常大。
mysql> explain select * from student limit 2000000,10;
最佳化思路一
在索引上完成排序分頁操作,最後根據主鍵關聯回原表查詢所需要的其他列內容
explain select * from student t,(select id from student order by id limit 2000000,10) a
where t.id = a.id;
最佳化思路二
該方案適用於主鍵自增的表,可以把Limit查詢轉換成某個位置的查詢。
EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;
8.優先使用覆蓋索引
8.1 什麼是覆蓋索引?
理解方式一:索引是高效找到行的一種方法,但是一般資料庫也能使用索引找到一個列的資料,因此它不必讀取整個行。畢竟索引葉子節點儲存了它們索引的資料;當能透過讀取索引可以得到想要的資料,就不需要讀取行了。一個索引包含了滿足查詢結果的資料就叫做覆蓋索引。
索引方式二:非聚簇索引的一種形式,它包括在查詢裡的SELECT,JOIN和WHERE子句用到的所有列(建索引的欄位正好是覆蓋查詢條件中所設計的欄位)。
簡單說,就是索引列+主鍵
包含SELECT到FROM之間查詢的列
。
舉例一
#刪除之間的索引
CALL proc_drop_index('atguigudb3','student');
CREATE INDEX idx_age_name ON student(age,`name`);
EXPLAIN SELECT * FROM student WHERE age != 20;
#上面查詢失效案例,使用!=會導致索引失效,但從結果看,也使用了索引。這是因為覆蓋索引現象
#這樣也可以看出,上面講的只是一般情況下的基本規則,方便理解查詢最佳化器,但是也有特殊情況,
#因為是否使用索引,是由查詢最佳化器基於成本的計算來選擇的,所以要具體情況,具體分析。當然最好是,在業務程式碼中使用
#sql語句時,先explain檢視一下。
EXPLAIN SELECT age,name,id FROM student WHERE age != 20;
舉例二
#舉例2
EXPLAIN SELECT * FROM student WHERE NAME LIKE '%abc';
#覆蓋索引
EXPLAIN SELECT id,age,NAME FROM student WHERE NAME LIKE '%abc';
8.2 覆蓋索引的利弊
好處:
1. 避免Innodb表進行索引的二次查詢(回表)
Innodb是以聚集索引的順序來儲存的,對於Innodb來說,二級索引在葉子節點中所儲存的是行的主鍵資訊,如果是用二級索引查詢資料,在查詢到相應的鍵值後,還需透過主鍵進行二次查詢才能獲取我們真實所需要的資料。
在覆蓋索引中,二級索引的鍵值中可以獲取所要的資料,避免了對主鍵的二次查詢
,減少了IO操作
,提升了查詢效率。
2. 可以把隨機IO變成順序IO加快查詢效率
由於覆蓋索引是按鍵值的順序儲存的,對於IO密集型的範圍查詢來說,對比隨機從磁碟讀取每一行的資料IO要少的多,因此利用覆蓋索引在訪問時也可以把磁碟的隨機讀取的IO
轉變成索引查詢的順序IO
。
由於覆蓋索引可以減少樹的搜尋次數,顯著提升查詢效能,所以使用覆蓋索引是一個常用的效能最佳化手段。
弊端:
索引欄位的維護
總是有代價的。因此,在建立冗餘索引來支援覆蓋索引時就需要權衡考慮了。這是業務DBA,或者稱為業務資料架構師的工作。
9.如何給字串新增索引
有一張教師表,表定義如下:
create table teacher(
ID bigint unsigned primary key,
email varchar(64),
...
)engine=innodb;
教師要使用郵箱登入,所以業務程式碼中一定會出現類似於這樣的語句:
mysql> select col1, col2 from teacher where email='xxx';
如果email這個欄位上沒有索引,那麼這個語句就只能做 全表掃描 。
9.1 字首索引
MySQL是支援字首索引的。預設地,如果你建立索引的語句不指定字首長度,那麼索引就會包含整個字串。
mysql> alter table teacher add index index1(email);
#或
mysql> alter table teacher add index index2(email(6));
這兩種不同的定義在資料結構和儲存上有什麼區別呢?下圖就是這兩個索引的示意圖。
如果使用的是index1(即email整個字串的索引結構),執行順序是這樣的:
1.從index1索引樹找到滿足索引值是’ zhangssxyz@xxx.com ’的這條記錄,取得ID2的值;
2.到主鍵上查到主鍵值是ID2的行,判斷email的值是正確的,將這行記錄加入結果集;
3.取index1索引樹上剛剛查到的位置的下一條記錄,發現已經不滿足email=' zhangssxyz@xxx.com ’的條件了,迴圈結束。
這個過程中,只需要回主鍵索引取一次資料,所以系統認為只掃描了一行。
如果使用的是index2(即email(6)索引結構),執行順序是這樣的:
1.從index2索引樹找到滿足索引值是’zhangs’的記錄,找到的第一個是ID1;
2.到主鍵上查到主鍵值是ID1的行,判斷出email的值不是’ zhangssxyz@xxx.com ’,這行記錄丟棄;
3.取index2上剛剛查到的位置的下一條記錄,發現仍然是’zhangs’,取出ID2,再到ID索引上取整行然後判斷,這次值對了,將這行記錄加入結果集;
4.重複上一步,直到在idxe2上取到的值不是’zhangs’時,迴圈結束。
也就是說使用字首索引,定義好長度,就可以做到既節省空間,又不用額外增加太多的查詢成本。前面已經講過區分度,區分度越高越好。因為區分度越高,意味著重複的鍵值越少。見第八章,索引的建立和設計原則中的3.2小結中的9和10
9.2 字首索引對覆蓋索引的影響
結論:
使用字首索引就用不上覆蓋索引對查詢效能的最佳化了,這也是你在選擇是否使用字首索引時需要考慮的一個因素。
10.索引條件下推(索引下推)
Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一種在儲存引擎層使用索引過濾資料的一種最佳化方式。ICP可以減少儲存引擎訪問基表的次數以及MySQL伺服器訪問儲存引擎的次數。
10.1 使用前後的掃描過程
在不使用ICP索引掃描的過程:
storage層:只將滿足index key條件的索引記錄對應的整行記錄取出,返回給server層
server 層:對返回的資料,使用後面的where條件過濾,直至返回最後一行。
使用ICP掃描的過程:
- storage層:
首先將index key條件滿足的索引記錄區間確定,然後在索引上使用index filter進行過濾。將滿足的index filter條件的索引記錄才去回表取出整行記錄返回server層。不滿足index filter條件的索引記錄丟棄,不回表、也不會返回server層。
- server 層:
對返回的資料,使用table filter條件做最後的過濾。
使用前後的成本差別
使用前,儲存層多返回了需要被index filter過濾掉的整行記錄
使用ICP後,直接就去掉了不滿足index filter條件的記錄,省去了他們回表和傳遞到server層的成本。
ICP的 加速效果 取決於在儲存引擎內透過 ICP篩選 掉的資料的比例。
注意,索引條件下推,一般是用於組合索引中,就是在同一個索引樹上,索引查詢時,只是使用了部分索引,但是where條件中,還使用了索引樹中的其他列,進行條件過濾,這時就先過濾條件,不直接根據部分索引的查詢結果,進行回表操作。過濾條件執行後,滿足的再進行回表操作。
10.2 ICP的使用條件
① 只能用於二級索引(secondary index)
②explain顯示的執行計劃中type值(join 型別)為range
、ref
、eq_ref
或者ref_or_null
。
③ 並非全部where條件都可以用ICP篩選,如果where條件的欄位不在索引列中,還是要讀取整表的記錄到server端做where過濾。
④ ICP可以用於MyISAM
和InnnoDB
儲存引擎
⑤ MySQL 5.6版本的不支援分割槽表的ICP功能,5.7版本的開始支援。
⑥ 當SQL使用覆蓋索引時,不支援ICP最佳化方法。因為這種情況下使用ICP不能較少IO。
⑦相關子查詢的條件不能使用ICP
10.3 開啟和關閉索引下推
set optimizer_switch = 'index_condition_pushdown=on'#開啟,關閉是off 預設是開啟狀態 不要關閉,因為確實會最佳化查詢效率
不在舉例,宋紅康老師,課程中有舉例,但我覺得,理解這個最佳化的設計思想就可以了。
11.普通索引 VS 唯一索引
從效能的角度考慮,你選擇唯一索引還是普通索引呢?選擇的依據是什麼呢?
假設,我們有一個主鍵列為ID的表,表中有欄位k,並且在k上有索引,假設欄位 k 上的值都不重複。這個表的建表語句是:
mysql> create table test(
id int primary key,
k int not null,
name varchar(16),
index (k)
)engine=InnoDB;
表中R1~R5的(ID,k)值分別為(100,1)、(200,2)、(300,3)、(500,5)和(600,6)。
11.1 查詢過程
假設,執行查詢的語句是 select id from test where k=5。
- 對於普通索引來說,查詢到滿足條件的第一個記錄(5,500)後,需要查詢下一個記錄,直到碰到第一個不滿足k=5條件的記錄。
- 對於唯一索引來說,由於索引定義了唯一性,查詢到第一個滿足條件的記錄後,就會停止繼續檢索。
那麼,這個不同帶來的效能差距會有多少呢?答案是,微乎其微
。
11.2 更新過程
為了說明普通索引和唯一索引對更新語句效能的影響這個問題,介紹一下change buffer。
當需要更新一個資料頁時,如果資料頁在記憶體中就直接更新,而如果這個資料頁還沒有在記憶體中的話,在不影響資料一致性的前提下, InooDB會將這些更新操作快取在change buffer中
,這樣就不需要從磁碟中讀入這個資料頁了。在下次查詢需要訪問這個資料頁的時候,將資料頁讀入記憶體,然後執行changebuffer中與這個頁有關的操作。透過這種方式就能保證這個資料邏輯的正確性。
將change buffer中的操作應用到原資料頁,得到最新結果的過程稱為merge
。除了訪問這個資料頁
會觸發merge外,系統有後臺執行緒會定期
merge。在資料庫正常關閉(shutdown)
的過程中,也會執行merge操作。
如果能夠將更新操作先記錄在change buffer,減少讀磁碟
,語句的執行速度會得到明顯的提升。而且,資料讀入記憶體是需要佔用buffer pool
的,所以這種方式還能夠避免佔用記憶體
,提高記憶體利用率。
唯一索引的更新就不能使用change buffer
,實際上也只有普通索引可以使用。
11.3 change buffer的使用場景
- 1.普通索引和唯一索引應該怎麼選擇?其實,這兩類索引在查詢能力上是沒差別的,主要考慮的是對
更新效能
的影響。所以,建議你儘量選擇普通索引
。 - 2.在實際使用中會發現,
普通索引
和change buffer
的配合使用,對於資料量大
的表的更新最佳化還是很明顯的。 - 3.如果所有的更新後面,都馬上
伴隨著對這個記錄的查詢
,那麼你應該關閉change buffer
。而在其他情況下,change buffer都能提升更新效能。 - 4.由於唯一索引用不上change buffer的最佳化機制,因此如果
業務可以接受
,從效能角度出發建議優先考慮非唯一索引。但是如果"業務可能無法確保"的情況下,怎麼處理呢? - 首先,
業務正確性優先
。我們的前提是“業務程式碼已經保證不會寫入重複資料”的情況下,討論效能問題。如果業務不能保證,或者業務就是要求資料庫來做約束,那麼沒得選,必須建立唯一索引。這種情況下,本節的意義在於,如果碰上了大量插入資料慢、記憶體命中率低的時候,給你多提供一個排查思路。 - 然後,在一些“
歸檔庫
”的場景,你是可以考慮使用唯一索引的。比如,線上資料只需要保留半年,然後歷史資料儲存在歸檔庫。這時候,歸檔資料已經是確保沒有唯一鍵衝突了。要提高歸檔效率,可以考慮把表裡面的唯一索引改成普通索引。
阿里開發規範
【強制】業務上具有唯一特性的欄位,即使是多個欄位的組合,也必須建成唯一索引。
說明:不要以為唯一索引影響了 insert 速度,這個速度損耗可以忽略,但提高查詢速度是明顯的;另外,即使在應用層做了非常完善的校驗控制,只要沒有唯一索引,根據墨菲定律,必然有髒資料產生。
12.其他最佳化策略
12.1 EXISTS 和 IN 的區分
問題
不太理解哪種情況下應該使用EXISTS,哪種情況應該用IN。選擇的標準是看能否使用表的索引嗎?
回答
索引是個前提,其實選擇與否還是要看錶的大小。你可以將選擇的標準理解為小表驅動大表
。在這種方式下效率是最高的。
舉例
SELECT * FROM A WHERE cc IN ( SELECT cc FROM 3)
SELECT * FROM A WHERE EXISTS ( SELECT cc FROM B WHERE B.cc=A.cc)
當A表小於B時,用EXISTS,因為EXISTS的實現,相當於外表迴圈,實現邏輯類似於
for i in A
for j in B
if j.cc == i.cc then...
當B表小於A時,用IN
for i in B
for j in A
if j.cc == i.cc then...
那個表小就用那個表來驅動,A表小就用EXISTS,B表下用IN
12.2 COUNT(*)與COUNT(具體欄位)效率
問:在 MySQL 中統計資料表的行數,可以使用三種方式:SELECT COUNT(*)
、SELECT COUNT(1)
和SELECT COUNT(具體欄位)
,使用這三者之間的查詢效率是怎樣的?
環節1: COUNT(*)和 COUNT(1)都是對所有結果進行COUNT,COUNT(*)和COUNT(1)本質上並沒有區別(二者執行時間可能略有差別,不過你還是可以把它倆的執行效率看成是相等的)。如果有WHERE子句,則是對所有符合篩選條件的資料行進行統計;如果沒有WHERE子句,則是對資料表的資料行數進行統計。
環節2: 如果是MyISAM儲存引擎,統計資料表的行數只需要O(1)
的複雜度,這是因為每張MyISAM的資料表都有一個meta資訊儲存了row_count
值,而一致性則由表級鎖來保證。
如果是InnoDB儲存引擎,因為InnoDB支援事務,採用行級鎖和MVCC機制,所以無法像MyISAM一樣,維護一個row_count變數,因此需要採用掃描全表
,是O(n)
的複雜度,進行迴圈+計數的方式來完成統計。
環節3: 在InnoDB引擎中,如果採用COUNT(具體欄位)
來統計資料行數,要儘量採用二級索引。因為主鍵採用的索引是聚簇索引,聚簇索引包含的資訊多,明顯會大於二級索引(非聚簇索引)。對於COUNT(*)
和COUNT(1)
來說,它們不需要查詢具體的行,只是統計行數,系統會自動
採用佔用空間更小的二級索引來進行統計。
如果有多個二級索引,會使用 key_len 小的二級索引進行掃描。當沒有二級索引的時候,才會採用主鍵索引來進行統計。
12.3 關於SELECT(*)
在表查詢中,建議明確欄位,不要使用 * 作為查詢的欄位列表,推薦使用SELECT <欄位列表> 查詢。原因:
① MySQL 在解析的過程中,會透過 查詢資料字典 將"*"按序轉換成所有列名,這會大大的耗費資源和時間。
② 無法使用 覆蓋索引
12.4 LIMIT 1 對最佳化的影響
針對的是會掃描全表的 SQL 語句,如果你可以確定結果集只有一條,那麼加上LIMIT 1
的時候,當找到一條結果的時候就不會繼續掃描了,這樣會加快查詢速度。
如果資料表已經對欄位建立了唯一索引,那麼可以透過索引進行查詢,不會全表掃描的話,就不需要加上LIMIT 1
了。
12.5 多使用COMMIT
只要有可能,在程式中儘量多使用 COMMIT,這樣程式的效能得到提高,需求也會因為 COMMIT 所釋放的資源而減少。
COMMIT 所釋放的資源:
- 回滾段上用於恢復資料的資訊
- 被程式語句獲得的鎖
- redo / undo log buffer 中的空間
- 管理上述 3 種資源中的內部花費
13.淘寶訂單,主鍵設計的學習
13.1 自增ID的問題
自增ID做主鍵,簡單易懂,幾乎所有資料庫都支援自增型別,只是實現上各自有所不同而已。自增ID除了簡單,其他都是缺點,總體來看存在以下幾方面的問題:
1. 可靠性不高
存在自增ID回溯的問題,這個問題直到最新版本的MySQL 8.0才修復。自增ID回溯,是指在MySQL8.0之前,自增id的值,在mysql服務端重啟後,會退回1。
2. 安全性不高
對外暴露的介面可以非常容易猜測對應的資訊。比如:/User/1/這樣的介面,可以非常容易猜測使用者ID的值為多少,總使用者數量有多少,也可以非常容易地透過介面進行資料的爬取。
3. 效能差
自增ID的效能較差,需要在資料庫伺服器端生成。
4. 互動多
業務還需要額外執行一次類似 last_insert_id() 的函式才能知道剛才插入的自增值,這需要多一次的網路互動。在海量併發的系統中,多1條SQL,就多一次效能上的開銷。
5. 區域性唯一性
最重要的一點,自增ID是區域性唯一,只在當前資料庫例項中唯一,而不是全域性唯一,在任意伺服器間都是唯一的。對於目前分散式系統來說,這簡直就是噩夢。
13.2 業務欄位做主鍵
為了能夠唯一地標識一個會員的資訊,需要為會員資訊表
設定一個主鍵。那麼,怎麼為這個表設定主鍵,才能達到我們理想的目標呢? 這裡我們考慮業務欄位做主鍵。
表資料如下:
在這個表裡,哪個欄位比較合適呢?
選擇卡號(cardno)
會員卡號(cardno)看起來比較合適,因為會員卡號不能為空,而且有唯一性,可以用來 標識一條會員記錄。
mysql> CREATE TABLE demo.membermaster
-> (
-> cardno CHAR(8) PRIMARY KEY, -- 會員卡號為主鍵
-> membername TEXT,
-> memberphone TEXT,
-> memberpid TEXT,
-> memberaddress TEXT,
-> sex TEXT,
-> birthday DATETIME
-> );
Query OK, 0 rows affected (0.06 sec)
不同的會員卡號對應不同的會員,欄位“cardno”唯一地標識某一個會員。如果都是這樣,會員卡號與會員一一對應,系統是可以正常執行的。
但實際情況是,會員卡號可能存在重複使用
的情況。比如,張三因為工作變動搬離了原來的地址,不再到商家的門店消費了 (退還了會員卡),於是張三就不再是這個商家門店的會員了。但是,商家不想讓這個會 員卡空著,就把卡號是“10000001”的會員卡發給了王五。
從系統設計的角度看,這個變化只是修改了會員資訊表中的卡號是“10000001”這個會員 資訊,並不會影響到資料一致性。也就是說,修改會員卡號是“10000001”的會員資訊, 系統的各個模組,都會獲取到修改後的會員資訊,不會出現“有的模組獲取到修改之前的會員資訊,有的模組獲取到修改後的會員資訊,而導致系統內部資料不一致”的情況。因此,從資訊系統層面
上看是沒問題的。
但是從使用系統的業務層面
來看,就有很大的問題 了,會對商家造成影響
比如,我們有一個銷售流水錶(trans),記錄了所有的銷售流水明細。2020 年 12 月 01 日,張三在門店購買了一本書,消費了 89 元。那麼,系統中就有了張三買書的流水記錄,如下所示:
接著,我們查詢一下 2020 年 12 月 01 日的會員銷售記錄:
mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-> FROM demo.trans AS a
-> JOIN demo.membermaster AS b
-> JOIN demo.goodsmaster AS c
-> ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
+------------+-----------+----------+------------+---------------------+
| membername | goodsname | quantity | salesvalue | transdate |
+------------+-----------+----------+------------+---------------------+
| 張三 | 書 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
+------------+-----------+----------+------------+---------------------+
1 row in set (0.00 sec)
如果會員卡“10000001”又發給了王五,我們會更改會員資訊表。導致查詢時:
mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-> FROM demo.trans AS a
-> JOIN demo.membermaster AS b
-> JOIN demo.goodsmaster AS c
-> ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
+------------+-----------+----------+------------+---------------------+
| membername | goodsname | quantity | salesvalue | transdate |
+------------+-----------+----------+------------+---------------------+
| 王五 | 書 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
+------------+-----------+----------+------------+---------------------+
1 row in set (0.01 sec)
這次得到的結果是:王五在 2020 年 12 月 01 日,買了一本書,消費 89 元。顯然是錯誤的!結論:千萬不能把會員卡號當做主鍵。
- 選擇會員電話 或 身份證號
會員電話可以做主鍵嗎?不行的。在實際操作中,手機號也存在被運營商收回
,重新發給別人用的情況。
那身份證號行不行呢?好像可以。因為身份證決不會重複,身份證號與一個人存在一一對 應的關係。可問題是,身份證號屬於個人隱私
,顧客不一定願意給你。要是強制要求會員必須登記身份證號,會把很多客人趕跑的。其實,客戶電話也有這個問題,這也是我們在設計會員資訊表的時候,允許身份證號和電話都為空的原因。
所以,建議儘量不要用跟業務有關的欄位做主鍵。畢竟,作為專案設計的技術人員,我們誰也無法預測在專案的整個生命週期中,哪個業務欄位會因為專案的業務需求而有重複,或者重用之類的情況出現。
經驗:
剛開始使用 MySQL 時,很多人都很容易犯的錯誤是喜歡用業務欄位做主鍵,想當然地認為了解業務需求,但實際情況往往出乎意料,而更改主鍵設定的成本非常高。
13.3 淘寶的主鍵設計
在淘寶的電商業務中,訂單服務是一個核心業務。請問,訂單表的主鍵
淘寶是如何設計的呢?是自增ID嗎?
開啟淘寶,看一下訂單資訊:
從上圖可以發現,訂單號不是自增ID!我們詳細看下上述4個訂單號:
1550672064762308113
1481195847180308113
1431156171142308113
1431146631521308113
訂單號是19位的長度,且訂單的最後5位都是一樣的,都是08113。且訂單號的前面14位部分是單調遞增的。
大膽猜測,淘寶的訂單ID設計應該是:
訂單ID = 時間 + 去重欄位 + 使用者ID後6位尾號
這樣的設計能做到全域性唯一,且對分散式系統查詢及其友好。
13.4 推薦的主鍵設計
非核心業務
:對應表的主鍵自增ID,如告警、日誌、監控等資訊。
核心業務
:主鍵設計至少應該是全域性唯一且是單調遞增。全域性唯一保證在各系統之間都是唯一的,單調遞增是希望插入時不影響資料庫效能。
這裡推薦最簡單的一種主鍵設計:UUID。
UUID的特點:
全域性唯一,佔用36位元組,資料無序,插入效能差。
認識UUID:
- 為什麼UUID是全域性唯一的?
- 為什麼UUID佔用36個位元組?
- 為什麼UUID是無序的?
MySQL資料庫的UUID組成如下所示:
UUID = 時間+UUID版本(16位元組)- 時鐘序列(4位元組) - MAC地址(12位元組)
我們以UUID值e0ea12d4-6473-11eb-943c-00155dbaa39d舉例:
為什麼UUID是全域性唯一的?
在UUID中時間部分佔用60位,儲存的類似TIMESTAMP的時間戳,但表示的是從1582-10-15 00:00:00.00到現在的100ns的計數。可以看到UUID儲存的時間精度比TIMESTAMPE更高,時間維度發生重複的機率降低到1/100ns。
時鐘序列是為了避免時鐘被回撥導致產生時間重複的可能性。MAC地址用於全域性唯一。
為什麼UUID佔用36個位元組?
UUID根據字串進行儲存,設計時還帶有無用"-"字串,因此總共需要36個位元組。
為什麼UUID是隨機無序的呢?
因為UUID的設計中,將時間低位放在最前面,而這部分的資料是一直在變化的,並且是無序。
改造UUID
若將時間高低位互換,則時間就是單調遞增的了,也就變得單調遞增了。MySQL 8.0可以更換時間低位和時間高位的儲存方式,這樣UUID就是有序的UUID了。
MySQL 8.0還解決了UUID存在的空間佔用的問題,除去了UUID字串中無意義的"-"字串,並且將字串用二進位制型別儲存,這樣儲存空間降低為了16位元組。
可以透過MySQL8.0提供的uuid_to_bin函式實現上述功能,同樣的,MySQL也提供了bin_to_uuid函式進行轉化
SET @uuid = UUID();
SELECT @uuid,uuid_to_bin(@uuid),uuid_to_bin(@uuid,TRUE);
透過函式uuid_to_bin(@uuid,true)將UUID轉化為有序UUID了。全域性唯一 + 單調遞增,這不就是我們想要的主鍵!
4、有序UUID效能測試
16位元組的有序UUID,相比之前8位元組的自增ID,效能和儲存空間對比究竟如何呢?
我們來做一個測試,插入1億條資料,每條資料佔用500位元組,含有3個二級索引,最終的結果如下所示:
從上圖可以看到插入1億條資料有序UUID是最快的,而且在實際業務使用中有序UUID在業務端就可以生成
。還可以進一步減少SQL的互動次數。
另外,雖然有序UUID相比自增ID多了8個位元組,但實際只增大了3G的儲存空間,還可以接受。
在當今的網際網路環境中,非常不推薦自增ID作為主鍵的資料庫設計。更推薦類似有序UUID的全域性唯一的實現。
另外在真實的業務系統中,主鍵還可以加入業務和系統屬性,如使用者的尾號,機房的資訊等。這樣的主鍵設計就更為考驗架構師的水平了。
如果不是MySQL8.0腫麼辦?
手動賦值欄位做主鍵!
比如,設計各個分店的會員表的主鍵,因為如果每臺機器各自產生的資料需要合併,就可能會出現主鍵重複的問題。
可以在總部 MySQL 資料庫中,有一個管理資訊表,在這個表中新增一個欄位,專門用來記錄當前會員編號的最大值。
門店在新增會員的時候,先到總部 MySQL 資料庫中獲取這個最大值,在這個基礎上加 1,然後用這個值作為新會員的“id”,同時,更新總部 MySQL 資料庫管理資訊表中的當 前會員編號的最大值。
這樣一來,各個門店新增會員的時候,都對同一個總部 MySQL 資料庫中的資料表欄位進 行操作,就解決了各門店新增會員時會員編號衝突的問題。
只是為了記錄自己的學習歷程,且本人水平有限,不對之處,請指正。