Day517.索引优化与查询优化 -mysql
索引優化與查詢優化
一、數據準備
學員表 插 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; # 不加global只是當前窗口有效。步驟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 ;#假如要刪除 #drop function rand_string;隨機產生班級編號
#用于隨機產生多少到多少的編號 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 ;#假如要刪除 #drop function rand_num;步驟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表中插入數據的存儲過程
#執行存儲過程,往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
stu
#執行存儲過程,往stu表添加50萬條數據 CALL insert_stu(100000,500000);步驟6:刪除某表上的索引
創建存儲過程
執行存儲過程
CALL proc_drop_index("dbname","tablename");二、索引失效案例
1、全值匹配我最愛
全值的等值匹配
2、最佳左前綴法則
拓展:Alibaba《Java開發手冊》
索引文件具有 B-Tree 的最左前綴匹配特性,如果左邊的值未確定,那么無法使用此索引。
3、主鍵插入順序
如果此時再插入一條主鍵值為 9的記錄,那它插入的位置就如下圖:
可這個數據頁已經滿了,再插進來咋辦呢?我們需要把當前 頁面分裂 成兩個頁面,把本頁中的一些記錄移動到新創建的這個頁中。頁面分裂和記錄移位意味著什么?意味著: 性能損耗 !所以如果我們想盡量避免這樣無謂的性能損耗,最好讓插入的記錄的 主鍵值依次遞增 ,這樣就不會發生這樣的性能損耗了。
所以我們建議:讓主鍵具有 AUTO_INCREMENT ,讓存儲引擎自己為表生成主鍵,而不是我們手動插入 ,比如: person_info 表:
我們自定義的主鍵列 id 擁有 AUTO_INCREMENT 屬性,在插入記錄時存儲引擎會自動為我們填入自增的主鍵值。這樣的主鍵占用空間小,順序寫入,減少頁分裂。
4、計算、函數、類型轉換(自動或手動)導致索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%'; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc';創建索引
CREATE INDEX idx_name ON student(NAME);第一種:索引優化生效
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%'; mysql> SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%'; +---------+---------+--------+------+---------+ | id | stuno | name | age | classId | +---------+---------+--------+------+---------+ | 5301379 | 1233401 | AbCHEa | 164 | 259 | | 7170042 | 3102064 | ABcHeB | 199 | 161 | | 1901614 | 1833636 | ABcHeC | 226 | 275 | | 5195021 | 1127043 | abchEC | 486 | 72 | | 4047089 | 3810031 | AbCHFd | 268 | 210 | | 4917074 | 849096 | ABcHfD | 264 | 442 | | 1540859 | 141979 | abchFF | 119 | 140 | | 5121801 | 1053823 | AbCHFg | 412 | 327 | | 2441254 | 2373276 | abchFJ | 170 | 362 | | 7039146 | 2971168 | ABcHgI | 502 | 465 | | 1636826 | 1580286 | ABcHgK | 71 | 262 | | 374344 | 474345 | abchHL | 367 | 212 | | 1596534 | 169191 | AbCHHl | 102 | 146 |... | 5266837 | 1198859 | abclXe | 292 | 298 | | 8126968 | 4058990 | aBClxE | 316 | 150 | | 4298305 | 399962 | AbCLXF | 72 | 423 | | 5813628 | 1745650 | aBClxF | 356 | 323 | | 6980448 | 2912470 | AbCLXF | 107 | 78 | | 7881979 | 3814001 | AbCLXF | 89 | 497 | | 4955576 | 887598 | ABcLxg | 121 | 385 | | 3653460 | 3585482 | AbCLXJ | 130 | 174 | | 1231990 | 1283439 | AbCLYH | 189 | 429 | | 6110615 | 2042637 | ABcLyh | 157 | 40 | +---------+---------+--------+------+---------+ 401 rows in set, 1 warning (0.01 sec)第二種:索引優化失效
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc'; mysql> SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc'; +---------+---------+--------+------+---------+ | id | stuno | name | age | classId | +---------+---------+--------+------+---------+ | 5301379 | 1233401 | AbCHEa | 164 | 259 | | 7170042 | 3102064 | ABcHeB | 199 | 161 | | 1901614 | 1833636 | ABcHeC | 226 | 275 | | 5195021 | 1127043 | abchEC | 486 | 72 | | 4047089 | 3810031 | AbCHFd | 268 | 210 | | 4917074 | 849096 | ABcHfD | 264 | 442 | | 1540859 | 141979 | abchFF | 119 | 140 | | 5121801 | 1053823 | AbCHFg | 412 | 327 | | 2441254 | 2373276 | abchFJ | 170 | 362 | | 7039146 | 2971168 | ABcHgI | 502 | 465 | | 1636826 | 1580286 | ABcHgK | 71 | 262 | | 374344 | 474345 | abchHL | 367 | 212 | | 1596534 | 169191 | AbCHHl | 102 | 146 |... | 5266837 | 1198859 | abclXe | 292 | 298 | | 8126968 | 4058990 | aBClxE | 316 | 150 | | 4298305 | 399962 | AbCLXF | 72 | 423 | | 5813628 | 1745650 | aBClxF | 356 | 323 | | 6980448 | 2912470 | AbCLXF | 107 | 78 | | 7881979 | 3814001 | AbCLXF | 89 | 497 | | 4955576 | 887598 | ABcLxg | 121 | 385 | | 3653460 | 3585482 | AbCLXJ | 130 | 174 | | 1231990 | 1283439 | AbCLYH | 189 | 429 | | 6110615 | 2042637 | ABcLyh | 157 | 40 | +---------+---------+--------+------+---------+ 401 rows in set, 1 warning (3.62 sec)type為“ALL”,表示沒有使用到索引,查詢時間為 3.62秒,查詢效率較之前低很多。
再舉例:
- student表的字段stuno上設置有索引
- 索引優化生效:
再舉例:
- student表的字段name上設置有索引
5、類型轉換導致索引失效
下列哪個sql語句可以用到索引。(假設name字段上設置有索引)
# 未使用到索引 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name=123; # 使用到索引 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name='123';
name=123發生類型轉換,索引失效。(隱式的類型轉換)
6、范圍條件右邊的列索引失效
ALTER TABLE student DROP INDEX idx_name; ALTER TABLE student DROP INDEX idx_age; ALTER TABLE student DROP INDEX idx_age_classid; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.classId>20 AND student.name = 'abc' ; # student.classId>20的右側的student.name = 'abc'的索引就會失效 create index idx_age_name_classid on student(age,name,classid);- 將范圍查詢條件放置語句最后:
7、 不等于(!= 或者<>)索引失效
當sql語句中有!=或者<>會出現索引失效的問題,嘗試改寫為等于,或采用覆蓋索引
8、is null可以使用索引,is not null無法使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NULL; EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NOT NULL;9、 like以通配符%開頭索引失效
拓展:Alibaba《Java開發手冊》
【強制】頁面搜索嚴禁左模糊或者全模糊,如果需要請走搜索引擎來解決。
10、OR 前后存在非索引的列,索引失效
讓OR的前后條件都具備索引,如果缺少一個就會出現索引失效
# 未使用到索引 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR classid = 100; #使用到索引 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR name = 'Abel';11、數據庫和表的字符集統一使用utf8mb4
統一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,統一字符集可以避免由于字符集轉換產生的亂碼。不同的 字符集 進行比較前需要進行 轉換 會造成索引失效。
三、關聯查詢優化
1、數據準備
2、采用左外連接
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
結論:type 有All
添加索引優化
ALTER TABLE book ADD INDEX Y ( card); #【被驅動表】,可以避免全表掃描 EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
可以看到第二行的 type 變為了 ref,rows 也變成了優化比較明顯。這是由左連接特性決定的。
左外連接LEFT JOIN條件用于確定如何從右表搜索行,左邊一定都有,所以 右邊是我們的關鍵點,一定需要建立索引。
然后
3、采用內連接
drop index X on type; drop index Y on book;(如果已經刪除了可以不用再執行該操作)換成 inner join(MySQL自動選擇驅動表)
EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;
添加索引優化
接著:
DROP INDEX X ON `type`;EXPLAIN SELECT SQL_NO_CACHE * FROM TYPE INNER JOIN book ON type.card=book.card;
接著:
4、join語句原理
- Index Nested-Loop Join
如果直接使用join語句,MySQL優化器可能會選擇表t1或t2作為驅動表,這樣會影響我們分析SQL語句的執行過程。
所以,為了便于分析執行過程中的性能問題,我改用 straight_join讓MySQL使用固定的連接方式執行查詢,這樣優化器只會按照我們指定的方式去join。在這個語句里,t1 是驅動表,t2是被驅動表。
可以看到,在這條語句里,被驅動表t2的字段a上有索引,join過程用上了這個索引,因此這個語句的執行流程是這樣的:
這個過程是先遍歷表t1,然后根據從表t1中取出的每行數據中的a值,去表t2中查找滿足條件的記錄。在形式上,這個過程就跟我們寫程序時的嵌套查詢類似,并且可以用上被驅動表的索引,所以我們稱之為“Index Nested-Loop Join”,簡稱NLJ。
它對應的流程圖如下所示:
在這個流程里:
引申問題1:能不能使用join?
引申問題2:怎么選擇驅動表?
比如:
N擴大1000倍的話,掃描行數就會擴大1000倍;而M擴大1000倍,掃描行數擴大不到10倍。
兩個結論:
- Simple Nested-Loop Join
- Block Nested-Loop Join
這個過程的流程圖如下:
執行流程圖也就變成這樣:
總結1:能不能使用xxx join語句?
總結2:如果要使用join,應該選擇大表做驅動表還是選擇小表做驅動表?
總結3:什么叫作“小表”?
在決定哪個表做驅動表的時候,應該是兩個表按照各自的條件過濾,過濾完成之后,計算參與join的各個字段的總數據量,數據量小的那個表,就是“小表”,應該作為驅動表。
5、小結
- 保證被驅動表的JOIN字段已經創建了索引
- 需要JOIN 的字段,數據類型保持絕對一致。
- LEFT JOIN 時,選擇小表作為驅動表, 大表作為被驅動表 。減少外層循環的次數。
- INNER JOIN 時,MySQL會自動將 小結果集的表選為驅動表 。選擇相信MySQL優化策略。
- 能夠直接多表關聯的盡量直接關聯,不用子查詢。(減少查詢的趟數)
- 不建議使用子查詢,建議將子查詢SQL拆開結合程序多次查詢,或使用 JOIN 來代替子查詢。
- 衍生表建不了索引
四、子查詢優化
看看能不能將子查詢優化成內外連接查詢
MySQL從4.1版本開始支持子查詢,使用子查詢可以進行SELECT語句的嵌套查詢,即一個SELECT查詢的結果作為另一個SELECT語句的條件。 子查詢可以一次性完成很多邏輯上需要多個步驟才能完成的SQL操作。
子查詢是 MySQL 的一項重要的功能,可以幫助我們通過一個 SQL 語句實現比較復雜的查詢。但是,子查詢的執行效率不高。
原因:
① 執行子查詢時,MySQL需要為內層查詢語句的查詢結果建立一個臨時表 ,然后外層查詢語句從臨時表中查詢記錄。查詢完畢后,再 撤銷這些臨時表 。這樣會消耗過多的CPU和IO資源,產生大量的慢查詢。
② 子查詢的結果集存儲的臨時表,不論是內存臨時表還是磁盤臨時表都 不會存在索引 ,所以查詢性能會受到一定的影響。
③ 對于返回結果集比較大的子查詢,其對查詢性能的影響也就越大。
在MySQL中,可以使用連接(JOIN)查詢來替代子查詢。
連接查詢 不需要建立臨時表,其 速度比子查詢要快 ,如果查詢中使用索引的話,性能就會更好。
結論:
盡量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代
五、排序優化
1、排序優化
問題:
在 WHERE 條件字段上加索引,但是為什么在 ORDER BY 字段上還要加索引呢?
優化建議:
2、案例實戰
ORDER BY子句,盡量使用Index方式排序,避免使用FileSort方式排序。
執行案例前先清除student上的索引,只留主鍵:
DROP INDEX idx_age ON student; DROP INDEX idx_age_classid_stuno ON student; DROP INDEX idx_age_classid_name ON student; #或者 call proc_drop_index('atguigudb2','student');場景:查詢年齡為30歲的,且學生編號小于101000的學生,按用戶名稱排序
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno <101000 ORDER BY NAME ;
查詢結果如下:
結論:
type 是 ALL,即最壞的情況。Extra 里還出現了 Using filesort,也是最壞的情況。優化是必須的。
優化思路:
方案一: 為了去掉filesort我們可以把索引建成
#創建新索引 CREATE INDEX idx_age_name ON student(age,NAME);方案二: 盡量讓where的過濾條件和排序使用上索引
建一個三個字段的組合索引:
結果竟然有 filesort的 sql 運行速度,超過了已經優化掉 filesort的 sql,而且快了很多,幾乎一瞬間就出現了結果。
結論:
idx_age_stuno_name)。但是, 隨著數據量的變化,選擇的索引也會隨之變化的 。
思考:這里我們使用如下索引,是否可行?
DROP INDEX idx_age_stuno_name ON student; CREATE INDEX idx_age_stuno ON student(age,stuno);4、filesort算法:雙路排序和單路排序
雙路排序 (慢)
- MySQL 4.1之前是使用雙路排序 ,字面意思就是兩次掃描磁盤,最終得到數據, 讀取行指針和order by列 ,對他們進行排序,然后掃描已經排序好的列表,按照列表中的值重新從列表中讀取對應的數據輸出
- 從磁盤取排序字段,在buffer進行排序,再從 磁盤取其他字段 。
取一批數據,要對磁盤進行兩次掃描,眾所周知,IO是很耗時的,所以在mysql4.1之后,出現了第二種改進的算法,就是單路排序。
單路排序 (快)
從磁盤讀取查詢需要的 所有列 ,按照order by列在buffer對它們進行排序,然后掃描排序后的列表進行輸出, 它的效率更快一些,避免了第二次讀取數據。并且把隨機IO變成了順序IO,但是它會使用更多的空
間, 因為它把每一行都保存在內存中了。
結論及引申出的問題
- 由于單路是后出的,總體而言好過雙路
- 但是用單路有問題
優化策略:
六、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、group by、distinct這些語句較為耗費CPU,數據庫的CPU資源是極其寶貴的。
- 包含了order by、group by、distinct這些查詢的語句,where條件過濾出來的結果集請保持在1000行以內,否則SQL會很慢。
七、優化分頁查詢
優化思路一:
在索引上完成排序分頁操作,最后根據主鍵關聯回原表查詢所需要的其他列內容。
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;八、優先考慮覆蓋索引
1、什么是覆蓋索引?
直接通過二級索引對應的數據找到了查詢結果,無需回表
-
理解方式一:索引是高效找到行的一個方法,但是一般數據庫也能使用索引找到一個列的數據,因此它不必讀取整個行。畢竟索引葉子節點存儲了它們索引的數據;當能通過讀取索引就可以得到想要的數據,那就不需要讀取行了。一個索引包含了滿足查詢結果的數據就叫做覆蓋索引。
-
理解方式二:非聚簇復合索引的一種形式,它包括在查詢里的SELECT、JOIN和WHERE子句用到的所有列(即建索引的字段正好是覆蓋查詢條件中所涉及的字段)。
簡單說就是, 索引列+主鍵 包含 SELECT 到 FROM之間查詢的列 。
2、覆蓋索引的利弊
好處:
弊端:
索引字段的維護 總是有代價的。因此,在建立冗余索引來支持覆蓋索引時就需要權衡考慮了。這是業務DBA,或者稱為業務數據架構師的工作。
九、如何給字符串添加索引
有一張教師表,表定義如下:
create table teacher(ID bigint unsigned primary key,email varchar(64),... )engine=innodb;講師要使用郵箱登錄,所以業務代碼中一定會出現類似于這樣的語句:
mysql> select col1, col2 from teacher where email='xxx';如果email這個字段上沒有索引,那么這個語句就只能做 全表掃描。
1、前綴索引
MySQL是支持前綴索引的。默認地,如果你創建索引的語句不指定前綴長度,那么索引就會包含整個字符串。
mysql> alter table teacher add index index1(email); #或 mysql> alter table teacher add index index2(email(6));這兩種不同的定義在數據結構和存儲上有什么區別呢?下圖就是這兩個索引的示意圖。
以及
如果使用的是index1(即email整個字符串的索引結構),執行順序是這樣的:
這個過程中,只需要回主鍵索引取一次數據,所以系統認為只掃描了一行。
如果使用的是index2(即email(6)索引結構),執行順序是這樣的:
也就是說使用前綴索引,定義好長度,就可以做到既節省空間,又不用額外增加太多的查詢成本。前面已經講過區分度,區分度越高越好。因為區分度越高,意味著重復的鍵值越少。
2、前綴索引對覆蓋索引的影響
結論:
使用前綴索引就用不上覆蓋索引對查詢性能的優化了,這也是你在選擇是否使用前綴索引時需要考慮的一個因素。
十、索引下推
Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一種在存儲引擎層使用索引過濾數據的一種優化方式。
ICP可以減少存儲引擎訪問基表的次數以及MySQL服務器訪問存儲引擎的次數。
1、使用前后的掃描過程
在不使用ICP索引掃描的過程:
storage層:只將滿足index key條件的索引記錄對應的整行記錄取出,返回給server層
server 層:對返回的數據,使用后面的where條件過濾,直至返回最后一行。
使用ICP掃描的過程:
- storage層:
首先將index key條件滿足的索引記錄區間確定,然后在索引上使用index filter進行過濾。將滿足的indexfilter條件的索引記錄才去回表取出整行記錄返回server層。不滿足index filter條件的索引記錄丟棄,不回表、也不會返回server層。 - server 層:
對返回的數據,使用table filter條件做最后的過濾。
使用前后的成本差別:
使用前,存儲層多返回了需要被index filter過濾掉的整行記錄
使用ICP后,直接就去掉了不滿足index filter條件的記錄,省去了他們回表和傳遞server層的成本。
ICP的 加速效果 取決于在存儲引擎內通過 ICP篩選 掉的數據的比例。
2、ICP的使用條件
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優化方法。
3、ICP使用案例
案例1:
SELECT * FROM tuser WHERE NAME LIKE '張%' AND age = 10 AND ismale = 1;
案例2:
十一、普通索引 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)。
1、查詢過程
假設,執行查詢的語句是 select id from test where k=5。
- 對于普通索引來說,查找到滿足條件的第一個記錄(5,500)后,需要查找下一個記錄,直到碰到第一個不滿足k=5條件的記錄。
- 對于唯一索引來說,由于索引定義了唯一性,查找到第一個滿足條件的記錄后,就會停止繼續檢索。
那么,這個不同帶來的性能差距會有多少呢?答案是,微乎其微 。
2、更新過程
為了說明普通索引和唯一索引對更新語句性能的影響這個問題,介紹一下changebuffer。
當需要更新一個數據頁時,如果數據頁在內存中就直接更新,而如果這個數據頁還沒有在內存中的話,在不影響數據一致性的前提下, InooDB會將這些更新操作緩存在change buffer中 ,這樣就不需要從磁盤中讀入這個數據頁了。在下次查詢需要訪問這個數據頁的時候,將數據頁讀入內存,然后執行changebuffer中與這個頁有關的操作。通過這種方式就能保證這個數據邏輯的正確性。
將change buffer中的操作應用到原數據頁,得到最新結果的過程稱為 merge 。除了 訪問這個數據頁 會觸發merge外,系統有 后臺線程會定期 merge。在數據庫正常關閉(shutdown) 的過程中,也會執行merge操作。
如果能夠將更新操作先記錄在change buffer, 減少讀磁盤 ,語句的執行速度會得到明顯的提升。而且,數據讀入內存是需要占用 buffer pool 的,所以這種方式還能夠 避免占用內存,提高內存利用率。
唯一索引的更新就不能使用change buffer ,實際上也只有普通索引可以使用。
如果要在這張表中插入一個新記錄(4,400)的話,InnoDB的處理流程是怎樣的?
3、change buffer的使用場景
- 首先, 業務正確性優先 。我們的前提是“業務代碼已經保證不會寫入重復數據”的情況下,討論性能問題。如果業務不能保證,或者業務就是要求數據庫來做約束,那么沒得選,必須創建唯一索引。這種情況下,本節的意義在于,如果碰上了大量插入數據慢、內存命中率低的時候,給你多提供一個排查思路。
- 然后,在一些“ 歸檔庫 ”的場景,你是可以考慮使用唯一索引的。比如,線上數據只需要保留半年,然后歷史數據保存在歸檔庫。這時候,歸檔數據已經是確保沒有唯一鍵沖突了。要提高歸檔效率,可以考慮把表里面的唯一索引改成普通索引。
十二、其它查詢優化策略
1、EXISTS 和 IN 的區分
問題:
不太理解哪種情況下應該使用 EXISTS,哪種情況應該用 IN。選擇的標準是看能否使用表的索引嗎?
2、COUNT(*)與COUNT(具體字段)效率
問:在 MySQL 中統計數據表的行數,可以使用三種方式: SELECT COUNT(*) 、 SELECT COUNT(1) 和 SELECT COUNT(具體字段) ,使用這三者之間的查詢效率是怎樣的?
3、關于SELECT(*)
在表查詢中,建議明確字段,不要使用 * 作為查詢的字段列表,推薦使用SELECT <字段列表> 查詢。原因:
① MySQL 在解析的過程中,會通過 查詢數據字典 將"*"按序轉換成所有列名,這會大大的耗費資源和時間。
② 無法使用 覆蓋索引
4、LIMIT 1 對優化的影響
針對的是會掃描全表的 SQL 語句,如果你可以確定結果集只有一條,那么加上 LIMIT 1的時候,當找到一條結果的時候就不會繼續掃描了,這樣會加快查詢速度。
如果數據表已經對字段建立了唯一索引,那么可以通過索引進行查詢,不會全表掃描的話,就不需要加上 LIMIT 1 了。
5、多使用COMMIT
只要有可能,在程序中盡量多使用 COMMIT,這樣程序的性能得到提高,需求也會因為 COMMIT 所釋放
的資源而減少。
COMMIT 會有所釋放的資源:
- 回滾段上用于恢復數據的信息
- 被程序語句獲得的鎖
- redo / undo log buffer 中的空間
- 管理上述 3 種資源中的內部花費
十三、淘寶數據庫,主鍵如何設計的?
聊一個實際問題:淘寶的數據庫,主鍵是如何設計的?
某些錯的離譜的答案還在網上年復一年的流傳著,甚至還成為了所謂的MySQL軍規。
其中,一個最明顯的錯誤就是關于MySQL的主鍵設計。
大部分人的回答如此自信:用8字節的 BIGINT 做主鍵,而不要用INT。 錯!
這樣的回答,只站在了數據庫這一層,而沒有 從業務的角度 思考主鍵。主鍵就是一個自增ID嗎?站在 2022年的新年檔口,用自增做主鍵,架構設計上可能 連及格都拿不到 。
1、自增ID的問題
自增ID做主鍵,簡單易懂,幾乎所有數據庫都支持自增類型,只是實現上各自有所不同而已。自增ID除了簡單,其他都是缺點,總體來看存在以下幾方面的問題:
存在自增ID回溯的問題,這個問題直到最新版本的MySQL 8.0才修復。
對外暴露的接口可以非常容易猜測對應的信息。比如:/User/1/這樣的接口,可以非常容易猜測用戶ID的值為多少,總用戶數量有多少,也可以非常容易地通過接口進行數據的爬取。
自增ID的性能較差,需要在數據庫服務器端生成。
業務還需要額外執行一次類似 last_insert_id() 的函數才能知道剛才插入的自增值,這需要多一次的網絡交互。在海量并發的系統中,多1條SQL,就多一次性能上的開銷。
最重要的一點,自增ID是局部唯一,只在當前數據庫實例中唯一,而不是全局唯一,在任意服務器間都是唯一的。對于目前分布式系統來說,這簡直就是噩夢。
2、業務字段做主鍵
為了能夠唯一地標識一個會員的信息,需要為 會員信息表 設置一個主鍵。那么,怎么為這個表設置主鍵,才能達到我們理想的目標呢? 這里我們考慮業務字段做主鍵。
表數據如下:
在這個表里,哪個字段比較合適呢?
- 選擇卡號(cardno)
會員卡號(cardno)看起來比較合適,因為會員卡號不能為空,而且有唯一性,可以用來 標識一條會員記錄。
不同的會員卡號對應不同的會員,字段“cardno”唯一地標識某一個會員。如果都是這樣,會員卡號與會員一一對應,系統是可以正常運行的。
但實際情況是, 會員卡號可能存在重復使用 的情況。比如,張三因為工作變動搬離了原來的地址,不再到商家的門店消費了 (退還了會員卡),于是張三就不再是這個商家門店的會員了。但是,商家不想讓這個會 員卡空著,就把卡號是“10000001”的會員卡發給了王五。
從系統設計的角度看,這個變化只是修改了會員信息表中的卡號是“10000001”這個會員 信息,并不會影響到數據一致性。也就是說,修改會員卡號是“10000001”的會員信息, 系統的各個模塊,都會獲取到修改后的會員信息,不會出現“有的模塊獲取到修改之前的會員信息,有的模塊獲取到修改后的會員信息,而導致系統內部數據不一致”的情況。因此,從 信息系統層面 上看是沒問題的。
但是從使用 系統的業務層面 來看,就有很大的問題 了,會對商家造成影響。
比如,我們有一個銷售流水表(trans),記錄了所有的銷售流水明細。2020 年 12 月 01 日,張三在門店購買了一本書,消費了 89 元。那么,系統中就有了張三買書的流水記錄,如下所示:
接著,我們查詢一下 2020 年 12 月 01 日的會員銷售記錄:
如果會員卡“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 時,很多人都很容易犯的錯誤是喜歡用業務字段做主鍵,想當然地認為了解業務需求,但實際情況往往出乎意料,而更改主鍵設置的成本非常高。
3、淘寶的主鍵設計
在淘寶的電商業務中,訂單服務是一個核心業務。請問, 訂單表的主鍵 淘寶是如何設計的呢?是自增ID嗎?
打開淘寶,看一下訂單信息:
從上圖可以發現,訂單號不是自增ID!我們詳細看下上述4個訂單號:
1550672064762308113 1481195847180308113 1431156171142308113 1431146631521308113訂單號是19位的長度,且訂單的最后5位都是一樣的,都是08113。且訂單號的前面14位部分是單調遞增的。
大膽猜測,淘寶的訂單ID設計應該是:
訂單ID = 時間 + 去重字段 + 用戶ID后6位尾號這樣的設計能做到全局唯一,且對分布式系統查詢及其友好。
4、推薦的主鍵設計
可通過改變UUID的時間排序,將時分秒放在前面,而不是默認的秒分時,就可做到有序
非核心業務 :對應表的主鍵自增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了。全局唯一 + 單調遞增,這不就是我們想要的主鍵!
有序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 數據庫中的數據表字段進 行操作,就解決了各門店添加會員時會員編號沖突的問題。
總結
以上是生活随笔為你收集整理的Day517.索引优化与查询优化 -mysql的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 软件健康管理技术浅析
- 下一篇: 微信转发 jssdk的简单使用