欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

總結(jié)12個MySQL慢查詢的原因分析

 更新時間:2022年08月02日 15:26:59   作者:無薪法師  
這篇文章主要介紹了總結(jié)12個MySQL慢查詢的原因分析,慢查詢,都是因為沒有加索引。如果沒有加索引的話,會導致全表掃描的,更多相關內(nèi)容需要的朋友可以參考一下

1. SQL 沒加索引

很多時候,我們的慢查詢,都是因為沒有加索引。如果沒有加索引的話,會導致全表掃描的。因此,應考慮在 where 的條件列,建立索引,盡量避免全表掃描。

反例:

select * from user_info where name =‘撿田螺的小男孩公眾號' ;

正例:

//添加索引 alter table user_info add index idx_name (name);

2. SQL 索引不生效

有時候我們明明加了索引了,但是索引卻不生效。

在哪些場景,索引會不生效呢?主要有以下十大經(jīng)典場景:

2.1 隱式的類型轉(zhuǎn)換,索引失效

我們創(chuàng)建一個用戶 user 表:

CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, userId varchar(32) NOT NULL, age varchar(16) NOT NULL, name varchar(255) NOT NULL, PRIMARY KEY (id), KEY idx_userid (userId) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

userId 字段為字串類型,是 B + 樹的普通索引,如果查詢條件傳了一個數(shù)字過去,會導致索引失效。

如下:

如果給數(shù)字加上’', 也就是說,傳的是一個字符串呢,當然是走索引,如下圖:

為什么第一條語句未加單引號就不走索引了呢?這是因為不加單引號時,是字符串跟數(shù)字的比較,它們類型不匹配,MySQL 會做隱式的類型轉(zhuǎn)換,把它們轉(zhuǎn)換為浮點數(shù)再做比較。隱式的類型轉(zhuǎn)換,索引會失效。

2.2 查詢條件包含 or,可能導致索引失效

我們還是用這個表結(jié)構(gòu):

CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, userId varchar(32) NOT NULL, age varchar(16) NOT NULL, name varchar(255) NOT NULL, PRIMARY KEY (id), KEY idx_userid (userId) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

其中 userId 加了索引,但是 age 沒有加索引的。我們使用了 or,以下 SQL 是不走索引的,如下:

對于 or+ 沒有索引的 age 這種情況,假設它走了 userId 的索引,但是走到 age 查詢條件時,它還得全表掃描,也就是需要三步過程:全表掃描 + 索引掃描 + 合并。如果它一開始就走全表掃描,直接一遍掃描就完事。Mysql 優(yōu)化器出于效率與成本考慮,遇到 or 條件,讓索引失效,看起來也合情合理嘛。

注意:如果 or 條件的列都加了索引,索引可能會走也可能不走,大家可以自己試一試哈。但是平時大家使用的時候,還是要注意一下這個 or,學會用 explain 分析。遇到不走索引的時候,考慮拆開兩條 SQL。

2.3. like 通配符可能導致索引失效

并不是用了 like 通配符,索引一定會失效,而是 like 查詢是以 % 開頭,才會導致索引失效。

like 查詢以 % 開頭,索引失效

explain select * from user where userId like ‘%123';

把 % 放后面,發(fā)現(xiàn)索引還是正常走的,如下:

explain select * from user where userId like ‘123%';

既然 like 查詢以 % 開頭,會導致索引失效。我們?nèi)绾蝺?yōu)化呢?

使用覆蓋索引把 % 放后面 2.4 查詢條件不滿足聯(lián)合索引的最左匹配原則

MySQl 建立聯(lián)合索引時,會遵循最左前綴匹配的原則,即最左優(yōu)先。如果你建立一個(a,b,c)的聯(lián)合索引,相當于建立了 (a)、(a,b)、(a,b,c) 三個索引。

假設有以下表結(jié)構(gòu):

CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, user_id varchar(32) NOT NULL, age varchar(16) NOT NULL, name varchar(255) NOT NULL, PRIMARY KEY (id), KEY idx_userid_name (user_id,name) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

有一個聯(lián)合索引 idx_userid_name,我們執(zhí)行這個 SQL,查詢條件是 name,索引是無效:

explain select * from user where name =‘撿田螺的小男孩';

因為查詢條件列 name 不是聯(lián)合索引 idx_userid_name 中的第一個列,索引不生效

在聯(lián)合索引中,查詢條件滿足最左匹配原則時,索引才正常生效。

2.5 在索引列上使用 mysql 的內(nèi)置函數(shù)

表結(jié)構(gòu):

CREATE TABLEuser(idint(11) NOT NULL AUTO_INCREMENT,userIdvarchar(32) NOT NULL,login_timedatetime NOT NULL, PRIMARY KEY (id), KEYidx_userId(userId) USING BTREE, KEYidx_login_time(login_Time) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

雖然 login_time 加了索引,但是因為使用了 mysql 的內(nèi)置函數(shù) Date_ADD(),索引直接 GG,如圖:

一般這種情況怎么優(yōu)化呢?可以把內(nèi)置函數(shù)的邏輯轉(zhuǎn)移到右邊,如下:

explain select * from user where login_time = DATE_ADD(‘2022-05-22 00:00:00',INTERVAL -1 DAY);

2.6 對索引進行列運算(如,+、-、*、/), 索引不生效

表結(jié)構(gòu):

CREATE TABLEuser(idint(11) NOT NULL AUTO_INCREMENT,userIdvarchar(32) NOT NULL,ageint(11) DEFAULT NULL, PRIMARY KEY (id), KEYidx_age(age) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

雖然 age 加了索引,但是因為它進行運算,索引直接迷路了

如圖:

所以不可以對索引列進行運算,可以在代碼處理好,再傳參進去

2.7 索引字段上使用(!= 或者 < >),索引可能失效

表結(jié)構(gòu):

CREATE TABLEuser(idint(11) NOT NULL AUTO_INCREMENT,userIdint(11) NOT NULL,ageint(11) DEFAULT NULL,namevarchar(255) NOT NULL, PRIMARY KEY (id), KEYidx_age(age) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

雖然 age 加了索引,但是使用了!= 或者 < >,not in 這些時,索引如同虛設。

如下:

其實這個也是跟 mySQL優(yōu)化器有關,如果優(yōu)化器覺得即使走了索引,還是需要掃描很多很多行的哈,它覺得不劃算,不如直接不走索引。平時我們用!= 或者 < >,not in 的時候,留點心眼哈。

2.8 索引字段上使用 is null, is not null,索引可能失效

表結(jié)構(gòu):

CREATE TABLEuser(idint(11) NOT NULL AUTO_INCREMENT,cardvarchar(255) DEFAULT NULL,namevarchar(255) DEFAULT NULL, PRIMARY KEY (id), KEYidx_name(name) USING BTREE, KEYidx_card(card) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

單個 name 字段加上索引,并查詢 name 為非空的語句,其實會走索引的,如下:

單個 card 字段加上索引,并查詢 name 為非空的語句,其實會走索引的,如下:圖片

但是它兩用 or 連接起來,索引就失效了,如下:

很多時候,也是因為數(shù)據(jù)量問題,導致了 MySQL 優(yōu)化器放棄走索引。同時,平時我們用 explain 分析 SQL 的時候,如果 type=range, 要注意一下哈,因為這個可能因為數(shù)據(jù)量問題,導致索引無效。

2.9 左右連接,關聯(lián)的字段編碼格式不一樣

新建兩個表,一個 user,一個 user_job

CREATE TABLEuser(idint(11) NOT NULL AUTO_INCREMENT,namevarchar(255) CHARACTER SET utf8mb4 DEFAULT NULL,ageint(11) NOT NULL, PRIMARY KEY (id), KEYidx_name(name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

CREATE TABLE user_job (
id int(11) NOT NULL,
userId int(11) NOT NULL,
job varchar(255) DEFAULT NULL,
name varchar(255) DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_name (name) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

user 表的 name 字段編碼是 utf8mb4,而 user_job 表的 name 字段編碼為 utf8。

執(zhí)行左外連接查詢,user_job 表還是走全表掃描,如下:

如果把它們的 name 字段改為編碼一致,相同的 SQL,還是會走索引。

所以大家在做表關聯(lián)時,注意一下關聯(lián)字段的編碼問題哈。

2.10 優(yōu)化器選錯了索引

MySQL 中一張表是可以支持多個索引的。你寫 SQL 語句的時候,沒有主動指定使用哪個索引的話,用哪個索引是由 MySQL 來確定的。

我們?nèi)粘i_發(fā)中,不斷地刪除歷史數(shù)據(jù)和新增數(shù)據(jù)的場景,有可能會導致 MySQL 選錯索引。那么有哪些解決方案呢?

使用 force index 強行選擇某個索引修改你的 SQl,引導它使用我們期望的索引優(yōu)化你的業(yè)務邏輯優(yōu)化你的索引,新建一個更合適的索引,或者刪除誤用的索引。

3. limit 深分頁問題

limit 深分頁問題,會導致慢查詢,應該大家都司空見慣了吧。

3.1 limit 深分頁為什么會變慢

limit 深分頁為什么會導致 SQL 變慢呢?假設我們有表結(jié)構(gòu)如下:

CREATE TABLE account ( id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘主鍵Id', name varchar(255) DEFAULT NULL COMMENT ‘賬戶名', balance int(11) DEFAULT NULL COMMENT ‘余額', create_time datetime NOT NULL COMMENT ‘創(chuàng)建時間', update_time datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘更新時間', PRIMARY KEY (id), KEY idx_name (name), KEY idx_create_time (create_time) //索引 ) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8 ROW_FORMAT=REDUNDANT COMMENT=‘賬戶表';

你知道以下 SQL,執(zhí)行過程是怎樣的嘛?

select id,name,balance from account where create_time> ‘2020-09-19' limit 100000,10;

這個 SQL 的執(zhí)行流程:

通過普通二級索引樹 idx_create_time,過濾 create_time 條件,找到滿足條件的主鍵 id。通過主鍵id,回到 id主鍵索引樹,找到滿足記錄的行,然后取出需要展示的列(回表過程)掃描滿足條件的 100010 行,然后扔掉前 100000 行,返回。

limit 深分頁,導致 SQL 變慢原因有兩個:

limit 語句會先掃描 offset+n 行,然后再丟棄掉前 offset 行,返回后 n 行數(shù)據(jù)。也就是說 limit 100000,10,就會掃描 100010 行,而 limit 0,10,只掃描 10 行。limit 100000,10 掃描更多的行數(shù),也意味著回表更多的次數(shù)。 3.2 如何優(yōu)化深分頁問題

我們可以通過減少回表次數(shù)來優(yōu)化。一般有標簽記錄法和延遲關聯(lián)法。

標簽記錄法

就是標記一下上次查詢到哪一條了,下次再來查的時候,從該條開始往下掃描。就好像看書一樣,上次看到哪里了,你就折疊一下或者夾個書簽,下次來看的時候,直接就翻到啦。

假設上一次記錄到 100000,則 SQL 可以修改為:

select id,name,balance FROM account where id > 100000 limit 10;

這樣的話,后面無論翻多少頁,性能都會不錯的,因為命中了 id索引。但是這種方式有局限性:需要一種類似連續(xù)自增的字段。

延遲關聯(lián)法

延遲關聯(lián)法,就是把條件轉(zhuǎn)移到主鍵索引樹,然后減少回表。

如下:

select acct1.id,acct1.name,acct1.balance FROM account acct1 INNER JOIN (SELECT a.id FROM account a WHERE a.create_time > ‘2020-09-19' limit 100000, 10) AS acct2 on acct1.id= acct2.id;

優(yōu)化思路就是,先通過 idx_create_time 二級索引樹查詢到滿足條件的主鍵ID,再與原表通過主鍵ID 內(nèi)連接,這樣后面直接走了主鍵索引了,同時也減少了回表。

4. 單表數(shù)據(jù)量太大

4.1 單表數(shù)據(jù)量太大為什么會變慢?

一個表的數(shù)據(jù)量達到好幾千萬或者上億時,加索引的效果沒那么明顯啦。性能之所以會變差,是因為維護索引的 B+ 樹結(jié)構(gòu)層級變得更高了,查詢一條數(shù)據(jù)時,需要經(jīng)歷的磁盤 IO 變多,因此查詢性能變慢。

4.2 一棵 B + 樹可以存多少數(shù)據(jù)量

大家是否還記得,一個 B + 樹大概可以存放多少數(shù)據(jù)量呢?

InnoDB 存儲引擎最小儲存單元是頁,一頁大小就是 16k。

B + 樹葉子存的是數(shù)據(jù),內(nèi)部節(jié)點存的是鍵值 + 指針。索引組織表通過非葉子節(jié)點的二分查找法以及指針確定數(shù)據(jù)在哪個頁中,進而再去數(shù)據(jù)頁中找到需要的數(shù)據(jù);

假設 B + 樹的高度為 2 的話,即有一個根結(jié)點和若干個葉子結(jié)點。這棵 B + 樹的存放總記錄數(shù)為 = 根結(jié)點指針數(shù) * 單個葉子節(jié)點記錄行數(shù)。

如果一行記錄的數(shù)據(jù)大小為 1k,那么單個葉子節(jié)點可以存的記錄數(shù) =16k/1k =16.非葉子節(jié)點內(nèi)存放多少指針呢?我們假設主鍵 ID 為 bigint 類型,長度為 8 字節(jié) (面試官問你 int 類型,一個 int 就是 32 位,4 字節(jié)),而指針大小在 InnoDB 源碼中設置為 6 字節(jié),所以就是 8+6=14 字節(jié),16k/14B =16*1024B/14B = 1170

因此,一棵高度為 2 的 B + 樹,能存放 1170 * 16=18720 條這樣的數(shù)據(jù)記錄。同理一棵高度為 3 的 B + 樹,能存放 1170 *1170 *16 =21902400,也就是說,可以存放兩千萬左右的記錄。B + 樹高度一般為 1-3 層,已經(jīng)滿足千萬級別的數(shù)據(jù)存儲。

如果 B + 樹想存儲更多的數(shù)據(jù),那樹結(jié)構(gòu)層級就會更高,查詢一條數(shù)據(jù)時,需要經(jīng)歷的磁盤 IO 變多,因此查詢性能變慢。

4.3 如何解決單表數(shù)據(jù)量太大,查詢變慢的問題

一般超過千萬級別,我們可以考慮分庫分表了。

分庫分表可能導致的問題:

  • 事務問題
  • 跨庫問題
  • 排序問題
  • 分頁問題
  • 分布式 ID

因此,大家在評估是否分庫分表前,先考慮下,是否可以把部分歷史數(shù)據(jù)歸檔先,如果可以的話,先不要急著分庫分表。如果真的要分庫分表,綜合考慮和評估方案。比如可以考慮垂直、水平分庫分表。水平分庫分表策略的話,range 范圍、hash 取模、range+hash 取?;旌?/strong>等等。

5. join 或者子查詢過多

一般來說,不建議使用子查詢,可以把子查詢改成 join 來優(yōu)化。而數(shù)據(jù)庫有個規(guī)范約定就是:盡量不要有超過 3 個以上的表連接。為什么要這么建議呢?我們來聊聊,join 哪些方面可能導致慢查詢吧。

MySQL 中,join 的執(zhí)行算法,分別是:Index Nested-Loop Join 和 Block Nested-Loop Join。

  • Index Nested-Loop Join:這個 join 算法,跟我們寫程序時的嵌套查詢類似,并且可以用上被驅(qū)動表的索引。
  • Block Nested-Loop Join:這種 join 算法,被驅(qū)動表上沒有可用的索引 , 它會先把驅(qū)動表的數(shù)據(jù)讀入線程內(nèi)存 join_buffer 中,再掃描被驅(qū)動表,把被驅(qū)動表的每一行取出來,跟 join_buffer 中的數(shù)據(jù)做對比,滿足 join 條件的,作為結(jié)果集的一部分返回。

join 過多的問題:

一方面,過多的表連接,會大大增加 SQL 復雜度。另外一方面,如果可以使用被驅(qū)動表的索引那還好,并且使用小表來做驅(qū)動表,查詢效率更佳。如果被驅(qū)動表沒有可用的索引,join 是在 join_buffer 內(nèi)存做的,如果匹配的數(shù)據(jù)量比較小或者 join_buffer 設置的比較大,速度也不會太慢。但是,如果 join 的數(shù)據(jù)量比較大時,mysql 會采用在硬盤上創(chuàng)建臨時表的方式進行多張表的關聯(lián)匹配,這種顯然效率就極低,本來磁盤的 IO 就不快,還要關聯(lián)。

一般情況下,如果業(yè)務需要的話,關聯(lián) 2~3 個表是可以接受的,但是關聯(lián)的字段需要加索引哈。如果需要關聯(lián)更多的表,建議從代碼層面進行拆分,在業(yè)務層先查詢一張表的數(shù)據(jù),然后以關聯(lián)字段作為條件查詢關聯(lián)表形成 map,然后在業(yè)務層進行數(shù)據(jù)的拼裝。

6. in 元素過多

如果使用了 in,即使后面的條件加了索引,還是要注意 in 后面的元素不要過多哈。in 元素一般建議不要超過 500 個,如果超過了,建議分組,每次 500 一組進行哈。

反例:

select user_id,name from user where user_id in (1,2,3…1000000);

如果我們對 in的條件不做任何限制的話,該查詢語句一次性可能會查詢出非常多的數(shù)據(jù),很容易導致接口超時。尤其有時候,我們是用的子查詢,in 后面的子查詢,你都不知道數(shù)量有多少那種,更容易采坑(所以我把 in 元素過多抽出來作為一個小節(jié))。如下這種子查詢:

select * from user where user_id in (select author_id from artilce where type = 1);

正例是,分批進行,每批 500 個:

select user_id,name from user where user_id in (1,2,3…500);

如果傳參的 ids 太多,還可以做個參數(shù)校驗什么的

if (userIds.size() > 500) { throw new Exception(“單次查詢的用戶Id不能超過200”); }

7. 數(shù)據(jù)庫在刷臟頁

7.1 什么是臟頁

當內(nèi)存數(shù)據(jù)頁跟磁盤數(shù)據(jù)頁內(nèi)容不一致的時候,我們稱這個內(nèi)存頁為 “臟頁”。內(nèi)存數(shù)據(jù)寫入到磁盤后,內(nèi)存和磁盤上的數(shù)據(jù)頁的內(nèi)容就一致了,稱為 “干凈頁”。一般有更新 SQL 才可能會導致臟頁,我們回憶一下:一條更新語句是如何執(zhí)行的

7.2 一條更新語句是如何執(zhí)行的?

以下的這個更新 SQL,如何執(zhí)行的呢?

update t set c=c+1 where id=666;
  • 對于這條更新 SQL,執(zhí)行器會先找引擎取 id=666 這一行。如果這行所在的數(shù)據(jù)頁本來就在內(nèi)存中的話,就直接返回給執(zhí)行器。如果不在內(nèi)存,就去磁盤讀入內(nèi)存,再返回。
  • 執(zhí)行器拿到引擎給的行數(shù)據(jù)后,給這一行 C 的值加一,得到新的一行數(shù)據(jù),再調(diào)用引擎接口寫入這行新數(shù)據(jù)。
  • 引擎將這行新數(shù)據(jù)更新到內(nèi)存中,同時將這個更新操作記錄到 redo log 里面,但是此時 redo log 是處于 prepare 狀態(tài)的哈。
  • 執(zhí)行器生成這個操作的 binlog,并把 binlog 寫入磁盤。
  • 執(zhí)行器調(diào)用引擎的提交事務接口,引擎把剛剛寫入的 redo log 改成提交(commit)狀態(tài),更新完成。

InnoDB 在處理更新語句的時候,只做了寫日志這一個磁盤操作。這個日志叫作 redo log(重做日志)。平時更新 SQL 執(zhí)行得很快,其實是因為它只是在寫內(nèi)存和 redo log 日志,等到空閑的時候,才把 redo log 日志里的數(shù)據(jù)同步到磁盤中。

有些小伙伴可能有疑惑,redo log 日志不是在磁盤嘛?那為什么不慢?其實是因為寫 redo log 的過程是順序?qū)懘疟P的。磁盤順序?qū)憰p少尋道等待時間,速度比隨機寫要快很多的。

7.3 為什么會出現(xiàn)臟頁呢?

更新 SQL 只是在寫內(nèi)存和 redo log 日志,等到空閑的時候,才把 redo log 日志里的數(shù)據(jù)同步到磁盤中。這時內(nèi)存數(shù)據(jù)頁跟磁盤數(shù)據(jù)頁內(nèi)容不一致,就出現(xiàn)臟頁。

7.4 什么時候會刷臟頁(flush)?

InnoDB 存儲引擎的 redo log 大小是固定,且是環(huán)型寫入的,如下圖(圖片來源于 MySQL 實戰(zhàn) 45 講):

那什么時候會刷臟頁?有幾種場景:

  • redo log 寫滿了,要刷臟頁。這種情況要盡量避免的。因為出現(xiàn)這種情況時,整個系統(tǒng)就不能再接受更新啦,即所有的更新都必須堵住。
  • 內(nèi)存不夠了,需要新的內(nèi)存頁,就要淘汰一些數(shù)據(jù)頁,這時候會刷臟頁

InnoDB 用緩沖池(buffer pool)管理內(nèi)存,而當要讀入的數(shù)據(jù)頁沒有在內(nèi)存的時候,就必須到緩沖池中申請一個數(shù)據(jù)頁。這時候只能把最久不使用的數(shù)據(jù)頁從內(nèi)存中淘汰掉:如果要淘汰的是一個干凈頁,就直接釋放出來復用;但如果是臟頁呢,就必須將臟頁先刷到磁盤,變成干凈頁后才能復用。

  • MySQL 認為系統(tǒng)空閑的時候,也會刷一些臟頁
  • MySQL 正常關閉時,會把內(nèi)存的臟頁都 flush 到磁盤上

7.5 為什么刷臟頁會導致 SQL 變慢呢?

  • redo log 寫滿了,要刷臟頁,這時候會導致系統(tǒng)所有的更新堵住,寫性能都跌為 0 了,肯定慢呀。一般要杜絕出現(xiàn)這個情況。
  • 一個查詢要淘汰的臟頁個數(shù)太多,一樣會導致查詢的響應時間明顯變長。

8. order by 文件排序

order by 就一定會導致慢查詢嗎?不是這樣的哈,因為 order by 平時用得多,并且數(shù)據(jù)量一上來,還是走文件排序的話,很容易有慢 SQL 的。聽我娓娓道來,order by 哪些時候可能會導致慢 SQL 哈。

8.1 order by 的 Using filesort 文件排序

我們平時經(jīng)常需要用到 order by ,主要就是用來給某些字段排序的。

比如以下 SQL:

select name,age,city from staff where city = ‘深圳' order by age limit 10;

它表示的意思就是:查詢前 10 個,來自深圳員工的姓名、年齡、城市,并且按照年齡小到大排序。

查看 explain 執(zhí)行計劃的時候,可以看到 Extra 這一列,有一個 Using filesort,它表示用到文件排序。

8.2 order by 文件排序效率為什么較低

order by 用到文件排序時,為什么查詢效率會相對低呢?

order by 排序,分為全字段排序和 rowid 排序。它是拿 max_length_for_sort_data 和結(jié)果行數(shù)據(jù)長度對比,如果結(jié)果行數(shù)據(jù)長度超過 max_length_for_sort_data 這個值,就會走 rowid 排序,相反,則走全字段排序。

rowid 排序

rowid 排序,一般需要回表去找滿足條件的數(shù)據(jù),所以效率會慢一點。以下這個 SQL,使用 rowid 排序,執(zhí)行過程是這樣:

  • select name,age,city from staff where city = ‘深圳’ order by age limit 10;
  • MySQL 為對應的線程初始化 sort_buffer,放入需要排序的 age 字段,以及主鍵id;
  • 從索引樹 idx_city, 找到第一個滿足 city='深圳’條件的主鍵id,也就是圖中的 id=9;
  • 到主鍵id索引樹拿到 id=9 的這一行數(shù)據(jù), 取 age和主鍵id 的值,存到 sort_buffer;
  • 從索引樹 idx_city 拿到下一個記錄的主鍵id,即圖中的 id=13;
  • 重復步驟 3、4 直到 city 的值不等于深圳為止;
  • 前面 5 步已經(jīng)查找到了所有 city 為深圳的數(shù)據(jù),在 sort_buffer 中,將所有數(shù)據(jù)根據(jù) age 進行排序;
  • 遍歷排序結(jié)果,取前 10 行,并按照 id 的值回到原表中,取出 city、name 和 age 三個字段返回給客戶端。

全字段排序

同樣的 SQL,如果是走全字段排序是這樣的:

select name,age,city from staff where city = ‘深圳' order by age limit 10;
  • MySQL 為對應的線程初始化 sort_buffer,放入需要查詢的 name、age、city 字段;
  • 從索引樹 idx_city, 找到第一個滿足 city='深圳’條件的主鍵 id,也就是圖中的 id=9;
  • 到主鍵 id索引樹拿到 id=9 的這一行數(shù)據(jù), 取 name、age、city 三個字段的值,存到 sort_buffer;
  • 從索引樹 idx_city 拿到下一個記錄的主鍵 id,即圖中的 id=13;
  • 重復步驟 3、4 直到 city 的值不等于深圳為止;
  • 前面 5 步已經(jīng)查找到了所有 city 為深圳的數(shù)據(jù),在 sort_buffer 中,將所有數(shù)據(jù)根據(jù) age 進行排序;
  • 按照排序結(jié)果取前 10 行返回給客戶端。

sort_buffer 的大小是由一個參數(shù)控制的:sort_buffer_size。

  • 如果要排序的數(shù)據(jù)小于 sort_buffer_size,排序在 sort_buffer 內(nèi)存中完成
  • 如果要排序的數(shù)據(jù)大于 sort_buffer_size,則借助磁盤文件來進行排序。

借助磁盤文件排序的話,效率就更慢一點。因為先把數(shù)據(jù)放入 sort_buffer,當快要滿時。會排一下序,然后把 sort_buffer 中的數(shù)據(jù),放到臨時磁盤文件,等到所有滿足條件數(shù)據(jù)都查完排完,再用歸并算法把磁盤的臨時排好序的小文件,合并成一個有序的大文件。

8.3 如何優(yōu)化 order by 的文件排序

order by 使用文件排序,效率會低一點。我們怎么優(yōu)化呢?

  • 因為數(shù)據(jù)是無序的,所以就需要排序。如果數(shù)據(jù)本身是有序的,那就不會再用到文件排序啦。而索引數(shù)據(jù)本身是有序的,我們通過建立索引來優(yōu)化 order by 語句。
  • 我們還可以通過調(diào)整 max_length_for_sort_data、sort_buffer_size 等參數(shù)優(yōu)化;

9. 拿不到鎖

有時候,我們查詢一條很簡單的 SQL,但是卻等待很長的時間,不見結(jié)果返回。一般這種時候就是表被鎖住了,或者要查詢的某一行或者幾行被鎖住了。我們只能慢慢等待鎖被釋放。

舉一個生活的例子哈,你和別人合租了一間房子,這個房子只有一個衛(wèi)生間的話。假設某一時刻,你們都想去衛(wèi)生間,但是對方比你早了一點點。那么此時你只能等對方出來后才能進去。

這時候,我們可以用 show processlist 命令,看看當前語句處于什么狀態(tài)哈。

10. delete + in 子查詢不走索引!

之前見到過一個生產(chǎn)慢 SQL 問題,當 delete 遇到 in 子查詢時,即使有索引,也是不走索引的。而對應的 select + in 子查詢,卻可以走索引。

MySQL 版本是 5.7,假設當前有兩張表 account 和 old_account, 表結(jié)構(gòu)如下:

CREATE TABLEold_account(idint(11) NOT NULL AUTO_INCREMENT COMMENT ‘主鍵Id',namevarchar(255) DEFAULT NULL COMMENT ‘賬戶名',balanceint(11) DEFAULT NULL COMMENT ‘余額',create_timedatetime NOT NULL COMMENT ‘創(chuàng)建時間',update_timedatetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘更新時間', PRIMARY KEY (id), KEYidx_name(name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8 ROW_FORMAT=REDUNDANT COMMENT='老的賬戶表';
CREATE TABLE account (
id int (11) NOT NULL AUTO_INCREMENT COMMENT ‘主鍵 Id',
name varchar (255) DEFAULT NULL COMMENT ‘賬戶名',
balance int (11) DEFAULT NULL COMMENT ‘余額',
create_time datetime NOT NULL COMMENT ‘創(chuàng)建時間',
update_time datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘更新時間',
PRIMARY KEY (id),
KEY idx_name (name) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8 ROW_FORMAT=REDUNDANT COMMENT='賬戶表';
`

執(zhí)行的 SQL 如下:

delete from account where name in (select name from old_account);

查看執(zhí)行計劃,發(fā)現(xiàn)不走索引:

但是如果把 delete 換成 select,就會走索引。

如下:

為什么 select + in 子查詢會走索引,delete + in 子查詢卻不會走索引呢?

我們執(zhí)行以下 SQL 看看:

explain select * from account where name in (select name from old_account); show WARNINGS; //可以查看優(yōu)化后,最終執(zhí)行的sql

結(jié)果如下:

selecttest2.account.idASid,test2.account.nameASname,test2.account.balanceASbalance,test2.account.create_timeAScreate_time,test2.account.update_timeASupdate_timefromtest2.accountsemi join (test2.old_account) where (test2.account.name=test2.old_account.name)

可以發(fā)現(xiàn),實際執(zhí)行的時候,MySQL 對 select in 子查詢做了優(yōu)化,把子查詢改成 join 的方式,所以可以走索引。但是很遺憾,對于 delete in 子查詢,MySQL 卻沒有對它做這個優(yōu)化。

日常開發(fā)中,大家注意一下這個場景哈,大家有興趣可以看下這篇文章哈:生產(chǎn)問題分析!delete in 子查詢不走索引?!

11、group by 使用臨時表

group by 一般用于分組統(tǒng)計,它表達的邏輯就是根據(jù)一定的規(guī)則,進行分組。日常開發(fā)中,我們使用得比較頻繁。如果不注意,很容易產(chǎn)生慢 SQL。

11.1 group by 的執(zhí)行流程

假設有表結(jié)構(gòu):

CREATE TABLEstaff(idbigint(11) NOT NULL AUTO_INCREMENT COMMENT ‘主鍵id',id_cardvarchar(20) NOT NULL COMMENT ‘身份證號碼',namevarchar(64) NOT NULL COMMENT ‘姓名',ageint(4) NOT NULL COMMENT ‘年齡',cityvarchar(64) NOT NULL COMMENT ‘城市', PRIMARY KEY (id) ) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8 COMMENT=‘員工表';

我們查看一下這個 SQL 的執(zhí)行計劃:

explain select city ,count(*) as num from staff group by city;

  • Extra 這個字段的 Using temporary 表示在執(zhí)行分組的時候使用了臨時表
  • Extra 這個字段的 Using filesort 表示使用了文件排序

group by 是怎么使用到臨時表和排序了呢?我們來看下這個 SQL 的執(zhí)行流程

select city ,count(*) as num from staff group by city;
  • 創(chuàng)建內(nèi)存臨時表,表里有兩個字段 city和num;
  • 全表掃描 staff 的記錄,依次取出 city = ‘X’ 的記錄。
    • 判斷臨時表中是否有為 city=‘X’ 的行,沒有就插入一個記錄 (X,1);
    • 如果臨時表中有 city=‘X’ 的行,就將 X 這一行的 num 值加 1;

遍歷完成后,再根據(jù)字段 city 做排序,得到結(jié)果集返回給客戶端。

這個流程的執(zhí)行圖如下:

臨時表的排序是怎樣的呢?

就是把需要排序的字段,放到 sort buffer,排完就返回。在這里注意一點哈,排序分全字段排序和 rowid 排序

如果是全字段排序,需要查詢返回的字段,都放入 sort buffer,根據(jù)排序字段排完,直接返回如果是 rowid 排序,只是需要排序的字段放入 sort buffer,然后多一次回表操作,再返回。

11.2 group by 可能會慢在哪里?

group by 使用不當,很容易就會產(chǎn)生慢 SQL 問題。因為它既用到臨時表,又默認用到排序。有時候還可能用到磁盤臨時表。

如果執(zhí)行過程中,會發(fā)現(xiàn)內(nèi)存臨時表大小到達了上限(控制這個上限的參數(shù)就是 tmp_table_size),會把內(nèi)存臨時表轉(zhuǎn)成磁盤臨時表。如果數(shù)據(jù)量很大,很可能這個查詢需要的磁盤臨時表,就會占用大量的磁盤空間。

11.3 如何優(yōu)化 group by 呢?

從哪些方向去優(yōu)化呢?

方向 1:既然它默認會排序,我們不給它排是不是就行啦。方向 2:既然臨時表是影響 group by 性能的 X 因素,我們是不是可以不用臨時表?

我們一起來想下,執(zhí)行 group by 語句為什么需要臨時表呢?group by 的語義邏輯,就是統(tǒng)計不同的值出現(xiàn)的個數(shù)。如果這個這些值一開始就是有序的,我們是不是直接往下掃描統(tǒng)計就好了,就不用臨時表來記錄并統(tǒng)計結(jié)果啦?

可以有這些優(yōu)化方案:

group by 后面的字段加索引order by null 不用排序盡量只使用內(nèi)存臨時表使用 SQL_BIG_RESULT

12. 系統(tǒng)硬件或網(wǎng)絡資源

如果數(shù)據(jù)庫服務器內(nèi)存、硬件資源,或者網(wǎng)絡資源配置不是很好,就會慢一些哈。這時候可以升級配置。這就好比你的計算機有時候很卡,你可以加個內(nèi)存條什么的一個道理。如果數(shù)據(jù)庫壓力本身很大,比如高并發(fā)場景下,大量請求到數(shù)據(jù)庫來,數(shù)據(jù)庫服務器 CPU 占用很高或者 IO利用率很高,這種情況下所有語句的執(zhí)行都有可能變慢的哈。

最后

如果測試環(huán)境數(shù)據(jù)庫的一些參數(shù)配置,和生產(chǎn)環(huán)境參數(shù)配置不一致的話,也容易產(chǎn)生慢 SQL 哈。之前見過一個慢 SQL 的生產(chǎn)案例,就是測試環(huán)境用了 index merge,所以查看 explain 執(zhí)行計劃時,是可以走索引的,但是到了生產(chǎn),卻全表掃描,最后排查發(fā)現(xiàn)是生產(chǎn)環(huán)境配置把 index merge 關閉了。

到此這篇關于總結(jié)12個MySQL慢查詢的原因分析的文章就介紹到這了,更多相關 MySQL慢查詢 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • mysql解壓縮方式安裝和徹底刪除的方法圖文詳解

    mysql解壓縮方式安裝和徹底刪除的方法圖文詳解

    這篇文章主要介紹了mysql解壓縮方式安裝和徹底刪除的方法,只有mysql徹底刪除干凈了,才可以裝另外新的版本,需要的朋友可以參考下
    2018-01-01
  • 在Win下mysql備份恢復命令

    在Win下mysql備份恢復命令

    假設mysql安裝在c:盤,mysql數(shù)據(jù)庫的用戶名是root,密碼是123456,數(shù)據(jù)庫名是database_name
    2010-02-02
  • MySQL操作數(shù)據(jù)庫實戰(zhàn)指南

    MySQL操作數(shù)據(jù)庫實戰(zhàn)指南

    這篇文章主要給大家介紹了關于MySQL數(shù)據(jù)庫操作庫的相關資料,MySQL數(shù)據(jù)庫是一個關系型數(shù)據(jù)庫管理系統(tǒng),所采用的SQL語言是用于訪問數(shù)據(jù)庫最常用的標準會語言,需要的朋友可以參考下
    2023-07-07
  • RHEL6.2編譯安裝MySQL 5.6.16過程分享

    RHEL6.2編譯安裝MySQL 5.6.16過程分享

    這篇文章主要介紹了RHEL6.2編譯安裝MySQL 5.6.16過程分享,還包括了mysql_secure_installation腳本運行指導,需要的朋友可以參考下
    2014-07-07
  • Mysql中創(chuàng)建高性能索引詳解

    Mysql中創(chuàng)建高性能索引詳解

    這篇文章主要介紹了Mysql中創(chuàng)建高性能索引詳解,索引相信大家都聽說過,但是真正會用的又有幾人,平時工作中寫SQL真的會考慮到這條SQL如何能夠用上索引,如何能夠提升執(zhí)行效率,文本就來詳細解讀如何創(chuàng)建高性能索引,需要的朋友可以參考下
    2023-07-07
  • MySQL中如何在原有的表中增加一列

    MySQL中如何在原有的表中增加一列

    這篇文章主要介紹了MySQL中如何在原有的表中增加一列問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-02-02
  • mysql免安裝制作使用說明

    mysql免安裝制作使用說明

    mysql免安裝版本的制作方法,需要的朋友可以參考下。
    2010-08-08
  • MySQL數(shù)據(jù)庫三種常用存儲引擎特性對比

    MySQL數(shù)據(jù)庫三種常用存儲引擎特性對比

    MySQL中的數(shù)據(jù)用各種不同的技術存儲在文件(或內(nèi)存)中,這些技術中的每一種技術都使用不同的存儲機制,索引技巧,鎖定水平并且最終提供廣泛的不同功能和能力。在MySQL中將這些不同的技術及配套的相關功能稱為存儲引擎。
    2016-01-01
  • Finished with error:Navicat運行SQL文件報錯的解決

    Finished with error:Navicat運行SQL文件報錯的解決

    這篇文章主要介紹了Finished with error:Navicat運行SQL文件報錯的問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-04-04
  • 詳解MySql存儲過程參數(shù)的入門使用

    詳解MySql存儲過程參數(shù)的入門使用

    這篇文章主要介紹了MySql存儲過程參數(shù)的入門使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-04-04

最新評論